Skip to content

PARSE_MAX_DEPTH is unreachable under a 64 KiB stack, and the soak gate's premise is stale (EigenOS is 1 MiB now) #758

Description

@InauguralPhysicist

Found while measuring the stack cost of the #730 json_encode depth bound. Not a crash in shipped code — a gap between what the depth guards claim to bound and what actually binds first on the constrained profile.

Measured

Probe binary = the normal hosted build, run under decreasing ulimit -s:

Workload Stack floor
print of 1 — baseline, no recursion 32–40 KiB
json_encode at 191 levels 48–56 KiB
Nested expression x is ((((1)))), at a 64 KiB stack SIGSEGV between 40 and 60 levels

Cross-check from the other direction: a 250-level nested expression is rc=139 at ulimit -s 64, and a clean parse error(s) — aborting at ulimit -s 128.

The two findings

1. PARSE_MAX_DEPTH (256) cannot fire under a 64 KiB stack. The process dies at roughly 40–60 source nesting levels — four to six times below the guard. On that profile the depth limit is not the binding constraint; the stack is, and it binds far earlier. Same likely applies to COMPILE_MAX_DEPTH (128): compile_node + compile_node_inner measure 1552 B/level via -fstack-usage, so 128 levels is ~194 KiB on its own.

2. tools/embed_stack_soak.sh's comment is a fossil. It says the 64 KiB rlimit is "the EigenOS boot-stack size". EigenOS/src/boot.S:40 now allocates 1 MiB — raised as part of the #361 fix, with a comment explaining that 64 KiB was what produced the #UD heisenbug. So the gate tests against a stack size EigenOS no longer uses.

The baseline is the interesting number

~40 KiB is consumed before any recursive walker runs — startup, parse, VM. That is ~62% of the soak's 64 KiB budget gone before the first recursive descent, leaving ~24 KiB of usable headroom. The soak passes only because a REPL soak runs shallow programs.

That does not make it worthless — as a per-level cost regression detector it is what caught #361, and a tighter-than-reality limit is arguably the right design for that job. But it is not the worst-case bound its comment implies, and someone reading that comment would believe 64 KiB is the live EigenOS constraint.

Suggested

  1. Fix the comment in tools/embed_stack_soak.sh — state that 64 KiB is a deliberately tight per-level regression budget, not EigenOS's stack, and that EigenOS is 1 MiB with a canary.
  2. Decide whether the depth guards are meant to be reachable on the constrained profile. If yes, they need to be stack-derived rather than fixed counts, or the profile needs a documented minimum stack. If no, document that the guards are sized for the 1 MiB boot stack and the constrained profile relies on the canary instead.
  3. Consider a soak case that actually approaches a guard, so "the guard fires cleanly" is tested rather than assumed.

Not addressed in #757 (the json_encode PR), which only shares the measurement.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions