Skip to content

vm: count derivation string-coercion against max-call-depth - #8

Open
dan-v wants to merge 1 commit into
psyclyx:mainfrom
dan-v:pr/coerce-depth-cap
Open

dan-v wants to merge 1 commit into
psyclyx:mainfrom
dan-v:pr/coerce-depth-cap

Conversation

@dan-v

@dan-v dan-v commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Problem

A self-referential list or attrset reaching a derivation argument recurses natively through the string coercion until the fiber stack faults: SIGSEGV, where Nix reports stack overflow; max-call-depth exceeded.

$ fix eval --read-write-mode -E 'let r = [ r ];
    in (derivation { name = "d"; system = "x86_64-linux"; builder = ":"; attr = r; }).drvPath'
Segmentation fault

Fix

Mirror Nix's mechanism: EvalState::coerceToString counts each coercion level against the same max-call-depth budget as function calls. The coercion here checks policy.max_call_depth (the max-call-depth setting, default 10000 as in Nix) once per level and raises the shared error. The forceThunkImpl native-stack soft guard backstops the recursion the same way it backstops deep thunk forcing, so an undersized stack degrades to a graceful error rather than a fault.

Verification

  • Boundary parity with nix-instantiate: nesting depths 5000 and 9000 evaluate to identical drvPaths on both evaluators, 11000 errors on both, and the cycle errors identically.
  • Unit tests cover deep-finite, cyclic-list, and cyclic-outPath vectors; the test binary segfaults on the parent commit.
  • Full e2e and unit suites otherwise unchanged.

A self-referential list or attrset reaching a derivation argument
(let r = [ r ]; ... attr = r) recursed natively through the coercion
until the fiber stack faulted: SIGSEGV on eval, where Nix reports
'stack overflow; max-call-depth exceeded'.

Mirror Nix's mechanism: EvalState::coerceToString counts each coercion
level against the same max-call-depth budget as function calls, so the
coercion here checks policy.max_call_depth (the max-call-depth setting,
default 10000 as in Nix) once per level and raises the shared error.
The forceThunkImpl native-stack soft guard backstops the recursion the
same way it backstops deep thunk forcing, so an undersized stack
degrades to a graceful error rather than a fault.

Nix-parity checked across the boundary: nesting depths 5000 and 9000
evaluate to identical drvPaths on both evaluators, 11000 errors on
both, and the cycle errors identically. Unit tests cover deep-finite,
cyclic list, and cyclic-outPath vectors; the test binary segfaults on
the parent commit.
psyclyx added a commit that referenced this pull request Aug 26, 2026
A self-referential value reached through string coercion recursed on the
native stack until the fiber faulted: 'let r = { outPath = r; }; in
toString r' was a SIGSEGV, as were the __toString and nested-list shapes
and the same values passed to builtins.path.

Count each nested coercion level against max-call-depth, as Nix's
EvalState::coerceToString does via addCallDepth, with the same native
stack backstop forceThunkImpl uses so an undersized fiber stack degrades
to an error instead of a fault. The depth is fiber state alongside
tryeval_depth, so a nested import VM continues its caller's coercion and
every clone of the walk shares one budget: the non-copying coercion in
vm/strings.zig and the toString attrs and list walks.

Cyclic values now report 'stack overflow; max-call-depth exceeded' and
finite chains are unchanged, byte-identical to the reference evaluator.

The derivation-specific coercion clones still recurse unguarded; PR #8
covers those and can adopt coercionEnter/coercionExit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant