Skip to content

fix(ts): enforce max_memory and wall-clock limit for TypeScript - #2910

Merged
chaliy merged 6 commits into
mainfrom
claude/project-thread-hcbtls
Nov 10, 2026
Merged

chaliy merged 6 commits into
mainfrom
claude/project-thread-hcbtls

Conversation

@chaliy

@chaliy chaliy commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Михайло · project thread

What changed

  • TypeScriptLimits::max_memory is now enforced. ZapCode 1.5.1 stores the limit but never reads it, so one tenant's script could grow until an allocation failed, and Rust aborts the whole process when that happens. Each ts run now gets its own heap meter (the one Monty already uses, renamed from python_meter to vm_meter). Every instruction stops the run with memory limit exceeded once the run's live heap passes the limit.
  • Strings are shared, so [s, s, s] costs a few pointers but join, array toString, JSON.stringify, ''.concat(...) and console.log(...) each produce full copies. These builtins, plus repeat, split and replace/replaceAll, now work out the size of their result and refuse before allocating it.
  • On native targets, ts refuses to run unless MeteredAllocator is the global allocator. Every binding, example and test harness that enables typescript now installs it.
  • max_duration now bounds wall-clock time across host calls. Before, while (true) { await writeFile(...) } ran 30 s under a 500 ms limit because ZapCode restarts its clock at every resume. Each host call now checks the run's deadline, callbacks are capped at the time remaining, and the loop yields to the executor so caller timeouts can cancel the run.
  • New tests: a multi-tenant stress suite (ts_tenant_stress_tests, 3 rounds on every PR), a ts_fuzz target, a nightly ASan step with --features typescript, and a 300-round ts-stress-soak nightly job.

TM-TS-026, TM-TS-027. Every vendor change is a listed bashkit fork: edit (zapcode_vendor/fork-edits.json), checked in CI by scripts/check_zapcode_vendor.py.

Why

This was the hardening asked for before 0.21.0. Probing the ZapCode builtins after #2909 found three more ways to abort the host (template-literal doubling, concat doubling, spread doubling). The root cause was that memory limits were never enforced. The stress suite then found the time-limit bypass.

Before / After

Before, on main as of #2909, with the default 64 MB limit:

tmpl_dbl    let s='x'.repeat(1e7); for(...20){s=`${s}${s}`}   -> SIGABRT: memory allocation of 5120000000 bytes failed
concat_dbl  let a=[1]; for(...40){a=a.concat(a)}               -> SIGABRT: memory allocation of 1744830464 bytes failed
spread_dbl  let a=[1]; for(...40){a=[...a,...a]}               -> SIGABRT
json_big    JSON.stringify([o x32]) with a 10 MB string         -> exit 0, 320 MB string built
host loop   while(true){ await writeFile(...) }, 500 ms limit   -> ran 30 s (shell timeout)

After:

each doubling / amplification case -> exit 1, "memory limit exceeded: script heap reached max_memory"
host loop                          -> exit 124 after 0.50 s, "ts: execution timed out after 0.5s"

Locally: the full bashkit lib and integration suite with typescript,python passes, the TS examples run, cargo clippy --workspace --all-targets --all-features -D warnings is clean, the vendor drift check passes, and a 30-round stress soak passes.

Risk

  • Medium.
  • Scripts that hold large data now fail at max_memory where they used to succeed (or crash). Each host call snapshots the VM heap, so a script holding 8 MiB needs about 3x that during a readFile/writeFile call.
  • Embedders that enable typescript without bashkit::MeteredAllocator as the global allocator now get a clear error from ts.

Checklist

  • Tests added or updated
  • Backward compatibility considered

ZapCode 1.5.1 never reads memory_limit_bytes, and a failed allocation
aborts the process with every tenant. Template-literal, concat and spread
doubling all aborted the host. Each ts run now gets its own vm_meter
(the Monty meter, renamed from python_meter): every instruction stops the
run past max_memory, and output-amplifying builtins (join, toString,
JSON.stringify, console.log, concat, repeat, split, replace) size their
result before allocating. Native ts fails closed without MeteredAllocator.

A new multi-tenant stress suite found a host-call loop ran 30 s under a
500 ms limit because ZapCode restarts its clock at every resume. The
host-call loop now checks the run's wall-clock deadline, caps callbacks
at the remaining time, and yields.

TM-TS-026, TM-TS-027.
Mirror the Monty coverage for the vendored ZapCode VM: a ts_fuzz target
under the metered allocator (nightly fuzz matrix plus CI compile check),
an ASan step for the ts unit, threat-model and stress tests, and a
300-round ts-stress-soak job.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
bashkit ec09005 Commit Preview URL

Branch Preview URL
Nov 10 2026, 10:06 PM

@bashkit-pr-reviewer bashkit-pr-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memory-limit + wall-clock enforcement for ts: solid two-layer design (per-instruction heap check plus pre-sizing of fan-out builtins, deadline checked at every host suspension). No blocking issues; two hardening gaps below. I could not run the test suite here (no build in this environment), so this is code reasoning only.

@bashkit-pr-reviewer

bashkit-pr-reviewer Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Review summary (head ec09005, 6 commits)

Verdict: Both P2s from the previous review are resolved in this push. No new findings; no REQUEST_CHANGES. (No inline review submitted — nothing new to flag.)

Earlier findings, now closed:

  • P2 suspend-arm missing over-limit check — fixed: the Suspended arm now gates on meter.is_over_limit() (crates/bashkit/src/builtins/typescript.rs ~L1068-1076) with a TM-TS-026 comment, mirroring the Complete arm. Using the meter directly (rather than the thread-local over_limit()) is the correct choice in that async context, where no VM window is entered.
  • P2 wasm32 metering gap — addressed: knowledge/runtimes/zapcode-runtime.md (~L224-229) now states the allocator_installed check is skipped on wasm32, that bashkit-wasm installs the allocator, and names the residual risk (third-party wasm32 embedder without the allocator gets per-operation checks only, no cumulative heap limit). Documented and accepted.

Since the last review (heads 95ee6aa → 43b1831 → ec09005, +40 additions): the two fixes above, the wasm32 residual-risk note, and an expanded PR description (Before/After enforcement table, test/stress-suite inventory, 3× transient-memory risk note for host calls on 8 MiB heaps). fork-edits.json is unchanged; the new meter.is_over_limit() (vm_meter.rs) is consistent with reserve()'s limit > 0 semantics. Per-operation-vs-cumulative behavior on wasm32 without the allocator matches the documented claim.

Could not review: full unified diff (99 KB, truncated in tooling) — reviewed the head snapshot instead; no build/test in this environment, so the Before/After numbers and the new ts threat-model/stress tests are taken from the PR description, not executed. The sizing-walk recursion note from last time stands as a non-finding (depth bounded by meter/time limits).

@chaliy
chaliy merged commit b826173 into main Nov 10, 2026
57 checks passed
@chaliy
chaliy deleted the claude/project-thread-hcbtls branch November 10, 2026 22:40
@chaliy chaliy mentioned this pull request Nov 10, 2026
2 tasks done
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