Summary
#713 (x86 SPAWN syscall, fix/713-x86-spawn, merged via the #713 fix-round-2)
adds x86's first production path that lets a second userspace process exist
and touch the filesystem while another process's syscall is mid-flight. That
newly makes a pre-existing lock/parking shape on the ext2 filesystems
reachable in x86 production, on the gate's own -smp 1 configuration.
Both arches, both filesystems (widened by PR #744 review B1): the
lock/parking shape below is byte-identical, arch-neutral production code —
kernel/src/fs/ext2/mod.rs carries no #[cfg(target_arch)] anywhere near
it. aarch64 was originally checked and ruled NOT-LIVE on the theory that its
-smp 4+ configs structurally defuse the livelock; that reasoning was
falsified (see the corrected comment below) — the livelock generalizes to N
CPUs given N concurrent ext2 lock acquirers, and aarch64's stock boot
sequence already supplies most of that concurrency (4-5 fire-and-forget
service spawns, each an ext2 read, with no wait between them). What's
missing from today's aarch64 boot workload is one overlapping ext2 write
— a workload fact ("no boot service happens to write today"), not a
structural mitigation. An interactive bssh session doing rm/mkdir/etc.
while anything else reads (any exec, any sys_read) supplies it. This issue
is therefore latent-live on aarch64 too, not merely an x86 -smp 1 gate
artifact.
The same shape also exists on HOME_EXT2
(kernel/src/fs/ext2/mod.rs:1652, home_fs_write()/home_fs_read()) —
identical upgradeable_read().upgrade() / plain .read() construction,
never mentioned in the original filing. Any fix here must cover both
filesystems on both arches.
This is filed per this project's "any failure you find is your problem"
policy — the shape is pre-existing (every ext2 read has always done this),
but #713 is genuinely what makes it collidable on x86: before #713, x86
production booted exactly one userspace process (init) that never raced
itself against the filesystem. aarch64 has had multiprocess userspace for
longer, so the read-side concurrency precondition has been present there for
some time; only the write-side precondition is presently unmet by aarch64's
stock boot workload.
The shape
kernel/src/boot/init_image.rs::read_init_from_ext2 (used by sys_spawn,
kernel/src/syscall/handlers.rs:~2624) on x86; arch_impl/aarch64/syscall_entry.rs::load_elf_from_ext2
(used by sys_exec_aarch64/sys_spawn_aarch64) on aarch64 — same shape on
both:
pub fn read_init_from_ext2(path: &str) -> Result<Vec<u8>, &'static str> {
let fs_guard = crate::fs::ext2::root_fs_read(); // ROOT_EXT2.read()
let fs = fs_guard.as_ref().ok_or("ext2 root filesystem not mounted")?;
let inode_num = fs.resolve_path(path).map_err(|_| "init not found")?;
let inode = fs.read_inode(inode_num).map_err(|_| "failed to read inode")?;
if inode.is_dir() { return Err("init is a directory"); }
let elf_data = fs.read_file_content(&inode).map_err(|_| "failed to read init")?;
// ^ VirtIO block read; may PARK the caller
drop(fs_guard);
Ok(elf_data)
}
ROOT_EXT2.read() (kernel/src/fs/ext2/mod.rs:1470/1573) is held across
fs.read_file_content(&inode), which issues a VirtIO block request and waits
on completion — a genuine scheduler park on both arches (x86:
kernel/src/drivers/virtio/block.rs:122, block_request_gate_can_sleep();
aarch64: block_mmio.rs's wait_for_completion →
Completion::wait_timeout_uninterruptible → block_current_for_io_with_timeout,
same arch-neutral completion.rs).
The write side, root_fs_write() (kernel/src/fs/ext2/mod.rs:1588), and its
HOME_EXT2 twin home_fs_write() (mod.rs:1652):
pub fn root_fs_write() -> spin::RwLockWriteGuard<'static, Option<Ext2Fs>> {
ROOT_EXT2.upgradeable_read().upgrade()
}
is a spin (upgradeable-read-then-upgrade, spin crate's default
RelaxStrategy::Spin = a hardware pause with no OS yield, ever — arch
neutral), called from ordinary write syscalls any userspace process can
trigger — sys_open with O_CREAT, sys_write, unlink, rename,
mkdir, etc. (kernel/src/syscall/fs.rs, ten call sites total across both
filesystems; kernel/src/syscall/handlers.rs).
The collision
Both arches admit the same general shape: whenever one thread holds
WRITER (or has set UPGRADED en route to it) and parks for its own block
I/O, every other thread's attempt to acquire the same RwLock — reader or
upgrader — must spin non-yieldingly (no try_read-then-park fallback exists
at any of these call sites). If enough such spinners exist to occupy every
CPU simultaneously, the original lock-holder, though runnable once its own
I/O completes, has no CPU left to be dispatched on. Work-stealing needs an
idle CPU; there is none. The block-I/O completion timeout does not rescue
it — observing a timeout itself requires being scheduled. Unbounded
livelock.
x86 (-smp 1, the prod-profile gate's own configuration): trivial case,
N=1 — a single spinning writer alone monopolizes the only CPU that could
ever run the parked reader it's waiting on.
aarch64 (-smp 4+, Parallels 8 vCPUs): needs N concurrent ext2 lock
acquirers, N = CPU count. Stock boot already supplies N-1 of them (4-5
services' own spawn-time exec, each an ext2 read, fired with no wait between
launches) — see the corrected analysis in the comment below for exactly why
this is a workload fact today, not a structural mitigation, and what closes
the remaining gap.
This is a real starvation/livelock shape, not merely a slow path. It is
pre-existing (exec() and every ordinary file read do the same
park-while-holding-the-read-lock thing on both arches), but #713 is what
gave x86 production its first second process able to be the collider;
aarch64 has had that precondition for longer via its own multiprocess boot
sequence.
Why not fixed in #713 itself
#713's own diff does not introduce this lock/parking pattern — it only
(indirectly, via sys_spawn) creates the first x86-production scenario where
a second concurrent filesystem-touching process exists. The fix belongs to
the ext2 locking design (kernel/src/fs/ext2/mod.rs), which is out of
#713's diff surface, and is arch-neutral (both ROOT_EXT2 and HOME_EXT2,
both arches). Filing per the review's C5-adjacent finding rather than
silently absorbing it.
Suggested directions (not prescriptive)
- Make
root_fs_write()/home_fs_write()'s spin itself parkable/yield-aware
when preempt_count() > 0, mirroring how the read side already parks
instead of spinning.
- Or: never hold
ROOT_EXT2/HOME_EXT2's read or write guard across a
block I/O wait at all — copy inode metadata out under the lock, drop it,
then issue the block read/write against a snapshot, re-validating after
reacquiring if the filesystem could have changed underneath.
Either direction, applied once, covers both locks and both arches — the
code is shared.
Provenance
Found during the #713 x86 SPAWN review (fix/713-x86-spawn), citing the
review's own N5 analysis. Not independently reproduced with a live hang in
this pass (the 25+ boot batteries never collided two real processes on
-smp 1 this way — spawn_smoke_target exits before bsshd starts a
client) — filed as a structural risk with exact file:line evidence, not a
reproduced incident. The aarch64/HOME_EXT2 scope was added by a follow-up
verification pass (PR #744 review B1) after an initial aarch64-specific
check incorrectly concluded NOT-LIVE; see the corrected comment below for
the full aarch64 analysis and what it superseded.
Summary
#713(x86 SPAWN syscall,fix/713-x86-spawn, merged via the #713 fix-round-2)adds x86's first production path that lets a second userspace process exist
and touch the filesystem while another process's syscall is mid-flight. That
newly makes a pre-existing lock/parking shape on the ext2 filesystems
reachable in x86 production, on the gate's own
-smp 1configuration.Both arches, both filesystems (widened by PR #744 review B1): the
lock/parking shape below is byte-identical, arch-neutral production code —
kernel/src/fs/ext2/mod.rscarries no#[cfg(target_arch)]anywhere nearit. aarch64 was originally checked and ruled NOT-LIVE on the theory that its
-smp 4+ configs structurally defuse the livelock; that reasoning wasfalsified (see the corrected comment below) — the livelock generalizes to N
CPUs given N concurrent ext2 lock acquirers, and aarch64's stock boot
sequence already supplies most of that concurrency (4-5 fire-and-forget
service spawns, each an ext2 read, with no wait between them). What's
missing from today's aarch64 boot workload is one overlapping ext2 write
— a workload fact ("no boot service happens to write today"), not a
structural mitigation. An interactive
bsshsession doingrm/mkdir/etc.while anything else reads (any exec, any
sys_read) supplies it. This issueis therefore latent-live on aarch64 too, not merely an x86
-smp 1gateartifact.
The same shape also exists on
HOME_EXT2(
kernel/src/fs/ext2/mod.rs:1652,home_fs_write()/home_fs_read()) —identical
upgradeable_read().upgrade()/ plain.read()construction,never mentioned in the original filing. Any fix here must cover both
filesystems on both arches.
This is filed per this project's "any failure you find is your problem"
policy — the shape is pre-existing (every ext2 read has always done this),
but #713 is genuinely what makes it collidable on x86: before #713, x86
production booted exactly one userspace process (
init) that never raceditself against the filesystem. aarch64 has had multiprocess userspace for
longer, so the read-side concurrency precondition has been present there for
some time; only the write-side precondition is presently unmet by aarch64's
stock boot workload.
The shape
kernel/src/boot/init_image.rs::read_init_from_ext2(used bysys_spawn,kernel/src/syscall/handlers.rs:~2624) on x86;arch_impl/aarch64/syscall_entry.rs::load_elf_from_ext2(used by
sys_exec_aarch64/sys_spawn_aarch64) on aarch64 — same shape onboth:
ROOT_EXT2.read()(kernel/src/fs/ext2/mod.rs:1470/1573) is held acrossfs.read_file_content(&inode), which issues a VirtIO block request and waitson completion — a genuine scheduler park on both arches (x86:
kernel/src/drivers/virtio/block.rs:122,block_request_gate_can_sleep();aarch64:
block_mmio.rs'swait_for_completion→Completion::wait_timeout_uninterruptible→block_current_for_io_with_timeout,same arch-neutral
completion.rs).The write side,
root_fs_write()(kernel/src/fs/ext2/mod.rs:1588), and itsHOME_EXT2twinhome_fs_write()(mod.rs:1652):is a spin (upgradeable-read-then-upgrade,
spincrate's defaultRelaxStrategy::Spin= a hardware pause with no OS yield, ever — archneutral), called from ordinary write syscalls any userspace process can
trigger —
sys_openwithO_CREAT,sys_write,unlink,rename,mkdir, etc. (kernel/src/syscall/fs.rs, ten call sites total across bothfilesystems;
kernel/src/syscall/handlers.rs).The collision
Both arches admit the same general shape: whenever one thread holds
WRITER(or has setUPGRADEDen route to it) and parks for its own blockI/O, every other thread's attempt to acquire the same
RwLock— reader orupgrader — must spin non-yieldingly (no
try_read-then-park fallback existsat any of these call sites). If enough such spinners exist to occupy every
CPU simultaneously, the original lock-holder, though runnable once its own
I/O completes, has no CPU left to be dispatched on. Work-stealing needs an
idle CPU; there is none. The block-I/O completion timeout does not rescue
it — observing a timeout itself requires being scheduled. Unbounded
livelock.
x86 (
-smp 1, the prod-profile gate's own configuration): trivial case,N=1 — a single spinning writer alone monopolizes the only CPU that could
ever run the parked reader it's waiting on.
aarch64 (
-smp 4+, Parallels 8 vCPUs): needs N concurrent ext2 lockacquirers, N = CPU count. Stock boot already supplies N-1 of them (4-5
services' own spawn-time exec, each an ext2 read, fired with no wait between
launches) — see the corrected analysis in the comment below for exactly why
this is a workload fact today, not a structural mitigation, and what closes
the remaining gap.
This is a real starvation/livelock shape, not merely a slow path. It is
pre-existing (
exec()and every ordinary file read do the samepark-while-holding-the-read-lock thing on both arches), but #713 is what
gave x86 production its first second process able to be the collider;
aarch64 has had that precondition for longer via its own multiprocess boot
sequence.
Why not fixed in #713 itself
#713's own diff does not introduce this lock/parking pattern — it only
(indirectly, via
sys_spawn) creates the first x86-production scenario wherea second concurrent filesystem-touching process exists. The fix belongs to
the ext2 locking design (
kernel/src/fs/ext2/mod.rs), which is out of#713's diff surface, and is arch-neutral (both
ROOT_EXT2andHOME_EXT2,both arches). Filing per the review's C5-adjacent finding rather than
silently absorbing it.
Suggested directions (not prescriptive)
root_fs_write()/home_fs_write()'s spin itself parkable/yield-awarewhen
preempt_count() > 0, mirroring how the read side already parksinstead of spinning.
ROOT_EXT2/HOME_EXT2's read or write guard across ablock I/O wait at all — copy inode metadata out under the lock, drop it,
then issue the block read/write against a snapshot, re-validating after
reacquiring if the filesystem could have changed underneath.
Either direction, applied once, covers both locks and both arches — the
code is shared.
Provenance
Found during the #713 x86 SPAWN review (
fix/713-x86-spawn), citing thereview's own N5 analysis. Not independently reproduced with a live hang in
this pass (the 25+ boot batteries never collided two real processes on
-smp 1this way —spawn_smoke_targetexits beforebsshdstarts aclient) — filed as a structural risk with exact file:line evidence, not a
reproduced incident. The aarch64/
HOME_EXT2scope was added by a follow-upverification pass (PR #744 review B1) after an initial aarch64-specific
check incorrectly concluded NOT-LIVE; see the corrected comment below for
the full aarch64 analysis and what it superseded.