Skip to content

sandbox_run: max_iterations is now a cumulative back-edge budget, and docs/BUILTINS.md still describes a per-loop cap #948

Description

@InauguralPhysicist

Follow-up from the #942 review (closed #940). Not a defect in that PR — the
behaviour is deliberate and argued for in its body. What is missing is that the
meaning of max_iterations changed, and the user-facing doc did not.

What changed

The pre-existing budget, g_loop_iterations, is saved and restored per call
frame
(frame->saved_loop_iter, src/vm.c). The new back-edge counter,
g_loop_backedge_count, is deliberately not — it is scoped to the whole
sandbox_run invocation, so an assembled chunk cannot reset its own budget by
calling a function. That is the right call for a DoS bound. The consequence is
that max_iterations now means two different things depending on which counter
trips: a per-frame loop cap for compiler-emitted OP_LOOP_CAP_CHECK output, and
a cumulative back-edge total for the entire run for everything else.

Measured

Two calls to a function whose loop is individually well inside the documented
max_iterations of 10:

ABI is 1
JUMP_BACK is 30
RETURN_NULL is 41
LOOP_CAP_CHECK is 63
CLOSURE is 38
SET_NAME_LOCAL is 27
POP is 35
GET_NAME is 25
CALL is 39

f_desc is [[LOOP_CAP_CHECK,3,0, JUMP_BACK,6,0, RETURN_NULL], [], [], 0, "f", []]

two is [CLOSURE,0,0, SET_NAME_LOCAL,0,0, POP, GET_NAME,0,0, CALL,0,0, POP, GET_NAME,0,0, CALL,0,0, POP, RETURN_NULL]
r2 is sandbox_run of [[ABI, two, ["f"], [f_desc], 0, "<module>", []], 10]
print of ["2 calls -> ok=", r2["ok"]]
one call two calls
00a74b7 (before #942) ok=1 ok=1
9c018e1 (after #942) ok=1 ok=0sandbox loop budget exceeded

Each call's loop exits gracefully at its own cap check; it is the second call's
back edges, added to the first call's, that trip. tests/test_sandbox_backedge_cap.eigs
section 3 covers the #772 guarantee for a flat module chunk, where the cap
check provably wins by one iteration — it does not cover the same loop inside a
called function, which is the case above.

What to do

  1. docs/BUILTINS.md's sandbox_run entry still reads "Loops are capped at
    max_iterations (default 1e6)". Say what the bound actually is: a total
    back-edge budget for the run, plus the per-frame cap check for compiler
    output, whichever trips first.
  2. Extend tests/test_sandbox_backedge_cap.eigs with the cross-frame case above,
    so the divergence is pinned by a test rather than rediscovered.
  3. Decide whether the default (1000000 in code — see Header comment says the sandbox loop default is 100M; the code uses 1,000,000 #941 for the header/code
    disagreement) is still the right number now that it is cumulative across a
    whole run rather than per frame. A sandboxed workload doing 1M total loop
    iterations across many loops now trips where it previously would not.

docs/SPEC.md's statement ("An absolute iteration cap exists only under an
explicitly armed sandbox budget") is checked and remains accurate.

Related: #940, #941.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdocumentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions