Skip to content

unbounded native recursion crashes the process at four sites #34

Description

@wyattgill9

The VM's depth guard is real and works — fix eval --strict -E 'let f = n: if n == 0 then [] else [ (f (n - 1)) ]; in f 176000' returns error: stack overflow (possible infinite recursion), and max-call-depth is honoured. But several native recursive walks are not routed through it, and they take the process down instead.

Four sites, all on a ReleaseSafe build:

Parser, src/expr/compiler/driver.zig:95

$ python3 -c 'open("p.nix","w").write("("*200000 + "1" + ")"*200000)'
$ fix eval p.nix
Segmentation fault at address 0x16be2bfe0
$ nix-instantiate --eval p.nix
1

Constant folding, src/expr/compiler/fold.zig:171,260

$ python3 -c 'open("s.nix","w").write("+".join(["1"]*100001))'
$ fix eval s.nix
Segmentation fault at address 0x16a4afe80
$ nix-instantiate --eval s.nix
100001

builtins.fromJSON, src/expr/vm/builtins/serial.zig:490

$ python3 -c 'n=2000000; open("j.nix","w").write("builtins.typeOf (builtins.fromJSON \"" + "["*n + "]"*n + "\")")'
$ fix eval j.nix
Segmentation fault

__functor chain, src/expr/vm/closures.zig:455

$ fix eval --expr 'let g = n: if n == 0 then (x: x) else { __functor = self: g (n - 1); }; in (g 1000000) 1'
Segmentation fault at address 0x137dffff8

$ nix-instantiate --eval -E 'let g = n: if n == 0 then (x: x) else { __functor = self: g (n - 1); }; in (g 100000) 1'
error: ...                             # clean error at 10x less depth

The fromJSON case is worth separating out, because it does not crash reliably. Same input, same binary, 12 runs each:

depth clean exit crash
200,000 11 1
800,000 6 6
2,000,000 4 8

The crashes arrive as SIGSEGV, SIGABRT and once as SIGTRAP. A stack overrun that sometimes returns a correct answer is not a stack overrun that hits an unmapped page — it is one that lands in live memory and gets away with it.

That points at src/base/fiber.zig:463, which mmaps the fiber stack as one READ|WRITE region:

const stack_raw = std.posix.mmap(
    null,
    aligned_len,
    .{ .READ = true, .WRITE = true },
    .{ .TYPE = .PRIVATE, .ANONYMOUS = true },
    -1,
    0,
) catch return error.OutOfMemory;

There is no PROT_NONE page below the stack. An overrun walks into whatever is mapped next, which at these depths is another fiber's live frames. The observed crash is the lucky outcome; the silent successes in the table above are the unlucky one.

A guard page would not fix the missing depth checks, but it would convert every one of these from "sometimes corrupts a neighbour" into a deterministic fault, which seems worth having on its own.


fix 0.3.0 at 1f1a99cf, built -Doptimize=ReleaseSafe with Zig 0.16.0, darwin/aarch64. Reference implementation is Nix 2.34.8.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions