Skip to content

fuelUsed is a constant 10 for while and do...while loops that finish under the budget #43

Closed

Description

@siktec-lab

fuelUsed stays at a constant 10 for while and do...while loops that complete under the fuel budget, regardless of how many iterations run. Equivalent for loops report fuel proportional to the work. The fuel limit itself is enforced correctly for every loop shape (each traps at the budget), so this is a reporting problem, not an enforcement problem.

Repro (fuel.js)

for (const n of [1000, 100000, 1000000]) {
  const f = Slice.evalSandbox(`let x=0; for (let i=0;i<${n};i++) x+=i; x`, { fuel: 1e12 });
  const w = Slice.evalSandbox(`let x=0, i=0; while (i < ${n}) { x += i; i++; } x`, { fuel: 1e12 });
  console.log(`n=${n}: for fuelUsed=${f.fuelUsed}  while fuelUsed=${w.fuelUsed}`);
}
for (const fuel of [100000, 10000000]) {
  const r = Slice.evalSandbox(`let x=0, i=0; while (i < 1000000) { x += i; i++; } x`, { fuel });
  console.log(`while 1M iterations, budget ${fuel}: ok=${r.ok} fuelUsed=${r.fuelUsed}`);
}
slice run fuel.js

Expected: while loops report fuel that grows with n, similar to the for loops.

Actual

n=1000: for fuelUsed=9  while fuelUsed=10
n=100000: for fuelUsed=405  while fuelUsed=10
n=1000000: for fuelUsed=4005  while fuelUsed=10
while 1M iterations, budget 100000: ok=false fuelUsed=100000
while 1M iterations, budget 10000000: ok=true fuelUsed=10

The last two lines show that the loop does consume fuel internally (it traps at a 100,000 budget), but when it completes under a larger budget the reported value is 10. The same happens through slice mcp (slice_eval_ts). do { i++; } while (i < n) behaves the same way.

Impact: hosts that report or bill on fuelUsed under-count work done in while loops, and the value cannot be used to size budgets.

Environment

  • Version: v0.10.6, commit c6b3e77
  • OS: Windows 11 x64, release build (cargo build --release)

Activity

  1. siktec-lab commented on Nov 10, 2026

    @siktec-lab
    CollaboratorAuthor

    Fixed in #46 (2c3e16a):
    Tracked cumulative loop_iterations_executed in boa_engine::vm::RuntimeLimits on every IncrementLoopIteration opcode and charged (loop_iters / 250).max(1) in finish_boa_compartment_execution, so while and do...while loops that finish under budget report fuelUsed on the same scale as for loops (1,000 -> 14, 100,000 -> 410, 1,000,000 -> 4,010).

  2. siktec-lab commented on Nov 10, 2026

    @siktec-lab
    CollaboratorAuthor

    Re-tested on v0.10.7 (commit ea7c70c, Windows 11 x64 release build).

    fuelUsed for while and do...while now grows with the iteration count, which fixes the constant 10. However, the reported value and the enforced budget are still measured in different units for these loop shapes. For a for loop, the reported fuelUsed is exactly the smallest budget that lets the code complete. For while and do...while, the smallest budget that completes is about 250 times the reported value.

    Repro (fuel.js)

    const shapes = {
      for: "let x=0; for (let i=0;i<1000000;i++) x+=i; x",
      while: "let x=0, i=0; while (i < 1000000) { x += i; i++; } x",
      "do-while": "let i=0; do { i++; } while (i < 1000000); i",
    };
    for (const [name, code] of Object.entries(shapes)) {
      const used = Slice.evalSandbox(code, { fuel: 1e12 }).fuelUsed;
      let lo = 1, hi = 1e9;
      while (lo < hi) { const mid = Math.floor((lo + hi) / 2); if (Slice.evalSandbox(code, { fuel: mid }).ok) hi = mid; else lo = mid + 1; }
      console.log(`${name.padEnd(9)} reported fuelUsed=${used}  smallest budget that completes=${lo}`);
    }
    slice run fuel.js

    Actual

    for       reported fuelUsed=4005  smallest budget that completes=4005
    while     reported fuelUsed=4010  smallest budget that completes=1000010
    do-while  reported fuelUsed=4010  smallest budget that completes=1000009
    

    Expected: for every loop shape, a run that reports fuelUsed = N completes with fuel: N. Here a while loop reporting 4010 traps with any budget below about 1000010, for example:

    Slice.evalSandbox("let x=0, i=0; while (i < 1000000) { x += i; i++; } x", { fuel: 100000 })
    // ok: false, fuelUsed: 100000, "exceeded deterministic CPU instruction fuel limit (100000 ops)"

    It looks like the reporting path now uses the same per-iteration cost as for loops, while the enforcement path still charges while / do...while about one unit per iteration. Either both should charge per iteration, or both should use the batched cost; what matters is that they agree, so hosts can size budgets from observed fuelUsed.

  3. siktec-lab commented on Nov 11, 2026

    @siktec-lab
    CollaboratorAuthor

    Good catch — in v0.10.7, finish_boa_compartment_execution scaled loop_iterations_executed by 1 / 250 while set_loop_iteration_limit enforced 1 fuel unit per iteration (plus compile_while_loop emitted IncrementLoopIteration before the exit condition check).

    Fixed in #64 (474f7db):

    • Unified apply_boa_realm_runtime_limits (set_loop_iteration_limit(remaining_fuel)) and finish_boa_compartment_execution (consume_fuel(loop_iters)) on the exact same 1 fuel per iteration scale, and moved IncrementLoopIteration after the loop condition check in while so while and do...while execute the exact iteration count.
    • Verified with your binary-search script (for: 4005 == 4005, while: 1000010 == 1000010, do-while: 1000010 == 1000010).
  4. added a commit that references this issue on Nov 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions