Skip to content

feat(isolate): V8-isolate runtime (workerd) — Phases 1–3, off by default#302

Merged
sumansaurabh merged 13 commits into
mainfrom
plans/isolate-runtime
Jul 18, 2026
Merged

feat(isolate): V8-isolate runtime (workerd) — Phases 1–3, off by default#302
sumansaurabh merged 13 commits into
mainfrom
plans/isolate-runtime

Conversation

@sumansaurabh-slice

@sumansaurabh-slice sumansaurabh-slice commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds the V8-isolate runtime (runtime: "isolate"), the 5th AerolVM runtime, backed by Cloudflare workerd (Workers model). Host-mediated like WASM: no IP/TAP/iptables — one workerd OS process per isolate group, sandboxes loaded dynamically by id via a controller worker + workerLoader.
  • Ships Phases 1–3: enum + tenant_id schema + jail spec (P1); content-addressed JS/TS bundle store + POST /v1/js-bundles upload catalogue + cold create path (P2); group router, inbound HTTP fetch-proxy, exec=invoke, idle-TTL reaper, warm pool, bundle GC, egress boundary (P3). Plus the isolate constant + tenant_id across all 5 SDKs and a docs page.
  • Off by default (SB_ENABLE_ISOLATE=false; Terraform/install.sh WITH_ISOLATE=false) — merging activates nothing until an operator opts in.

Review fixes in this PR (pre-merge /review pass)

