From da7dcce64e3bec05300af6a08c0800daa4151bf0 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Mon, 10 Aug 2026 01:52:47 +0100 Subject: [PATCH] kernel/vfs: raise the mount ceiling for N volumes; refuse (not drop) a full table (S3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multi-volume makes the mount table the bottleneck: each volume installs one id-path mount and the system volume two FHS rewrites, so at the volume manager's maximum_volumes (16) the old cap of 8 is far too low. Raise maximum_mounts to 32 (headroom over the ~20-mount worst case) and declare its bounds block; drop it from the bounds allowlist. Fix a latent bug the higher pressure would expose: installMount silently dropped a mount when the table was full, and mountBackend returned true anyway — a full table was reported as a successful mount. installMount now returns whether it placed the mount, and mountBackend propagates a false so the mounting filesystem's harness logs "could not mount ". At-limit is now a refusal that is observed, not a silent success. (The full-table path has no host unit test: vfs.zig's tests are not wired into the host aggregate — its import graph reaches the freestanding kernel — so the correction rests on the propagated return and the truthful bounds block.) --- system/kernel/vfs.zig | 31 +++++++++++++++++++++++++------ tools/bounds-allowlist.txt | 1 - 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/system/kernel/vfs.zig b/system/kernel/vfs.zig index c3842f7..e2839d6 100644 --- a/system/kernel/vfs.zig +++ b/system/kernel/vfs.zig @@ -55,7 +55,17 @@ fn tokenIndex(t: u64) u64 { // --- the mount table --------------------------------------------------------- -pub const maximum_mounts = 8; +/// bound: prefixes mounted in the kernel VFS table at once +/// decided-by: ours +/// protects: the `mounts` table below +/// at-limit: refuse - installMount returns false and mountBackend propagates it; +/// the mounting filesystem's harness logs "could not mount " and the +/// mount simply does not exist (no silent success). Budget: the initrd's +/// top-level dirs (/system, /test) plus one id-path mount per volume and the +/// system volume's two FHS rewrites — a few over the volume manager's +/// maximum_volumes (16); 32 leaves headroom. +/// observed-by: the harness "file-system: could not mount " ring line +pub const maximum_mounts = 32; const maximum_prefix = 64; const maximum_rewrite = 32; @@ -156,7 +166,10 @@ pub fn setInitialRamdisk(image: []const u8) void { for (directories[0..directory_count], 0..) |*d, index| { const parent = parentOf(d.slice()); d.parent = directoryIndex(parent) orelse index; - if (parent.len == 1) installMount(d.slice(), .kernel_initrd, null, ""); + // Boot-time install of one mount per top-level initrd dir (/system, /test): + // provably few, far under maximum_mounts, so a full table here is + // impossible — but discard the result explicitly rather than assume it. + if (parent.len == 1) _ = installMount(d.slice(), .kernel_initrd, null, ""); } } @@ -167,7 +180,12 @@ fn directoryIndex(path: []const u8) ?usize { return null; } -fn installMount(prefix: []const u8, kind: MountKind, backend: ?*ipc.Endpoint, rewrite: []const u8) void { +/// Install (or remount-replace) a prefix. Returns false when the table is full +/// and no slot could be claimed — the caller must surface that, never report a +/// dropped mount as success. A remount of an already-mounted prefix reuses its +/// slot and always succeeds; a /protocol remount is refused-as-noop (returns +/// true: the first mount stands, nothing is dropped). +fn installMount(prefix: []const u8, kind: MountKind, backend: ?*ipc.Endpoint, rewrite: []const u8) bool { // Remount replaces: a restarted backend re-mounts its prefix. var slot: ?*Mount = null; for (&mounts) |*m| { @@ -176,19 +194,20 @@ fn installMount(prefix: []const u8, kind: MountKind, backend: ?*ipc.Endpoint, re // restarted FAT retakes /volumes/usb; letting it retake /protocol // would hand the whole naming layer to whoever asked second. // First mount wins, and init (PID 1) is always first. - if (std.mem.eql(u8, prefix, protocol_root)) return; + if (std.mem.eql(u8, prefix, protocol_root)) return true; if (m.backend) |old| ipc.dropRef(old); slot = m; break; } if (slot == null and !m.used) slot = m; } - const m = slot orelse return; + const m = slot orelse return false; m.* = .{ .used = true, .kind = kind, .backend = backend }; @memcpy(m.prefix[0..prefix.len], prefix); m.prefix_len = prefix.len; @memcpy(m.rewrite[0..rewrite.len], rewrite); m.rewrite_len = rewrite.len; + return true; } // --- resolve ----------------------------------------------------------------- @@ -406,7 +425,7 @@ pub fn mountBackend(prefix: []const u8, backend: *ipc.Endpoint, rewrite: []const if (!isInitrdCarveOut(prefix)) return false; } } - installMount(prefix, .backend, backend, rewrite); + if (!installMount(prefix, .backend, backend, rewrite)) return false; // table full for (&mounts) |*m| { if (m.used and std.mem.eql(u8, m.prefixSlice(), prefix)) m.owner = owner; } diff --git a/tools/bounds-allowlist.txt b/tools/bounds-allowlist.txt index 3eea7a4..8582a87 100644 --- a/tools/bounds-allowlist.txt +++ b/tools/bounds-allowlist.txt @@ -194,7 +194,6 @@ system/kernel/process.zig:write_buffer system/kernel/scheduler.zig:ipc_maximum_handles system/kernel/scheduler.zig:maximum_space_mappings system/kernel/vfs.zig:maximum_directories -system/kernel/vfs.zig:maximum_mounts system/kernel/vfs.zig:maximum_prefix system/kernel/vfs.zig:maximum_rewrite system/services/acpi/acpi.zig:blocks