Releases: deeplethe/forkd
v0.5.2
v0.5.2 is a small feature + security-fix release headlined by forkd's first external contribution.
🎉 First external PR — @theflashwin
#230 plumbs hugepage-backed memfd snapshots end-to-end — Rust MemoryBackend::MemfdShared { use_hugepages: bool }, REST + CLI + Python SDK + TypeScript SDK all carry the flag, a forkd doctor hugepage-pool check, an ENOMEM → normal-page fallback with warning, and a fresh A/B bench harness (bench/live-fork-pause-window/bench-hugepages.py) that interleaves baseline/hugepages iterations for fair numbers.
Unusually complete for a first PR. Welcome aboard, @theflashwin.
What it does
Sandboxes spawned with --live-fork can now back their guest RAM with 2 MiB hugepages (MFD_HUGETLB | MFD_HUGE_2MB) instead of 4 KiB pages, reducing TLB pressure on spawn-many and live-BRANCH bulk-copy. Opt in per spawn:
# CLI
forkd fork --tag my-snapshot --live-fork --hugepages -n 10
# REST
POST /v1/sandboxes { "snapshot_tag": "my-snapshot", "live_fork": true, "hugepages": true, "n": 10 }
# Python SDK
sandbox = Sandbox.spawn(tag="my-snapshot", live_fork=True, hugepages=True)
# TypeScript SDK
const sb = await client.sandboxes.create({ snapshotTag: "my-snapshot", liveFork: true, hugepages: true });Pre-flight check: forkd doctor now reports HugePages_Total / HugePages_Free. Reserve pages with echo 512 | sudo tee /proc/sys/vm/nr_hugepages before use.
N=4 spawn numbers from @theflashwin's hardware are in the PR README. N=100 numbers are deferred to #234 — fresh dev-host setup needed.
🔒 axum-server 0.7 → 0.8 — closes #192
Drops the unmaintained rustls-pemfile dependency flagged by RUSTSEC-2025-0134. The daemon's TLS path now goes through rustls-pki-types directly. Internal-only change; HTTPS bind / cert loading are byte-compatible.
cargo audit on forkd v0.5.2 is now clean (0 advisories). (#232)
📚 Non-AI recipes
recipes/README.md now leads with a "by problem you're solving" table that surfaces CI test parallelization, database fixture forking, and browser farming alongside the AI-agent use case, instead of pitching forkd as AI-only.
New recipe recipes/ci-parallel-pytest ships a complete Python pytest fan-out demo (5 test files, ~30 tests across numpy / pandas / sklearn / regex / stdlib) backed by a forkd snapshot so each worker starts with import numpy etc. already paid for in the parent. Dev-box numbers: ~20 ms per worker batch-spawned vs ~2–3 s per cold container. (#231)
Install / upgrade
# Linux x86_64 binary (forkd + forkd-controller + forkd-uffd-handler)
curl -L https://github.com/deeplethe/forkd/releases/download/v0.5.2/forkd-v0.5.2-x86_64-linux.tar.gz | tar xz
# Python SDK
pip install -U forkd==0.5.2
# TypeScript / Node SDK
npm install @deeplethe/forkd@0.5.2No breaking changes — --hugepages requires --live-fork; everything else is additive. Existing spawn / branch flows work unchanged.
Thanks
- @theflashwin — first external contribution; hugepage memfd path
- Everyone who filed RUSTSEC-2025-0134 follow-ups (#192)
Full Changelog: v0.5.1...v0.5.2
What's Changed
- bench(v0.5.1): real-package pip-install chain spawn numbers by @WaylandYang in #228
- chore(sdk): bump Python + TS SDK versions to 0.5.1 — unblock PyPI publish by @WaylandYang in #229
- recipes: surface non-AI use cases + ship ci-parallel-pytest by @WaylandYang in #231
- chore(deps): bump axum-server 0.7 → 0.8 — closes #192 by @WaylandYang in #232
- feat(live-fork, memfd): Back Mem Snapshot with Hugepages by @theflashwin in #230
- release: v0.5.2 — hugepage memfd + axum-server 0.8 + non-AI recipes by @WaylandYang in #233
- fix(docker): include experiments/ in builder so workspace manifest parses by @WaylandYang in #235
New Contributors
- @theflashwin made their first contribution in #230
Full Changelog: v0.5.1...v0.5.2
v0.5.1 — guest kernel rebuild closes #218 + #225
v0.5.1 is a bug-fix release that finally makes pip install (and anything else that hits OpenSSL) work inside forkd guests.
The bug
Pre-v0.5.1 forkd shipped a Linux 4.14.174 guest kernel built 2021-07-14. That kernel lacked every entropy modernization since:
| Feature | Available since | 4.14.174 |
|---|---|---|
CONFIG_HW_RANDOM_VIRTIO (virtio-rng driver) |
— | not built in |
CONFIG_RANDOM_TRUST_CPU (RDRAND credit-init) |
— | not set |
random.trust_cpu= cmdline option |
Linux 4.19 | silently ignored |
CONFIG_VMGENID (FC-fork CRNG reseed) |
Linux 5.20 | absent |
Net effect: getrandom(2) blocked forever after FC restore, since the CRNG never marked itself initialized. Any guest workload that touched OpenSSL — pip install, urllib.request HTTPS, requests — hung indefinitely. Tracked in #218 (symptom) + #225 (root cause).
The fix
Swap to Firecracker's CI-blessed Linux 6.1.141 vmlinux. SHA-256 pinned: b36a4a1b10f33b9cfdcde3d1a787d9c090556a3edb211cd06d1f3f9a6c7e8724. End-to-end verified on the dev box on 2026-06-05:
| before | after | |
|---|---|---|
uname -r |
4.14.174 | 6.1.141 |
/dev/hwrng |
absent | crw, virtio-rng |
getrandom(GRND_NONBLOCK) |
EAGAIN | 16 bytes, errno=0 |
ssl.create_default_context() |
∞ | 0.01 s |
urllib.urlopen("https://pypi.org/…") |
∞ | 200, 0.25 s |
pip install numpy==2.0.2 |
exit 1, empty output | 21 s, exit 0 |
| VMGENID after restore | n/a | dmesg: random: crng reseeded due to virtual machine fork ✓ |
Two new helper scripts
scripts/install-guest-kernel.sh— downloads the FC CI vmlinux into/var/lib/forkd/kernels/vmlinux. SHA-256 pinned. Default path — recommended for almost everyone:sudo ./scripts/install-guest-kernel.sh
scripts/build-guest-kernel.sh— clones Linux 6.1 LTS, applies FC's reference microvm config (auto-asserts the entropy options), builds vmlinux from source. ~10 min on a 20-core box. For users who want to audit / tune the kernel config or target a non-x86_64 architecture.
forkd doctor's "kernel image" check now suggests these scripts when no vmlinux is found.
Migration
Existing snapshot vmstates from 4.14 restore fine on 6.1 — they just don't gain the new entropy device until they're re-baked. Run forkd from-image <image> --tag <tag> for each snapshot you care about to pick up CRNG init + /dev/hwrng + VMGENID.
Diff vs v0.5.0
- #224 — wire virtio-rng +
random.trust_cpu=on(forward-prep) - #226 — replace 4.14 guest vmlinux with Linux 6.1.141 (the actual fix)
- #227 — workspace version bump + CHANGELOG header
What this unlocks
The v0.5 chain story can now be demoed for real:
forkd snapshot-diff --from py-base --tag py-numpy --exec "pip install numpy"
forkd snapshot-diff --from py-numpy --tag py-pandas --exec "pip install pandas"
forkd snapshot-diff --from py-pandas --tag py-sklearn --exec "pip install scikit-learn"
forkd fork --tag py-sklearn -n 1 # walks the chain transparentlyPreviously this hung at "Collecting numpy" forever. v0.5.1 runs to completion.
What's Changed
- feat(vmm): forward-prep for #218 — virtio-rng + random.trust_cpu=on by @WaylandYang in #224
- fix(kernel): swap 4.14 guest vmlinux → Linux 6.1.141 — closes #218 + #225 by @WaylandYang in #226
- release: v0.5.1 — guest kernel rebuild + pip-install demo unblocked by @WaylandYang in #227
Full Changelog: v0.5.0...v0.5.1
v0.5.0 — diff-snapshot chains + v0.4 live BRANCH
v0.5.0 ships v0.5 (diff-snapshot chains) and folds in v0.4 (live-fork BRANCH) — the first formal release since v0.3.4.
v0.5: diff-snapshot chains
Stack snapshots into a chain — base → +numpy → +pandas → +sklearn — instead of one flat snapshot per dependency set. The daemon walks the chain at spawn time and assembles memory in one pass.
forkd snapshot-diff --from py-base --tag py-numpy --exec "pip install numpy==2.0.2"
forkd snapshot-diff --from py-numpy --tag py-pandas --exec "pip install pandas==2.2.3"
forkd fork --tag py-pandas -n 1 # daemon walks the chain transparently
forkd snapshot-info py-pandas # depth, parent, ancestors, dependents
forkd rmi py-numpy # 409 + actionable msg if children
forkd rmi py-numpy --cascade # subtree delete
forkd snapshot-compact --from py-pandas --to py-pandas-flat
forkd pack --tag py-pandas --out chain.tar.zst # v2 manifest, bundles every ancestor
forkd unpack chain.tar.zst # one snapshot dir per chain linkPhase 5 bench (RESULTS-v0.5.md)
| chain head | depth | spawn p50 | per-link tax |
|---|---|---|---|
| base (flat) | 0 | 59 ms | — |
| +numpy | 1 | 751 ms | +692 ms |
| +pandas | 2 | 1 222 ms | +471 ms |
| +sklearn | 3 | 1 668 ms | +446 ms |
| flat-equivalent | 1 | 746 ms | — |
Correctness: 90/90 (100%) probe passes across L1/L2/L3 + flat-equivalent. The design's vmstate-drift question is empirically closed.
The ~460 ms per-link tax tracks SHA-256 of the base (1.1 GiB/s) — the mmap-once incremental verify optimization is queued for v0.6.
v0.4: live-fork BRANCH (also captured here)
Sub-50 ms source-pause BRANCH via UFFD_WP + memfd_create. 56 ms p50 / 64 ms p90 on a 1.5 GiB python-numpy source — 3.6× faster than v0.3 Diff at p50 and disk-independent. With wait: false the caller returns in ~70 ms while the background memory copy completes asynchronously (200× RT improvement for fire-and-forget BRANCH).
Reachable via mode: "live" / wait: false on REST, --live / --no-wait on CLI, and the Python / TypeScript / MCP SDKs. forkd doctor probes the kernel and FC prerequisites.
Requires Linux ≥ 5.7, vm.unprivileged_userfaultfd=1 (or CAP_SYS_PTRACE), and the vendored Firecracker fork (33-line mem_backend.shared patch, upstream proposal at #5912).
Real bugs caught during ship-out
Two bugs the test suite hadn't caught — both surfaced by the Phase 5 bench / Phase 6 E2E fixture:
- #217: Phase 2a chain-assembled
memory.binwas unlinked byrestore_many_with's startup sweep before FC's/snapshot/loadcould open it. Symptom: every spawn from a chained snapshot returned HTTP 500. Fixed; regression testwork_dir_sweep_preserves_chainstage_subdirectoryadded. - #219: Pre-Phase-4
forkd rmisilently orphaned chained children. Now 409 with actionable message +--cascade/--forcecarve-outs.
What's in this tag
forkd snapshot-diff(v0.5 Phase 2b) — build chain layersforkd snapshot-info(v0.5 Phase 4) — inspect chain shape + dependentsforkd snapshot-compact(v0.5 Phase 4) — flatten a chain headforkd rmi --cascade | --force(v0.5 Phase 4) — chain-aware deleteforkd pack/unpackmanifest v2 (v0.5 Phase 3) — chain-aware bundlesforkd doctorv0.4 checks (uffd_wp + memfd_create)mode: "live"BRANCH (v0.4) via REST / CLI / SDKsscripts/dev/v05-e2e.sh— 15-assertion smoke test for the whole v0.5 surface
Reproduce
End-to-end on a Linux + KVM host:
git checkout v0.5.0
cargo build --release -p forkd-cli -p forkd-controller
sudo target/release/forkd-controller serve --bind 127.0.0.1:8889 ...
FORKD_BIN=target/release/forkd FORKD_BASE_TAG=demo-pyt ./scripts/dev/v05-e2e.shFull v0.5 phase-by-phase notes: see CHANGELOG.md and the v0.5 tracking issue #216.
What's Changed
- chore: post-#146 cleanup — remove stale warnings + mark probes resolved by @WaylandYang in #154
- fix(cli):
forkd doctorno longer warns on idle TAP (admin-UP + no peer) by @WaylandYang in #155 - RFC: v0.4 live-fork via userfaultfd write-protect by @WaylandYang in #156
- v0.4 foundation: WpBranch primitive + 3 PoCs + wp-bench CLI by @WaylandYang in #157
- experiment: v0.4 Phase 4 PoC — WpBranch snapshot round-trip + restore by @WaylandYang in #160
- fix: address external issues #158 + #159 by @WaylandYang in #161
- security: fix bearer-token length oracle in constant_time_eq (#162) by @WaylandYang in #164
- docs: lead Quick start with forkd pull, plus v0.3.4 / doctor-screenshot polish by @WaylandYang in #165
- hub: add deeplethe/python-numpy v1 by @WaylandYang in #166
- hub: add deeplethe/playwright-browser v1 (M1.1 headliner) by @WaylandYang in #167
- hub: add deeplethe/postgres-fixture v1 (fork-per-test DB) by @WaylandYang in #168
- hub: add deeplethe/coding-agent v1 (SWE-bench) by @WaylandYang in #169
- hub: add deeplethe/nodejs v1 (generic JS base) by @WaylandYang in #170
- hub: add deeplethe/e2b-codeinterpreter v1 (E2B drop-in) by @WaylandYang in #171
- hub: add deeplethe/jupyter-kernel v1 by @WaylandYang in #172
- docs: close M1.2 — 9 recipes in Hub, hosting decision, README done by @WaylandYang in #173
- RFC: v0.4 user-facing API surface (CLI / REST / SDK) by @WaylandYang in #174
- fix(vmm): cap volume_device_name at MAX_VOLUMES (closes #175) by @WaylandYang in #176
- feat(controller): expose branch_concurrency via CLI + env var (closes #177) by @WaylandYang in #179
- fix(vmm): apply_diff rejects diff/base size mismatch (closes #180) by @WaylandYang in #182
- feat(metrics): expose forkd_branches_in_flight + cap (follow-up to #177) by @WaylandYang in #183
- packaging(systemd): add cgroup to RestrictNamespaces (closes #163) by @WaylandYang in #184
- docs(v0.4): vendor firecracker, document fork strategy by @WaylandYang in #185
- feat(vmm): v0.4 Phase 5a — memfd_create() helper for live-fork backing by @WaylandYang in #186
- feat(vmm): v0.4 Phase 5b — MemoryBackend::MemfdShared via restore_many_with by @WaylandYang in #187
- chore(vendor): refresh FC patch to v1.12 with full build-clean diff by @WaylandYang in #188
- docs(v0.4): add Phase 6 implementation blueprint by @WaylandYang in #189
- feat(vmm): Phase 6.1 — Vm::snapshot_vmstate_only + VmstateOnly FC patch by @WaylandYang in #190
- fix(docs): clear all rustdoc errors under -D warnings by @WaylandYang in #191
- docs(v0.4): Phase 6.1.5 — FC POST /uffd/wp endpoint shipped by @WaylandYang in #194
- feat(vmm): Phase 6.2 — Vm::memfd_handle + Vm::request_wp_uffd by @WaylandYang in #195
- fix(controller): /healthz auth bypass accepts trailing slash too by @WaylandYang in #197
- feat(uffd,vmm): Phase 6.3a — WpBranch::begin_with_external_uffd + PauseGuard by @WaylandYang in #198
- feat(controller): Phase 6.3b — mode="live" path in branch_sandbox by @WaylandYang in #199
- feat(controller): Phase 6.4 — wait=false live BRANCH + InFlightBranches by @WaylandYang in #200
- feat(controller): expose live_fork on CreateSandboxRequest by @WaylandYang in #201
- fix(v0.4): wire cross-process WP correctly + 2 latent bugs caught by E2E by @WaylandYang in #202
- feat(controller): Phase 7.1 — canonical
modefield on BRANCH by @WaylandYang in #204 - feat(cli): Phase 7.2 —
--liveand--no-waitflags onforkd snapshotby @WaylandYang in #205 - feat(sdk): Phase 7.3 —
mode/wait/live_forkacross Python, TS, MCP by @WaylandYang in #206 - feat(doctor,uffd): Phase 7.4 — uffd_wp + memfd_create capability checks by @WaylandYang in #207
- docs: align README + API.md + CHANGELOG with shipped v0.4 live-fork surface by @WaylandYang in #208
- bench(v0.4): Phase 7.5 — live BRANCH pause-window data on a clean source by @WaylandYang in #210
- feat(cli):
forkd fork --live-fork— memfd-backed children for local v0.4 path by @WaylandYang in #211 - feat(vmm): v0.5 Phase 1 — diff snapshot chain resolver + schema by @WaylandYang in h...
Hub: deeplethe/python-numpy v1
Hub snapshot pack for deeplethe/python-numpy — Python 3.12-slim + numpy preinstalled.
- Base image:
python:3.12-slim - Memory: 1536 MiB
- Compressed size: 15.8 MiB (1.50 GiB uncompressed, 97× zstd)
- Recipe:
recipes/python-numpy/
forkd pull deeplethe/python-numpy
sudo -E forkd fork --tag python-numpy -n 5 --per-child-netnsHub: deeplethe/postgres-fixture v1
Hub snapshot pack for deeplethe/postgres-fixture — PostgreSQL 16 with initdb complete and postmaster pre-launched.
- Base image:
postgres:16 - Memory: 1024 MiB
- Compressed size: 38.0 MiB (1.00 GiB uncompressed, 26.9× zstd)
- Recipe:
recipes/postgres-fixture/ - Per-child fork-per-test: ~10 ms (vs ~2 s for a fresh container start + initdb).
forkd pull deeplethe/postgres-fixture
sudo -E forkd fork --tag postgres-fixture -n 20 --per-child-netns --memory-limit-mib 512Each child accepts psql at 10.42.0.2:5432 (user=forkd, db=forkd_test). Connect from the host via the child's netns or expose via your usual netns plumbing.
Hub: deeplethe/playwright-browser v1
Hub snapshot pack for deeplethe/playwright-browser — Microsoft's official Playwright image + Chromium pre-warmed with one about:blank tab.
- Base image:
mcr.microsoft.com/playwright:v1.50.0-jammy - Memory: 2048 MiB (Chromium OOMs on the 512 MiB default)
- Compressed size: 105.5 MiB (2.00 GiB uncompressed, 19.4× zstd)
- Recipe:
recipes/playwright-browser/ - Per-child fork cost: ~56 ms (vs 2–3 s cold-starting a headless Chromium container).
forkd pull deeplethe/playwright-browser
sudo -E forkd fork --tag playwright-browser -n 3 --per-child-netnsPer-child sb.eval("return await page.title()") routes to the warmed Chromium via the recipe's JSON-RPC bridge.
Hub: deeplethe/nodejs v1
Hub snapshot pack for deeplethe/nodejs — Node.js 22 slim runtime preloaded.
- Base image:
node:22-slim - Memory: 512 MiB
- Compressed size: 10.5 MiB (512 MiB uncompressed, 49× zstd)
- Recipe:
recipes/nodejs/
forkd pull deeplethe/nodejs
sudo -E forkd fork --tag nodejs -n 5 --per-child-netnsGeneric JS workload base — useful for npm-based agents, JS test runners, or any tool that needs a real Node process per child.
Hub: deeplethe/jupyter-kernel v1
Hub snapshot pack for deeplethe/jupyter-kernel — Jupyter scipy-notebook image, IPython + numpy + scipy + pandas + matplotlib pre-imported.
- Base image:
quay.io/jupyter/scipy-notebook:latest - Memory: 2048 MiB
- Compressed size: 15.5 MiB (2.00 GiB uncompressed, 132× zstd)
- Recipe:
recipes/jupyter-kernel/
forkd pull deeplethe/jupyter-kernel
sudo -E forkd fork --tag jupyter-kernel -n 5 --per-child-netnsChildren fork into a ready-to-execute-notebook-cell state — IPython kernel is already up, the scientific Python stack is already imported. Useful for parallel notebook execution / per-user kernel isolation.
Hub: deeplethe/e2b-codeinterpreter v1
Hub snapshot pack for deeplethe/e2b-codeinterpreter — the E2B code-interpreter image (Python + Node + Jupyter + ML libs) preloaded, drop-in for the E2B sandbox API.
- Base image:
e2bdev/code-interpreter:latest - Memory: 2048 MiB
- Compressed size: 21.6 MiB (2.00 GiB uncompressed, 94.8× zstd)
- Recipe:
recipes/e2b-codeinterpreter/
forkd pull deeplethe/e2b-codeinterpreter
sudo -E forkd fork --tag e2b-codeinterpreter -n 3 --per-child-netnsUse with forkd's Python SDK as a drop-in for e2b:
from forkd import Sandbox # same surface as: from e2b import SandboxHub: deeplethe/coding-agent v1
Hub snapshot pack for deeplethe/coding-agent — Python 3.12 + git + build-essential + gh CLI + pytest, the default for SWE-bench-style parallel evals.
- Base image:
python:3.12(wrapped with apt + pip extras via recipe) - Memory: 1024 MiB
- Compressed size: 15.2 MiB (1.00 GiB uncompressed, 67.3× zstd)
- Recipe:
recipes/coding-agent/
forkd pull deeplethe/coding-agent
sudo -E forkd fork --tag coding-agent -n 10 --per-child-netnsEach child gets an isolated git clone + pip install + pytest-ready Python workspace. The fork-per-test isolation that makes parallel SWE-bench rollouts safe.