A full review + real-workerd integration run found and fixed several issues, including one that made the feature non-functional as committed:

  • CRITICAL — every isolate 500'd (was committed red): the per-sandbox egress shim wired globalOutbound: <dynamically-loaded worker stub>, which real workerd rejects (not a Fetcher; getEntrypoint() → "entrypoints of dynamically-loaded workers cannot be transferred"). Fixed to globalOutbound: env.EGRESS (static service Fetcher). Consequence: egress is fail-closed deny-all (see Known limitations).
  • Tenant isolation: jail was computed but never applied while SB_ISOLATE_USE_JAIL=true claimed confinement → now fail-closed (refuse to spawn when a required jail can't be realized) + honest jail_realizable boot log + Linux realization seam (priv-drop/chroot). Empty tenant_id collapsed all scoped tenants into one "default" process → now a stable per-owner group key.
  • Concurrency/correctness: group join↔last-member-teardown race (zombie sandbox) → router member-set under lock; idempotent duplicate create-with-id; host lifecycle data race; egress SSRF IP-range block (loopback/link-local/RFC1918/metadata) at dial time; expose_port listener leak on caddy failure; bundle-GC TOCTOU; cross-tenant blob delete now ref-counted (404 on unowned); file:// refs operator-only; isolate health surfaced in /health; warm-pool refill on daemon ctx; 4 non-compiling SDK doc examples.

Code-path diagram

flowchart TB
  C["POST /v1/sandboxes runtime=isolate"] --> R{"file:// ref & scoped caller?"}
  R -->|yes| Rej["reject: operator-only ref"]
  R -->|no| Res["resolve + stage bundle (Put, GC-pinned)"]
  Res --> Auth["authorizeIsolateTenantID<br/>empty+scoped -> per-owner key"]
  Auth --> Adm["admit"] --> Drv["driver.Create"]
  Drv --> AG["acquireGroup: add id to member set UNDER groupsMu"]
  AG -->|new key| Spawn["spawn workerd (jail: apply or FAIL CLOSED)"]
  AG -->|existing| Join["join group"]
  Spawn --> Load["host.Load (outside lock)"]
  Join --> Load
  Load -->|ok| Store["store.Create"]
  Load -->|fail| Rel["releaseFromGroup: member-set--, teardown iff empty"]
  Store -->|ErrSandboxExists| Idem["idempotent: re-Get, NO rollback of winner"]
  Store -->|other err| Unwind["Destroy + releaseAdmission"]
  Store -->|ok| OK["started"]
  subgraph egress["isolate outbound fetch"]
    EG["globalOutbound = static EGRESS service"] --> Proxy["Go egress proxy"]
    Proxy -->|no x-sb-id| Deny["403 deny-all (Phase-2 posture)"]
    Proxy -->|blocked IP range| Deny
  end
Loading

Sandbox boot impact

Isolate-only path (does not touch docker/fc/wasm boot). Per-create work, all called out:

  • One owner-scoped bundle resolve + a Put that is a fast no-op when the digest is already staged (the common case); first stage of a digest writes the index once. No network.
  • Group routing is a map lookup + single-flight spawn only on a tenant's first create (workerd spawn ~28ms cold / ~0.7ms warm inject, measured offline). Non-isolate runtimes unaffected.

Idempotency

  • Create: ret/concurrent same-id → store.Create PK conflict returns ErrSandboxExists, handled as idempotent success (re-Get; never rolls back the winner's shared driver state/admission). Group membership, host.Load, and admission are all keyed by id and idempotent.
  • POST /v1/js-bundles: content-addressed; re-uploading identical bytes → same digest, no error; whole Put under one lock.
  • expose_port (HTTP): EnsureHTTPListener returns the existing loopback listener; caddy upsert + store.UpsertPort are idempotent; never walks the TCP host-port pool (isolate rejects tcp/tls).
  • Group spawn: per-key single-flight — N concurrent first-creates → one workerd process.

Failure-path consistency

  • Create unwind is LIFO: host.Load fail → releaseFromGroup (member-set decremented under lock, group torn down iff it was the last member); store.Create fail (non-conflict) → driver.Destroy + releaseAdmission.
  • expose_port HTTP: caddy-upsert failure now releases the loopback listener before returning (no orphan listener/goroutine).
  • Bundle GC cannot reap a digest an in-flight create staged (staging pin), nor a digest a live sandbox pins.

L4 / host-port-pool changes

N/A — neither touched. Isolate is HTTP-only and host-mediated; it rejects tcp/tls and never calls TryReserveHostPort / allocateHostPort / EnsureLayer4. The store change is an additive tenant_id TEXT NOT NULL DEFAULT '' column (idempotent migration, provably balanced INSERT/SELECT/scan at 56 columns — verified), not the host-port region.

Known limitations (deliberate, safe-by-default)

  • Egress is deny-all. Per-sandbox allowlist attribution needs a workerd-supported mechanism (the loaded-shim globalOutbound is rejected by workerd) — tracked as a §4 follow-up. Until then every isolate outbound fetch is denied (safe).
  • Jail is fail-closed, not fully realized. Priv-drop + chroot land on Linux; seccomp BPF + cgroup limits + chroot population + a real-Linux-host integration test are follow-ups. SB_ISOLATE_USE_JAIL=true refuses to spawn where it can't realize the jail, so there is no silent-unconfined state.
  • Feature is off by default; not yet a hardened multi-tenant boundary for untrusted code.

Test plan

  • Full offline make test green (62 packages); gofmt clean.
  • New unit tests: egress matcher + SSRF IP-block, fail-closed jail, deterministic group teardown/join-race regression + -race concurrent churn, owner-key derivation, bundle ref-count delete.
  • All isolate integration tests pass against a real workerd binary (TestHostEndToEnd, TestDriverCreateEndToEnd, TestP0HostileIsolateContainment, all TestPhase3*) — including the ones that were red at HEAD before this pass.
  • Offline in-process create-latency benchmark harness (pkg/daemon/isolate_bench_integration_test.go): warm p50 ~0.7ms / cold ~28ms on darwin (not t3-comparable; live t3 bench pending).

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 11, 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
microvm 918cf14 Commit Preview URL

Branch Preview URL
Jul 18 2026, 03:24 AM

Design & implementation plan for a fifth runtime — V8 isolates (the
Deno/Workers model) — as the fast-JS, high-density tier complementing the
Firecracker microVM tier. Mirrors the wasm-runtime.md structure: reuse
surface, workerd engine choice, the isolate-group blast-radius decision
(P0 cross-tenant isolation gate), host-mediated networking, warm pool,
statekv durability, phasing, non-negotiables, and open questions.

Grounded in the existing runtime.Runtime / ContainerRuntime split, the
WASM driver seams, service dispatch, and the vmm/wasm warm-pool pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sumansaurabh
sumansaurabh force-pushed the plans/isolate-runtime branch from b001a6c to 45094e1 Compare July 17, 2026 05:22
sumansaurabh and others added 2 commits July 17, 2026 12:57
Reviewed 2026-07-17 (/office-hours design doc, 3 adversarial rounds 9/10,
+ /plan-eng-review 4 sections + Codex outside voice). Key amendments:

- Security re-scope (2.1): per-tenant OS process is the REQUIRED cross-tenant
  boundary (upstream: OSS workerd alone lacks hostile-code defense-in-depth);
  P0 gate reworded to drop cross-tenant memory-safety claims, executes end of
  Phase 3 as a tag-gated integration CI job
- TenantID group key: server-authorized only (forced co-residency attack)
- Group router: per-tenant single-flight; empty-group synchronous teardown
- Phase 1 gains: bundle-injection spike (hard exit criteria), jail workstream
  (+ --jitless eval), workerd binary distribution, tenant_id schema
- Demand checkpoint (10.1): pitch during Phase 2 gates Phase 4 density work
- /v1/js-bundles catalogue (experimental until checkpoint) + abuse controls
- poolcore extraction (rule of three); vmm/wasm migration deferred to Phase 4+
- Per-sandbox egress attribution + observability pulled into core scope
- Full 15-test map + GSTACK review report appended

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…runtime.md)

Fifth runtime skeleton — "isolate" (V8 isolates, Workers model), a peer to
docker/gvisor/firecracker/wasm:

- models: RuntimeIsolate enum accepted by ValidRuntime ahead of impl;
  durability set (ephemeral default, passivatable rejected, durable gated
  to Phase 5); server-authorized TenantID on CreateSandboxRequest + Sandbox
- store: tenant_id column (empty = null tenant, owner_ref convention) wired
  through Create/Upsert/Get/List/ListByOwner/ListByRuntime + scanner, with
  round-trip + null-tenant back-compat regression test
- config: SB_ENABLE_ISOLATE, SB_ISOLATE_WORKERD_PATH, SB_ISOLATE_RUN_DIR,
  SB_ISOLATE_GROUP_GRANULARITY (per-tenant default / per-sandbox),
  SB_ISOLATE_USE_JAIL + chroot/uid/gid knobs, SB_ISOLATE_JITLESS;
  isolate rejected as host-default runtime
- internal/runtime/isolate: Runtime-only driver (AsContainerRuntime false)
  whose methods return ErrRuntimeNotImplemented; Ping stats workerd; jail
  spec (chroot + cgroup + drop-priv + seccomp allowlist with JIT/jitless
  variants, strict group-key sanitizer) with profile-invariant tests; 97.2%
  package coverage
- service: isIsolateSandbox dispatch in runtimeForSandbox/runtimeRef;
  gated createIsolateSandbox (flag + driver-registration gates, isolate
  option validation, §2.1 tenant authorization with forced-co-residency
  regression tests); platform volumes reject isolate
- daemon: wireIsolateRuntime behind cfg.EnableIsolate on worker nodes;
  SupportedRuntimes advertises "isolate" (cluster placement parity)
- distribution: install.sh --with-isolate (workerd v1.20260717.1 pinned,
  SHA-256 verified), Terraform with_isolate passthrough, upgrade runbook
- feasibility gates: P0 hostile-isolate gate specified as a tag-gated
  ratchet test (fails once the create path lands until implemented);
  §2.2 injection-spike harness measuring the cold-boot baseline

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sumansaurabh-slice sumansaurabh-slice changed the title docs(plans): V8-isolate runtime design plan feat(isolate): V8-isolate runtime plan + Phase 1 — enum, tenant_id, driver skeleton, dispatch, jail spec, workerd distribution Jul 17, 2026
sumansaurabh and others added 10 commits July 17, 2026 22:48
…l workerd

Ran the dynamic worker-loading path against workerd v1.20260717.1: a
controller worker with a workerLoader binding loads a distinct isolate per
name and invokes it over getEntrypoint().fetch(). Measured, one process, no
restarts:

  fresh inject (new isolate/req): p50 1.2ms  (target: ≤10ms)
  cached warm hit (same name):    p50 0.32ms  (provider does not re-run)
  cold spawn→first-200 baseline:  38ms        (injection beats it ~23×)

Decision: the injection path wins — Phase 2 builds the controller-worker +
WorkerLoader architecture; the spawn-per-process and config-regen fallbacks
are shelved. Encoded as TestInjectionSpikeDynamicLoad (tag-gated); plan §2.2
marked GREEN. Requires compatibilityFlags=["experimental"] + --experimental.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ate Phase 2)

