Repository navigation
signal_quit's reloading child waits blocked, and a read across an interrupt moves to libc_read_restarts - #1214
Conversation
`libc_children`'s `signal_quit` stalled twice at "the child that reloads". The child said `armed` and then called `pause`; a `SIGHUP` raised in between ran its handler on libc's own thread, and the `pause` waited for a handler that had already run. That it is what the two sightings went through is inferred: it is the only wait on that path, and a control that holds the child 200 ms short of its `pause` stalls the row the same way. - `reloads` blocks `SIGHUP` and `SIGTERM` before it says `armed` and waits in `sigsuspend` both times, so neither signal can run before the child waits for it. libc's `pause` is `sigsuspend` with the thread's own mask, so nothing `pause` showed is lost. - `reads` is deleted: `read` cannot unblock a signal atomically, so its window cannot be closed in C. `read` answering `EINTR` across a handler without `SA_RESTART` is `libc_read_restarts`'s claim, ordered by the roster, and a quit's `SIGINT` running its handler and interrupting a blocked call is the `ppolls` child's second `ppoll`. Closes issues/signal-quits-children-can-take-their-signal-before-they-block.md. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Review r1 of #1214 at
|
| arm | what I found |
|---|---|
| branch | run 42945464142 at d9c7f606f, PASS libc_children, signal_quit's new line present |
| main plus the hold | run 42937578767 at a42c5fab8 (parent 9349705ce), signal_quit: stalled driving the child that reloads, exit 142 |
| branch plus the hold | run 42945978144 at 9c8e9a94e (parent d9c7f606f), green |
- Each published patch equals the control commit's diff against its parent.
- Nothing under
tests/guest/libc,sdk/libcorkerneldiffers between9349705ceand the branch's base, so the red arm stands for it. - In the red arm the handler runs inside the control's own
usleep, not on libc's thread. The effect is the same: it has run beforepause. - The second window has no arm. It is the same mask and the same call, so reading suffices.
Deleting reads loses one composition and nothing libc decides separately.
- Claim 1 holds with a qualifier.
libc_read_restartsassertsreadanswersEINTRacross a handler withoutSA_RESTART, ordered by the roster (tests/guest/rust-tests/src/bin/libc_read_restarts.rs:90,131). Its signal isSIGCHLD, not a quit'sSIGINT. - Claim 2 is inaccurate. The
ppollschild's secondppollnever blocks: the child loops untilsigpendingshows the quit, and the descriptor is still ready (tests/guest/libc/signal_quit.c:141-153). It shows a pending quit taken at a call's entry, with its handler run andEINTRanswered. It does not show a quit waking a parked call. - What makes the deletion sound is something else: libc takes every signal by one signal-blind path (
sigreq.rs:349-359), whichreadenters atposix_io.rs:171. A handler waking a parkedreadislibc_read_restarts's; a quit becoming its signal is every remaining child's. - So no test now runs a
readacross an interrupt. That is the composition lost, and it is an arm of a track's exit (below).
The record is closed by the procedure, as far as its own file goes.
- The file is deleted and the commit message carries the story.
git grepfor the slug atd9c7f606fand atorigin/mainfinds no citation.- Its rule is in the program's header, for the child it applies to.
BLOCKER
issues/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md:213-216,231-234— stage 6.2, in progress, names both cases as its exit and as met insignal_quit— the record is false after this change and is not updated with it.- Its exit lists "a C child's
readof an empty pipe,SIGINThandled withoutSA_RESTART, answersEINTRon an interrupt" and "a C child inpausewhoseSIGHUPhandler reloads". - Its Met by libc: a child's end, an alarm and a quit reach a handler #1011 sentence places "the
readansweringEINTR, the reload inpause" insignal_quit. - After this change
signal_quithas noreadarm and nopause. Whoever closes 6.2 would rely on a "met" that no test holds. - Root requires issue records directly affected by a change to stay accurate. The PR body's "nothing outside
issues/cited it" is true of the slug and misses this record, which describes the cases. - Needed: the record says what holds each arm since this change. The reload waits in
sigsuspendwith both signals blocked. Thereadarm is either dropped from the exit, naming what holds its parts and that no test runs areadacross an interrupt, or re-homed (suggestion 2). Which of the two is a change to a briefed stage's exit, so the record must show it; I leave who signs it to the orchestrator.
- Its exit lists "a C child's
SUGGESTION
tests/guest/libc/signal_quit.c:4-5,14-16— the rule sits in thereloadsbullet, where "Both wait blocked until then" is hard to parse. The closing paragraph still reads as ifarmedalone orders the signal, which is the misconception the record was about. One clause there serves the next child: a child that waits for its signal holds it blocked until the call that unblocks it.- To keep the exit arm literal, add a third raiser to
libc_read_restarts: an interrupt sent once the roster shows the main thread parked in theread. That is one arm in an existing program and no new boot.
For the description
- Replace the
ppollssentence, in the body and in whichever commit corrects the record, with what the sources show: the take is signal-blind,libc_read_restartsholds a handler waking a parkedread, andppollsholds a pending quit taken at a call's entry. - "No coverage is lost" should read: one composition is no longer run, a
readacross an interrupt, andpauseis no longer called. - Verified as stated: the three runs and their revisions, the inference about the two sightings labeled as one, and that no host suite builds the C program.
SEND BACK
…s, and stage 6.2's record says where each arm is `signal_quit` lost its `read` child, and with it the one test of a `read` answering `EINTR` on an interrupt, which stage 6.2 of issues/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md names in its exit. By the orchestrator's decision the arm stays literal: `libc_read_restarts` gains a child that handles `SIGINT` without `SA_RESTART` and reads its empty standard input, and its parent sends the interrupt once the child has said `armed` and the roster shows its main thread parked. The read answers `EINTR` and the child ends 0. The record says what holds each arm since this change: the reload waits in `sigsuspend` with both signals blocked, `pause` is called by no test, and the `read` arm is `libc_read_restarts`'s. `signal_quit`'s header states the rule once, where it describes what `armed` orders. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Review r2 of #1214 at
|
| item | what I found |
|---|---|
| stage 6.2, reload arm | true: reloads blocks SIGHUP and SIGTERM before armed and waits in sigsuspend both times (signal_quit.c:49-66) |
stage 6.2, "pause, which no test calls now" |
true: no call under tests/ |
stage 6.2, read arm |
true: libc_read_restarts is a Rust program calling libc's read, in the same boot, ordered by the roster. The exit still says "a C child's read"; the Since #1214 sentence states the departure, which is enough |
| stage 6.2, mutation text "the reload's wait" | true of #1011's pause and of today's sigsuspend |
signal_quit.c:13-16 |
true of every child that waits for its signal (reloads, blocks, ppolls), and the right length |
PR body, ppolls sentence and coverage statement |
match round 1's findings |
| PR body, "no host check ran" | holds: host clippy excludes guest packages (build/clippy.rs:70, build/hostws.rs:118), and the runner compiled both programs |
SUGGESTION
tests/common/libcchildren.rs:13-14— the header's line forlibc_read_restartsnames theSIGCHLDhandler alone. A clause for the interrupt keeps the inventory whole.
For the orchestrator
The 900 s is a harness defect and needs a record. None exists under issues/ that I could find.
- What the 300 s bounds: one program's wait on the host, not the row (
tests/common/libcchildren.rs:30-31,54).run_testscales it by the host's boot time, up to 8×, and ends the wait only once the guest has also been silent 15 s, or at twice the scaled bound if it keeps talking (tests/common/qemu.rs:291-295,503-539,1904). It kills nothing in the guest. - Why the row stayed silent, by reading:
- The row prints failures only after its last program (
libcchildren.rs:54-66), so a stall says nothing when it happens. - The guest runner reads one
runline at a time and waits for that job (tests/guest/test-runner/src/main.rs:127-131,323). After a stall the nextrunis never read, so every later program spends its own full bound in silence. - A hang at any of the 23 programs but the last costs at least two scaled bounds before the row speaks, and far more for an early one.
- The row prints failures only after its last program (
- The driver's comment says the guest harness ends a silent guest long before its 900 s (
build/ci.rs:230-232). For this row that is false. - Which mechanism this run hit is not in the log: two bounds scaled above 1.5×, or one wait held to its backstop by a talking guest. The log prints no scale and keeps no serial capture.
- Consequence: the red loses its name and its console window, and the killed suite leaks its scratch directory, which reds a second step.
- Cheapest fix, a proposal for the record and not for this PR: the row stops at the first program that ends with no exit code, since that boot runs nothing more. Other rows that call
run_testrepeatedly on one boot may share this.
One sentence of the PR body states an inference as an observation. "The row's own 300 s bound did not end the hung job" is not shown by the log: the bound may have expired without a line. Suggested: the row said nothing for 900 s after quit_tree and the driver's silence guard ended the suite; whether the row's own bound had expired is not in the log, because the row reports failures only at its end.
SEND BACK
…and the row's silent stall is recorded The new arm of `libc_read_restarts` failed as a hang: a read the interrupt did not end stayed parked, the child's assertion never ran, and the parent waited in `waitpid`. The child's handler now says it ran on the child's standard output, and the parent, having read that, writes the child one byte before it waits. An interrupted read has answered `EINTR` already; a restarted one, or one whose handler ran elsewhere, answers the byte, and the child's assertion names the arm. `tests/common/libcchildren.rs` names the interrupt in its line for the program. New record, not fixed here: a program that stalls in `libc_children` is not named when it happens, later programs each spend their bound in silence, and the driver's 900 s guard ends the suite and leaks its scratch directory. Seen in run 42949792797, this PR's own control. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Review r3 of #1214 at
|
| claim | what I checked |
|---|---|
| head green | run 42953449011, headSha ead7b3f4c, PASS libc_children (7s), both libc_read_restarts lines present |
| control is one flag | 2be1bb7a2 has parent ead7b3f4c; its diff is byte-identical to the published patch |
| control reds by name | run 42953815079, headSha 2be1bb7a2, FAIL libc_children (7s), libc_read_restarts ended Some(101) with the child's sentence at line 134 and the parent's at line 177 |
| no stall | tty_interrupt ran after it in the control |
The two questions asked
The wait on the handler's word has a failure timeout and needs no second one.
- When it hangs: the child stays alive and its handler never runs, so the interrupt was lost or never woke the read. The parent then waits at line 168.
- Its timeout is the row's per-program bound (
tests/common/libcchildren.rs:32,56), which reportslibc_read_restarts ended Nonewith the stall verdict. - That is this suite's stated ceiling for every unbounded guest wait (
tests/guest/rust-tests/src/roster.rs:51-52). The same ceiling stands behind this program's other waits: thearmedread, the roster poll,waitpid, and theSIGCHLDarms' identical wait for their handler's word at line 206. - What is weak is the ceiling, not this wait. For this program, second to last, the row names the stall only if two scaled bounds fit inside the driver's 900 s; otherwise the red is unnamed, as in round 2's control. The new record owns that, and its exit fixes it for all 23 programs at once.
- A private ceiling here, as
signal_quit'salarm(120), would be a second mechanism for one program. I do not ask for it.
The handler is safe as written.
- It does an atomic add and one
writeto a pipe.writeis on POSIX's async-signal-safe list. - ToyOS's libc never runs a handler asynchronously. It runs at a blocking call's take point with libc's lock released, or on libc's own thread (
sdk/libc/src/signal.rs:301-329,386-406,548-553). No interrupted code holds a lock the handler could need. - The child's stdout is a pipe, so the write goes straight to the descriptor (
sdk/toyos/src/log/stdio.rs:325). errnoneeds no save: the passing path sets it after the handler returns, and on the failing path it is only printed.- The green head run exercises the handler inside the parked read; the control exercises it with a restart.
Records and prose
| item | finding |
|---|---|
| new record, format | status: open, kind: tooling, opened: 2026-11-11, no severity: valid per issues/README.md. No other record covers the subject. |
| new record, observation | matches run 42949792797: last line quit_tree's at 03:02:03, ended at 03:17:04 after 900 s, second step red on the leaked scratch directory, no serial artifact |
| new record, mechanism | 18 C programs plus 5 Rust is 23; the bound, backstop and build/ci.rs comment are as described. Labelled as read, not run. |
| new record, exit | labelled as the review's proposal, not decided |
tests/common/libcchildren.rs:13-15 |
accurate |
| PR body, corrected sentence | matches the log, and no longer claims the row's bound did not expire |
| PR body, checks table and "no host check ran" | revisions and runs match; libcchildren.rs changed in a comment alone across the whole PR |
SUGGESTION
issues/a-program-that-stalls-in-libc-children-is-not-named-and-the-driver-ends-the-suite.md:10-11— the lead sentence is unconditional and the mechanism below it is not.- A hang in the last program is named at its own bound. One in the next to last, the case seen, reaches the guard only when the bound is scaled past about 1.45×.
- Suggested: "When one of them hangs, the row says nothing until its last program's bound has also expired, and the driver's 900 s silence guard usually ends the whole suite first."
- Same file, line 24 — the bound is also scaled by the guest's vCPU oversubscription, not the host's boot time alone (
tests/common/qemu.rsbudget_smp).
LAND
Problem
libc_children'ssignal_quitstalled twice at "the child that reloads"(
issues/signal-quits-children-can-take-their-signal-before-they-block.md).The
reloadschild saidarmedand then calledpause. The parent sendsSIGHUPas soon as it has readarmed. libc runs a raised signal at once:in a blocking call of a thread that leaves it unblocked, or else on its own
thread. A
SIGHUPraised before the child had enteredpauseran itshandler there, and the
pausewaited for a handler that had already run.That the two sightings went through this window is inferred: it is the only
wait on that path, and the control below shows the window is enough.
Resulting behavior
reloadswaits blocked. It blocksSIGHUPandSIGTERMbefore itsays
armedand waits insigsuspendboth times. Neither signal can runbefore the child waits for it. libc's
pauseissigsuspendwith thethread's own mask, so nothing
pauseshowed is lost.readsis deleted fromsignal_quit, and its arm moves.readcannot unblock a signal atomically, so its window cannot be closed in C.
readacross an interrupt answeringEINTRwith
SIGINThandled withoutSA_RESTART, is now a child oflibc_read_restarts. Its parent sends the interrupt once the child hassaid
armedand the roster shows its main thread parked. This is theorchestrator's decision, to keep stage 6.2's exit arm literal.
says it ran, and the parent then writes the child one byte, which only a
read still parked takes.
libc_read_restartsalready held a handler waking a parkedread, forSIGCHLD.ppollschild holds something else: a pending quit taken at acall's entry, not a quit waking a parked call.
pauseis no longer called by any test.Checks
libc_childrenon the guest runner (QEMU x86-64, 2 CPUs). The control holdsthe
reloadschild 200 ms short of its wait after it saysarmed.ead7b3f4cSIGINTwithSA_RESTART2be1bb7a2an interrupt did not end the child's read with EINTR: it answered 1 (errno 0), the handler run 1 times9349705ceplus the holda42c5fab8stalled driving the child that reloadsd9c7f606fplus the hold9c8e9a94ed9c7f606f, before the reviewd9c7f606fNo host check ran: the change is two guest programs and issue files. The
guest runner compiles both programs and no host suite builds either;
tests/common/libcchildren.rschanged in a comment alone.Evidence: r3 https://github.com/ToyOSOrg/evidence/tree/59dc9df432e4d007e6628cc415196f58003c8e00/r3, r2 https://github.com/ToyOSOrg/evidence/tree/59dc9df432e4d007e6628cc415196f58003c8e00/r2, r1 https://github.com/ToyOSOrg/evidence/tree/59dc9df432e4d007e6628cc415196f58003c8e00/r1
Round 2's control, before the arm failed by name (run 42949792797,
277825f76): the row said nothing for 900 s afterquit_treeand thedriver's silence guard ended the suite, leaving a scratch directory in the
runner's
/tmp. Whether the row's own bound had expired is not in the log,because the row reports failures only at its end.
Records
issues/signal-quits-children-can-take-their-signal-before-they-block.md.Its rule is one clause in
signal_quit's header.issues/a-program-that-stalls-in-libc-children-is-not-named-and-the-driver-ends-the-suite.md,the harness defect round 2's control met. Its exit is the review's
proposal, not a decision.
issues/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md,stage 6.2: the exit's reload arm reads
sigsuspend, and the "Met by"sentence says what holds each arm since this PR. The review found this
record, which my search for the closed slug had missed.
🤖 Generated with Claude Code