volume-manager: try every mass-storage entry, watch the one we opened

The full suite caught a V4 regression: under AMD-Vi the device-manager tree
carries more than one mass-storage-identity entry (a phantom no driver is
bound to, which answers a consumer hello with NO channel). V4 split presence
from acquisition and picked the FIRST identity match blindly, so it kept
helloing the phantom (device 27) and never reached the real storage (device
31). V3's inline loop had skipped no-channel entries with `orelse continue`;
the split lost that.

Restore it: openAnyStorage tries each matching entry and takes the first whose
channel opens, recording its device id. Removal detection then watches THAT
specific device id leave the tree (isDevicePresent), not "any mass-storage" —
so a phantom that never leaves cannot mask a real removal. Both are bare
enumerates; the hello only happens while bringing a volume up.

Green: amd-iommu-usb-storage, fat-mount, volume-removal.
This commit is contained in:
Daniel Samson
2026-08-09 20:08:30 +01:00
parent 9e67a74232
commit e3ec9fa668
@@ -81,16 +81,24 @@ var fs_failed = false;
var restart_pending = false;
var restart_due_ns: u64 = 0;
/// The device-manager id of the mass-storage provider currently in the tree, or
/// null if none. Presence only — no consumer-hello, so calling it every poll
/// leaks nothing. This is how removal (the id disappears) and insertion (it
/// appears) are detected.
fn findStorageDevice() ?u64 {
const manager = manager_handle orelse opened: {
const handle = channel.openEndpoint("device-manager") orelse return null;
manager_handle = handle;
break :opened handle;
};
fn deviceManager() ?ipc.Handle {
if (manager_handle) |h| return h;
const handle = channel.openEndpoint("device-manager") orelse return null;
manager_handle = handle;
return handle;
}
const OpenedStorage = struct { device_id: u64, device: block.Device };
/// The first mass-storage provider whose block channel actually opens, with its
/// device id. A device-manager tree can carry more than one entry of the
/// mass-storage identity — a phantom that no driver is bound to answers a
/// consumer hello with NO channel — so this tries each and takes the first that
/// yields a channel, exactly as a filesystem's own acquisition loop does.
/// Called only when there is no volume (an insertion), so the hellos it makes
/// are not per-poll churn.
fn openAnyStorage() ?OpenedStorage {
const manager = deviceManager() orelse return null;
const Entry = device_manager_protocol.ChildEntry;
var start: u64 = 0;
while (true) {
@@ -108,19 +116,38 @@ fn findStorageDevice() ?u64 {
const entry = std.mem.bytesToValue(Entry, tail[index * @sizeOf(Entry) ..][0..@sizeOf(Entry)]);
if (entry.device_id == device_manager_protocol.no_device) continue;
if ((entry.identity >> 16) & 0xff != 0x08 or (entry.identity >> 8) & 0xff != 0x06) continue;
return entry.device_id;
const exchanged = driver.helloOn(manager, .consumer, entry.device_id, null, true) orelse continue;
const provider = exchanged.channel orelse continue; // a phantom / not-yet-bound entry
return .{ .device_id = entry.device_id, .device = .{ .endpoint = provider } };
}
start += count;
}
}
/// Consumer-hello the device manager for `device_id`'s block channel. Called
/// once per insertion (not per poll), so no per-poll handle churn.
fn openStorage(device_id: u64) ?block.Device {
const manager = manager_handle orelse return null;
const exchanged = driver.helloOn(manager, .consumer, device_id, null, true) orelse return null;
const provider = exchanged.channel orelse return null;
return .{ .endpoint = provider };
/// Whether `device_id` is still in the device-manager tree — a bare enumerate,
/// no consumer-hello, so it is cheap to call every poll. This is how removal is
/// detected: the specific device the mounted volume sits on disappears.
fn isDevicePresent(device_id: u64) bool {
const manager = deviceManager() orelse return false;
const Entry = device_manager_protocol.ChildEntry;
var start: u64 = 0;
while (true) {
const enumerate = envelope.Header{ .operation = envelope.operation_enumerate, .target = start };
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
const length = ipc.call(manager, std.mem.asBytes(&enumerate), &reply) catch return false;
const status = envelope.statusOf(reply[0..length]) orelse return false;
if (status.status != 0) return false;
const carried = @min(@as(usize, status.len), length -| envelope.prefix_size);
const tail = reply[envelope.prefix_size..][0..carried];
const count = tail.len / @sizeOf(Entry);
if (count == 0) return false;
var index: usize = 0;
while (index < count) : (index += 1) {
const entry = std.mem.bytesToValue(Entry, tail[index * @sizeOf(Entry) ..][0..@sizeOf(Entry)]);
if (entry.device_id == device_id) return true;
}
start += count;
}
}
/// Spawn the filesystem for `v`, confine it to the volume's range, and record
@@ -157,12 +184,13 @@ fn armRestart() void {
/// present-but-unreadable device does not leak a handle every poll) and `volume`
/// stays null — the next poll retries. A fresh medium gets a fresh supervision
/// budget.
fn bringUpVolume(device_id: u64) void {
fn bringUpVolume() void {
if (!bounce_ready) {
bounce = memory.dmaAlloc(512, memory.dma_coherent | memory.dma_shareable) orelse return;
bounce_ready = true;
}
const device = openStorage(device_id) orelse return;
const opened = openAnyStorage() orelse return;
const device = opened.device;
// Attach the read buffer to THIS device (a no-op without an enforcing IOMMU).
// The handle is kept, not closed, so it can be re-attached to the next
// device after a replug.
@@ -193,7 +221,7 @@ fn bringUpVolume(device_id: u64) void {
fs_restarts = 0;
fs_failed = false;
restart_pending = false;
volume = .{ .storage = device, .storage_device_id = device_id, .base_lba = found.base_lba, .block_count = found.block_count, .identity = found.identity, .id = volume_id };
volume = .{ .storage = device, .storage_device_id = opened.device_id, .base_lba = found.base_lba, .block_count = found.block_count, .identity = found.identity, .id = volume_id };
spawnFilesystem(&volume.?);
}
@@ -220,10 +248,12 @@ fn pollTick() void {
if (volume) |*v| spawnFilesystem(v);
return;
}
if (findStorageDevice()) |device_id| {
if (volume == null) bringUpVolume(device_id);
if (volume) |v| {
// Serving: watch for the specific device leaving (a pulled stick).
if (!isDevicePresent(v.storage_device_id)) removeVolume();
} else {
if (volume != null) removeVolume();
// Idle: try to bring a present storage device up.
bringUpVolume();
}
}