pkg/jsbundle is the isolate runtime's bundle layer — the analog of pkg/wasmmod
for .wasm (plans/isolate-runtime.md §7). A bundle is the code a workerd isolate
runs: an ES module map + entry module + pinned compatibility date, identified
by the sha256 over a canonical (order-insensitive) serialization so create is
idempotent under retry and a failover peer resolves the exact same bytes.

- resolve.go: ref → Bundle. Digest (sha256:<hex> or bare 64-hex) loads from the
  store; file://path / *.js|.mjs|.ts builds a one-module bundle; any other token
  is an uploaded name looked up tenant-scoped (the "no image, no registry" path).
- store.go: content-addressed blobs + tenant-scoped name pointers + a restart-
  durable index. Ships the plan's day-one abuse controls: per-bundle size cap
  and per-tenant bundle-count quota (null/operator tenant exempt); Delete for
  reference-counted GC.
- build.go: single-file entrypoint → bundle (Phase-2 hot-path default; multi-
  module esbuild is a later seam).

85.3% coverage. No workerd dependency — pure Go, fully offline-tested.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hase 2)

pkg/isolate is the workerd engine wrapper (plans/isolate-runtime.md §7). One
Host owns one workerd process per isolate group, implementing the injection
architecture the §2.2 spike validated:

