Skip to content

Security review fixes (secure-exec)#76

Closed
NathanFlurry wants to merge 10 commits into
split/runtime-previewfrom
security/runtime-fixes
Closed

Security review fixes (secure-exec)#76
NathanFlurry wants to merge 10 commits into
split/runtime-previewfrom
security/runtime-fixes

Conversation

@NathanFlurry

@NathanFlurry NathanFlurry commented Jun 19, 2026

Copy link
Copy Markdown
Member

Security review remediations for the secure-exec runtime. Each fix is its own jj/git revision and ships with a verifying test (bounded SAFEGUARD variants run by default; unbounded resource-saturation variants stay env-gated per the test-suite convention).

  • F-001 (SE-EXEC-04) — JS CPU watchdog never armed. register_v8_session hardcoded cpu_time_limit_ms: 0, which non_zero_option/normalize_cpu_time_limit_ms turned into None, so the TimeoutGuard watchdog was never wired and while(true){} could pin a core forever on the shared V8 runtime. Added V8_CPU_TIME_LIMIT_MS_ENV / V8_DEFAULT_CPU_TIME_LIMIT_MS (30s default, env-overridable, 0 disables) and a javascript_cpu_time_limit_ms resolver; passed the resolved value into CreateSession. Verifying test: javascript_infinite_loop_is_terminated_by_cpu_watchdog (in javascript_v8_suite).
  • F-002 — configured WASM stack byte budget was dead. AGENT_OS_WASM_MAX_STACK_BYTES was plumbed into the runner env but never read. Added wasm_stack_limit_bytes() (typed InvalidLimit on malformed budget) and a stack-exhaustion guard in the embedded WASM runner that terminates nonzero and attributes the termination to the configured cap. Verifying test: wasm_deep_recursion_respects_configured_stack_byte_limit (in wasm_suite).
  • F-003 (SE-EXEC-06) — heap bomb fatal-aborted the whole process. Hitting AGENT_OS_V8_HEAP_LIMIT_MB triggered V8's default fatal-OOM abort (SIGTRAP), crashing every concurrent tenant on the process-global runtime. Added install_heap_limit_guard (near-heap-limit callback that terminates the isolate cleanly with headroom) and wired it into both create_isolate and the snapshot-restore path. Verifying test: javascript_heap_allocation_bomb_is_capped_by_oom_guard (in javascript_v8_suite).
  • F-004 (SE-EXEC-08) — no-fuel WASM infinite loop was unbounded. resolve_wasm_execution_timeout returned None unless AGENT_OS_WASM_MAX_FUEL was set, so WasmExecution::wait() never terminated a never-returning guest. Added DEFAULT_WASM_EXECUTION_TIMEOUT_MS (30s) fallback so a timeout is always returned (exit 124). Verifying test: wasm::tests::wasm_execution_timeout_defaults_to_bounded_value_without_fuel_env.
  • F-005 (SSRF) — unspecified + CGNAT egress not classified. restricted_non_loopback_ip_range returned None for 0.0.0.0/:: (routes to loopback on Linux, bypassing the port-ownership gate) and for 100.64.0.0/10. Added unspecified and CGNAT arms for both IPv4 and IPv6. Verifying tests: ssrf_egress_classifier_tests (classifier labeling + filter_dns_safe_ip_addrs EACCES).
  • F-006 (SSRF) — IPv4-compatible IPv6 metadata bypass. Only IPv4-mapped (::ffff:a.b.c.d) was canonicalized, so ::169.254.169.254 bypassed the 169.254.0.0/16 metadata block. Added ipv4_compatible_embedded and recurse into the IPv4 classifier for IPv4-compatible addresses. Verifying test: classifier_denies_ipv6_spelled_metadata_addresses.
  • F-007 (SSRF) — reserved/multicast IPv4 ranges unclassified. Added 224.0.0.0/4 (multicast) and 240.0.0.0/4 (reserved, incl. 255.255.255.255 broadcast) arms; IPv6 spellings covered via the recursion. Verifying test: classifier_denies_reserved_and_multicast_targets.
  • F-008 (H) — guest fetch rode the embedding page's ambient credentials. The browser network adapter delegated to host fetch with default same-origin credentials, enabling credential exfil / CSRF via _networkFetchRaw. Pinned credentials: "omit" in both the fetch() and httpRequest() paths of createBrowserNetworkAdapter(). Verifying test: packages/browser/tests/runtime-driver/network-adapter.test.ts.
  • F-009 (SSRF) — host-keyed network policy never matched in browser. wrapNetworkAdapter only forwarded {url} to the policy callback, so a host-keyed deny (e.g. req.host === "169.254.169.254") never fired. Added a checkUrl helper that parses hostname/port and passes {url, host, port}. Verifying test: packages/browser/tests/runtime-driver/network-permission-host.test.ts.

