You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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"]]
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
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.
Extend tests/test_sandbox_backedge_cap.eigs with the cross-frame case above,
so the divergence is pinned by a test rather than rediscovered.
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.
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_iterationschanged, and the user-facing doc did not.What changed
The pre-existing budget,
g_loop_iterations, is saved and restored per callframe (
frame->saved_loop_iter,src/vm.c). The new back-edge counter,g_loop_backedge_count, is deliberately not — it is scoped to the wholesandbox_runinvocation, so an assembled chunk cannot reset its own budget bycalling a function. That is the right call for a DoS bound. The consequence is
that
max_iterationsnow means two different things depending on which countertrips: a per-frame loop cap for compiler-emitted
OP_LOOP_CAP_CHECKoutput, anda 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_iterationsof 10:ok=1ok=1ok=1ok=0—sandbox loop budget exceededEach 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.eigssection 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
docs/BUILTINS.md'ssandbox_runentry still reads "Loops are capped atmax_iterations(default 1e6)". Say what the bound actually is: a totalback-edge budget for the run, plus the per-frame cap check for compiler
output, whichever trips first.
tests/test_sandbox_backedge_cap.eigswith the cross-frame case above,so the divergence is pinned by a test rather than rediscovered.
1000000in code — see Header comment says the sandbox loop default is 100M; the code uses 1,000,000 #941 for the header/codedisagreement) 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 anexplicitly armed sandbox budget") is checked and remains accurate.
Related: #940, #941.