- config.go generates the group's capnp config + a tiny CONTROLLER worker with
  a workerLoader binding. Sandbox traffic arrives keyed by x-sb-id (driver-set,
  unforgeable); the controller resolves the bundle from the HOST service
  binding (a Go bundle-server over a unix socket), then env.LOADER.get(id, …)
  loads/caches the isolate and invokes it. Egress is bound to EGRESS, a
  fail-closed deny-all service in Phase 2 (Phase 3 swaps in the per-sandbox
  attributed proxy) — an isolate cannot reach the network before policy exists.
- host.go: Start (gen config, serve bundle+egress sockets, spawn workerd, wait
  ready), Load/Unload (pin a sandbox's bundle; Unload returns remaining count
  for last-member teardown), Invoke (proxy an HTTP request to the controller),
  Stop. "No such sandbox" is a clean 404 (bundle probe), distinct from a 502
  bundle-server error and the isolate's own fetch response.

Offline tests cover every pure path at 100% (config gen, loader-id validation,
Load/Unload counting, bundle-server, fail-closed egress, config write, Stop
idempotency). The workerd-spawn paths (Start/waitReady/Invoke proxy) are
integration-only — proven by TestHostEndToEnd (tag-gated), which loads two
distinct isolates through the full stack against a real workerd and asserts
each serves its own bundle with correct warm-hit routing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wires the isolate driver to actually create sandboxes (plans/isolate-runtime.md
Phase 2), on the injection architecture the §2.2 spike validated.

- seams.go finalized post-spike: BundleResolver(tenant,ref)→*jsbundle.Bundle,
  GroupHost (Load/Unload/Invoke/Stop, satisfied by *pkg/isolate.Host), and
  HostSupervisor.SpawnGroup(spec)→GroupHost.
- grouprouter.go: per-tenant (or per-sandbox) group key → one workerd process,
  with PER-KEY SINGLE-FLIGHT so N concurrent first-creates for a tenant spawn
  ONE process (§11), and last-member teardown that removes the group before
  stopping it so a racing create spawns fresh.
- create.go: resolve bundle → acquire group (spawn under single-flight) → load.
  A load failure reaps a just-spawned empty group (§11 empty-group rule).
- driver.go: Start/Stop/Destroy/Inspect/ListManaged operate on the group
  router; Destroy routes the unload + triggers teardown. Snapshots stay
  unsupported (V8 has no serialize-running-isolate API).
- bundleresolver.go / hostsupervisor.go: production adapters over pkg/jsbundle
  and pkg/isolate.
- service/isolate.go: real boot path — durability normalize (isolate rejects
  passivatable, durable is Phase 5), admission (conservative: each sandbox at
  its declared footprint; per-group base RAM surfaced via expvar, not modeled),
  driver.Create, store.Create with LIFO unwind (Destroy + release on failure).
  No caddy on the boot path — external inbound routing is Phase 3, so a created
  isolate is invocable by the driver but not yet publicly routed (private-by-
  default).
- daemon: wireIsolateRuntime now builds the jsbundle store + resolver + workerd
  supervisor and injects them.

Boot-path impact: isolate creates now do bundle resolve (local, content-
addressed) + group-router lookup (in-memory) + first-create-per-tenant workerd
spawn. Other runtimes unaffected (the isolate branch is after the wasm branch,
one string compare). Idempotency: content-addressed bundle digest + single-
flight group spawn; retries join the existing group. Concurrent first-create
single-flight has a regression test.

Tests: driver 91% offline (group router, single-flight, per-tenant vs
per-sandbox, last-member teardown, load-failure reap, start/stop, adapters) +
TestDriverCreateEndToEnd (tag-gated) proving driver.Create → real workerd →
serve → teardown. Full offline make test green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mains

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…code

The isolate runtime's "no image, no registry" path (plans/isolate-runtime.md
§8): upload a JS/TS bundle over HTTP, reference it by name or digest on create.
Mirrors /v1/wasm-modules.

