Repository navigation
libc: a wait under an ignored SIGCHLD answers no child that ended - #1030
Conversation
wait_child forgot every ended child under one lock, then read the table again under another and reaped the first named child it found ended. A child that ended between the two reads was answered as reaped, where SIGCHLD ignored has it kept for no wait and the wait answer ECHILD once none is left. signal_sigchld_ignored saw it once. Each turn now reads every child's end once, under the lock that decides from it: `spawnreq::turn` makes the decision from that one read, and with SIGCHLD ignored it forgets each ended child and answers none. A child that ends after the read is the next turn's, which the poller's wake on its handle brings. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
Review of #1030, round 1 (head 92cc547, base origin/main 35e9301)ArchitectureThe fix sits where the defect was: libc's own child table in Checks requested
FindingsNo blockers. SUGGESTION SUGGESTION SUGGESTION VerdictLAND |
Problem
With
SIGCHLDignored, libc'swait_child(sdk/libc/src/spawn.rs) could answer a child as reaped, though POSIX has an ignoredSIGCHLDkeep no child for a wait and answerECHILDonce none is left.signal_sigchld_ignoredcaught it once, during #1012's rebase.The cause is a race between two reads. Each turn of the wait:
forget_ended(), which drops every child that has ended, under one lock;A child that ended between the two reads was found by the second, reaped and returned.
Resulting behaviour
Each turn reads every child's end once, under the one lock that decides from that read.
spawnreq::turnmakes the decision:SIGCHLDignored, every child that read saw ended is forgotten and none is answered. The wait answersECHILDonce none it names is left, and otherwise watches the rest.ECHILDwhen it names none.A child that ends after the read belongs to the next turn, and its handle's readiness wakes that turn.
forget_ended()shares the same removal (forget). The process track's record now names the new host law.Checks and evidence
All at
92cc5474e, onorigin/main35e930136:Host laws in
sdk/libc/arch/src/spawn_requests.rs:an_ignored_sigchld_answers_no_ended_childanda_wait_takes_the_first_ended_child. They are deterministic: one read's ends go in, and a decision comes out.Mutation (checked patch, built, restored): the ignored arm of
turnis skipped, which brings back the old answer, an ended child reaped underSIG_IGN.an_ignored_sigchld_answers_no_ended_childfails, exit 101.cargo run -- --ci host: exit 0, 101 steps green.cargo test --test toyos-build -- libc_children(QEMU x86-64):signal_sigchld_ignoredpasses. The boot still exits 1, onspawn_children_cap, which also fails on the base:posix_spawnansweredENOMEM(12).git revert -n, the same boot, then restored): child 240 ended bySIGSEGV(status0xb).Both are that case's 254 held children running out of guest memory.
issues/root-in-memory-costs-its-whole-size.mdrecords that ceiling (refused at 241,ENOMEM). This PR does not change it, because the spawn loop never waits.libc_childrenis red onmainat35e930136, independently of this change.The guest race itself is not reproduced on demand. The host laws establish the decision, and the single read is visible in the diff.
Evidence: https://github.com/ToyOSOrg/evidence/tree/97b3afdc21eb72c555c51dcb468920c971d5e24c (
waitrace/)🤖 Generated with Claude Code
https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q