test(freestanding): probe the parse-depth guard, and correct the soak's stale premise - #815
Merged
InauguralPhysicist merged 1 commit intoAug 2, 2026
Conversation
…guard (InauguralSystems#758) tools/embed_stack_soak.sh described its 64 KiB rlimit as "the EigenOS boot-stack size". Per InauguralSystems#758 that is stale — EigenOS raised its boot stack as part of the InauguralSystems#361 fix. (EigenOS is a separate repository, so the 1 MiB figure is the issue's measurement, not one this repo's CI can check; the wording is scoped accordingly.) The number itself stays: 64 KiB is a deliberately tight PER-LEVEL regression budget, and that is the right design for the job this gate does. Policy branch taken, of the two InauguralSystems#758 offers: document that the depth guards are sized for the roomy boot stack and that the constrained profile relies on the stack canary — NOT make the guards stack-derived. Deriving them from the live stack is a substantially larger behavioural change to guards that ship on every profile, in exchange for a bound still only as good as its estimate of per-level cost. The soak's value is specifically as a per-level cost regression detector — it is what caught InauguralSystems#361 — and for that job a budget tighter than any real target is correct, not a bug. docs/FREESTANDING.md now records the sizing policy next to the gate it belongs to, so the next reader learns which of the two constraints is actually holding on a constrained target. Third, the gate no longer assumes the guard fires cleanly. It could not have tested it: measured on this harness, deeply nested source is SIGSEGV 20/20 under the 64 KiB rlimit, and the guard rejects reliably only from ~144 KiB (18/20 at 128 KiB, 0/20 at 112 KiB). So the soak binary gains a `depth-guard` mode — source nested past PARSE_MAX_DEPTH, built in .bss so the probe is not charged for its own input — run at 256 KiB, and the script requires all three of: a non-crashing exit, the guard's own diagnostic on stderr, and a still-usable state afterwards. Both mutants fail as they should: running that probe at 64 KiB instead reports rc=139, and shortening the input below the guard reports the levels were ACCEPTED. Numbers re-measured rather than restated. Two disagreements with InauguralSystems#758's table, both reported there: the parse-depth guard trips at 128 source paren levels, not 256 (parse_expression and parse_unary each bump the counter, so PARSE_MAX_DEPTH counts parser levels at ~2 per source level), and the stack floor is a probabilistic band, not a threshold — this harness is 20/20 at 32 KiB, 16/20 at 28 KiB, 0/20 at 20 KiB. Gates: make freestanding-check, tools/freestanding_smoke.sh, tools/embed_stack_soak.sh, make freestanding-libc-diff all pass; CC=gcc ./build.sh and the full suite pass 3367/3367. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nitjsefnie
force-pushed
the
docs/stack-soak-premise-758
branch
from
August 2, 2026 14:38
701d18b to
b6cc865
Compare
InauguralPhysicist
approved these changes
Aug 2, 2026
InauguralPhysicist
left a comment
Collaborator
There was a problem hiding this comment.
Approving. Verified on current main (83ff452, post-#811/#814 — merges cleanly):
- Both soak runs pass on the merged tree: the original 64 KiB REPL soak (104 steps) and the new 256 KiB depth-guard probe (
nesting too deepon stderr, clean unwind,6 * 7still evaluates after). - Non-vacuity mutant reproduced:
GUARD_LEVELS300→100 fails with100 nested levels were ACCEPTED — the parse-depth guard did not fire, exactly as documented. The three-part assertion (rc, stderr marker, post-guard eval) is the right shape — none of the legs can pass for the wrong reason. - The measurement work is the part I want to call out: banded pass-rates instead of single-shot
ulimitbisects is the correct method under ASLR, and correcting the issue's numbers with the method shown (44 KiB reliable floor, ×2 parse-depth bump rate, 144 KiB floor for the guard probe) is exactly what the issue deserved. Same for refusing to restate the 1 MiB EigenOS figure you couldn't check from this repo — it's private, so your caveat-and-attribute handling was right. - Taking the "no" branch on item 2 (guards stay boot-stack-sized, soak stays a deliberately tight per-level budget) is the decision I'd have made — the soak's value is as a cost detector, and #361 is the proof.
The stale ci.yml:214 display name: don't respin for it — I'll fix the one-line name string on main directly after this merges.
Merging.
InauguralPhysicist
added a commit
that referenced
this pull request
Aug 2, 2026
…uded Display-string only; the step has run tools/embed_stack_soak.sh all along and #815 added a second run inside it. Folded here rather than respinning a green contributor PR for a label. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Replaces the fossil comment in
tools/embed_stack_soak.sh, documents what the depth guards are actually sized for, and adds a second soak run that exercisesPARSE_MAX_DEPTH— so "the guard fires cleanly" is tested rather than assumed.Related Issues and Pull Requests
Fixes #758
Takes items 1 and 3 of your Suggested list, and the second branch of item 2.
Changes
tools/embed_stack_soak.sh: header rewritten — 64 KiB is now described as a deliberately tight per-level regression budget rather than EigenOS's stack. New second run:embed_stack_soak depth-guardunderulimit -s 256.tools/embed_stack_soak_main.c: adepth-guardmode reached only by argv, building its nested source in.bssso the probe isn't charged for its own input.docs/FREESTANDING.md: new "Stack budget and the depth guards" subsection under "Build profile" — that section already said "Two CI gates keep it honest:" with a bullet per gate, and the soak was the one freestanding gate missing from the list. Now three. (grep -rn "PARSE_MAX_DEPTH\|COMPILE_MAX_DEPTH" docs/returned nothing beforehand — there was no prior home.)CHANGELOG.md: one### Changedbullet.On item 2, I took the "no" branch: the guards are documented as sized for the 1 MiB boot stack, with the constrained profile relying on the canary. Making them stack-derived is a much larger behavioural change to shipped guards, whereas the soak is explicitly valuable as a per-level cost detector — it caught #361 — and a tighter-than-reality budget is right for that job. Overrule me if you'd rather have the guards reachable everywhere.
Testing
Your premise holds: the guard is unreachable at 64 KiB, and the comment is a fossil. Three of your numbers I could not reproduce, and since they're yours I'd rather show the measurements than quietly restate them. Every figure below is a 10–20 trial pass-rate, because single-shot
ulimitbisects are not repeatable here — ASLR moves the initial stack pointer, and my first sweep came back non-monotonic (40→segv, 38→ok, 37→segv, 36→ok), which is what forced the method change.The floor is a band, not a threshold. Hosted release CLI,
print of 1:Reliable floor 44 KiB, not 32–40 — your range is the flaky band's lower half. On the soak harness (the binary the gate actually runs, which is different and lower) the baseline is ~32 KiB, i.e. half the 64 KiB budget rather than 62%. The doc and script use the soak's own numbers, since that's the binary the budget applies to.
PARSE_MAX_DEPTHfires at 128 source levels, not 256.parse_expressionandparse_unaryeach bumpg_parse_depth, so the counter runs ~2 per parenthesis level:So the death-vs-guard gap is ~2.3× (56 vs 128), not the four-to-six× in the issue. Nesting death under 64 KiB is clean to 54 levels, banded 56–60, dead from 62 — your "between 40 and 60" is right at the top but 40–54 was 10/10 clean.
ulimit -s 128is not clean. It's 18/20 on the soak binary and 5/10 on the CLI. The reliable floor is 144 KiB, which is why the new probe runs at 256 KiB — ~1.8× margin. At 64 KiB the same input is SIGSEGV 20/20, which is the direct evidence the guard was untestable under the existing gate.The new run asserts three things together: exit 0 (ruling out the 139),
nesting too deepon stderr (so an unrelated rejection can't pass it), anddepth-guard OKon stdout, which is emitted only after the harness confirms the eval returned NULL and a following6 * 7still evaluates to 42 — i.e. the guard unwound cleanly rather than wedging the runtime.GUARD_LEVELSis 300, past 256 whether that constant is read as source levels or parser recursion, so the probe stays valid if the bump rate changes.Two mutants confirm it isn't vacuous: running the probe at 64 KiB instead of 256 fails with
rc=139, and droppingGUARD_LEVELSto 100 fails with100 nested levels were ACCEPTED — the parse-depth guard did not fire.All four steps of the freestanding job pass, plus
CC=gcc ./build.shand the suite at 3367/3367, exit 0.doc_drift_check.shexit 0.Follow-ups / Known Limitations
EigenOSis not a submodule here (git submodule statusempty, no.gitmodules), andgh api repos/InauguralSystems/EigenOS404s for me — it isn't in the org's 22 public repos, so it's private or renamed, anddocs/FREESTANDING.md:5links to a URL I can't reach. Every mention is attributed to PARSE_MAX_DEPTH is unreachable under a 64 KiB stack, and the soak gate's premise is stale (EigenOS is 1 MiB now) #758 and explicitly marked as uncheckable from this repo — which is itself part of the argument for not describing the soak budget in terms of it. I did soften the pre-existing EigenOS #UD heisenbug root cause: 12.7 KiB compiler stack frames + the EIGS_POISON hunter #361 sentence to "the 64 KiB boot stack EigenOS used at the time", since that past-tense claim is one the issue supports..github/workflows/ci.yml:214is now stale:name: Embedded-stack soak (REPL soak in a 64 KiB stack rlimit)no longer describes a step that also runs a 256 KiB probe. It's a display string and I didn't want to touch CI config unilaterally in a PR about a stale comment — but say the word and I'll fold in the one-line rename.COMPILE_MAX_DEPTH(128) is left alone. Your-fstack-usagefigure implies ~194 KiB for it on its own, so the same reachability question applies, but testing it needs a different probe shape and belongs in its own change.Checklist
make testpasses locally — 3367/3367, plus all four freestanding stepsdocs/BUILTINS.md— n/adocs/STDLIB.md— n/aGenerated by Claude Opus 5 (brief, implementation, review)