Verification: full cargo build --workspace green; javascript_v8_suite, wasm_suite, secure-exec-execution --lib (91 passed), secure-exec-sidecar --lib (61 passed) all green; browser runtime-driver vitest (37 passed) and tsc --noEmit clean. CARGO_TARGET_DIR=/home/nathan/secure-exec/target.

Companion PR: the agent-os repo carries the matching consumer-side changes (agent-os → main).

NathanFlurry and others added 10 commits June 18, 2026 20:04
…-pkgs rename + publish @agent-os-pkgs/* software packages
…limit

Guest inline JS (e.g. while(true){}) ran unbounded on the shared, slot-bounded
V8 runtime because register_v8_session hardcoded cpu_time_limit_ms: 0 at every
CreateSession (0 normalizes to None, so no TimeoutGuard was armed), letting a
guest pin a CPU core and starve peers.

Resolve a default CPU-time budget (V8_DEFAULT_CPU_TIME_LIMIT_MS = 30000ms,
overridable/disable-able via AGENT_OS_V8_CPU_TIME_LIMIT_MS, mirroring the heap
limit env) and thread it through register_v8_session into the CreateSession
frame so the existing watchdog pipeline arms a TimeoutGuard.

Adds bounded safeguard test javascript_infinite_loop_is_terminated_by_cpu_watchdog
(runs by default, fenced behind a wall-clock watchdog) asserting a runaway loop
is terminated with a nonzero exit.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The operator-configured AGENT_OS_WASM_MAX_STACK_BYTES budget was plumbed
into the WASM runner env but never read by the engine, so the cap was
silently ignored (dead): runaway recursion only ever tripped V8's generic
default native stack guard with no attribution to the configured limit.

The engine now parses the stack byte budget (rejecting malformed values
like fuel/memory) and the WASM runner installs a stack-exhaustion guard
around wasi.start: when a stack byte limit is configured and the guest
exhausts the stack, the runner terminates nonzero and attributes the
failure to the configured budget instead of leaking the engine's generic
default-guard message.

Adds the bounded SAFEGUARD test wasm_deep_recursion_respects_configured_stack_byte_limit
(x-ref FAILURES.md#F-002), which now passes fast.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…minate isolate instead of fatal-aborting the shared process

Hitting the operator-configured AGENT_OS_V8_HEAP_LIMIT_MB cap previously
triggered V8's default fatal-OOM abort (SIGTRAP). Because the V8 runtime is
process-global, one tenant's heap bomb crashed the whole process and every
concurrent tenant (guest-triggerable cross-tenant host DoS).

create_isolate now installs a near-heap-limit callback (via the new
install_heap_limit_guard helper) on every isolate created with a heap cap,
covering both the fresh-isolate and snapshot-restore paths. On approaching
the cap the callback terminates the offending isolate's execution and returns
a small headroom bump so V8 can propagate the uncatchable termination
exception cleanly instead of aborting. The guest run ends with a nonzero exit.

Adds the bounded SAFEGUARD test javascript_heap_allocation_bomb_is_capped_by_oom_guard
to the javascript_v8 suite (runs by default, fenced behind a worker-thread
wall-clock watchdog); the unbounded RAM-saturation variant stays env-gated.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…100.64/10 as restricted egress in SSRF classifier
…) in SSRF classifier so ::169.254.169.254 is blocked
…apter so guest fetch cannot ride embedding page ambient cookies/HTTP-auth
…er so host-keyed egress denies (e.g. 169.254.169.254 metadata) are enforced
@NathanFlurry

Copy link
Copy Markdown
Member Author

Superseded by #79 — re-applied cleanly on main; this branch was cut from the abandoned split/runtime-preview base (2247-file diff).

@NathanFlurry
NathanFlurry deleted the security/runtime-fixes branch June 19, 2026 09:41
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