- pkg/models: CreateJSBundleRequest (source or modules map) + JSBundle
  catalogue DTO.
- pkg/jsbundle store: ListDigests / TenantOwns / NamesForTenant for the
  catalogue; null/operator tenant now tracked in the ownership index too (still
  quota-exempt) so its bundles list.
- service/isolate_bundles.go: CreateJSBundle / ListJSBundles / GetJSBundle /
  DeleteJSBundle, all OWNER-SCOPED via ownerRefForCreate/ownerScope — a
  user-scoped token only sees/resolves/deletes its own bundles (cross-tenant
  get/delete is a 404, not 403, so digests can't be probed); operator = global.
  Delete refuses a digest a live isolate sandbox still pins (store.ErrJSBundleInUse
  → 409), mirroring DeleteWasmModule.
- v1 routes + handlers: POST/GET/GET{id}/DELETE{id} under /v1/js-bundles,
  behind d.Auth. New route only — v1 soft-freeze respected (pr-review.md §6).
- service/isolate.go create path: owner-scoped name→digest resolution + stage,
  pinning "sha256:<digest>" onto module_ref before the driver runs, so an
  uploaded name a user references actually resolves under that user and the
  driver/failover resolve the exact bytes by digest.

pr-review.md call-outs:
- Idempotency (§1): content-addressed — re-uploading identical bytes yields the
  same digest with no error; concurrent duplicate uploads are safe under the
  store mutex; re-using a name deterministically repoints it; delete of an
  absent digest is a no-op.
- Boot-path (§2): createIsolateSandbox now does an owner-scoped bundle resolve
  + store stage before the driver — ISOLATE PATH ONLY (runtime=isolate, and
  only when the bundle store is wired). Local store read / file build + one
  content-addressed file write; no network, no lock beyond the store mutex.
  Other runtimes untouched.
- Failure-path (§4): handlers write only to the bundle store (no caddy). A
  bundle staged before a later create failure stays in the store — harmless
  (content-addressed, reusable, unreferenced-GC candidate); nothing to unwind.
- Abuse controls exist (per-bundle size cap, per-tenant quota) but default to
  unlimited for self-host; the managed control plane sets them.

Tests: v1 handler tests (create/list/get/delete/404/400), service tests
(owner scoping, idempotency, modules-form, in-use delete refusal, name→digest
create resolution), jsbundle store list/ownership tests. jsbundle 86.5%; new
service methods 82-93%. Full offline make test green (62 pkgs); isolate
integration tests still pass vs real workerd.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduces a new integration test for measuring the create latency of sandboxes in the isolate runtime. The test sets up a real HTTP API server and benchmarks the performance of sandbox creation, reporting cold and warm create times. It includes setup for the necessary environment and configurations, ensuring that the test runs against a real workerd binary. This addition enhances the testing coverage for the isolate runtime's performance metrics.
- Introduced a new link in the documentation sidebar for the Isolate Sandbox, providing a clear entry point for users.
- Added a comprehensive documentation file for the Isolate Sandbox, detailing its functionality, setup, and usage examples in TypeScript, Python, Go, Rust, and Java.
- Enhanced the configuration for the isolate runtime, including parameters for idle time-to-live, pool settings, and garbage collection intervals for unreferenced bundles.
- Implemented a warm pool for blank workerd hosts to optimize the creation of isolate sandboxes, improving performance and resource management.

This update enhances the usability and configurability of the isolate runtime, aligning with ongoing development efforts for the V8 isolate feature set.
- Changed variable names in the isolate sandbox code for consistency and clarity, including `module_ref` to `moduleRef` and `CreateSandboxRequest` to `CreateSandboxOptions`.
- Updated comments and documentation to reflect changes in the API and clarify the functionality of the isolate runtime.
- Enhanced the handling of group membership in the `acquireGroup` function to prevent race conditions during sandbox creation and destruction.
- Added tests to ensure the stability of the group management under concurrent operations, addressing potential issues with last-member teardown.

These changes improve code readability and maintainability while ensuring the robustness of the isolate runtime's functionality.
@sumansaurabh sumansaurabh changed the title feat(isolate): V8-isolate runtime plan + Phase 1 — enum, tenant_id, driver skeleton, dispatch, jail spec, workerd distribution feat(isolate): V8-isolate runtime (workerd) — Phases 1–3, off by default Jul 18, 2026
@sumansaurabh
sumansaurabh merged commit 60fccab into main Jul 18, 2026
12 checks passed
@sumansaurabh
sumansaurabh deleted the plans/isolate-runtime branch July 18, 2026 03:32
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.

2 participants