In one paragraph
Memory sharing had failed three ways (EXP-004, EXP-019, EXP-020), so we turned to time: checkpoint one warm session once, browser included, and restore copies of it instead of starting every session cold. It worked, which the author did not expect: with private CRIU 4.2 a full Drums L0 session with Chromium dumps in 0.81 s to a 253 MiB image and restores as isolated siblings. Measured time to agent-ready, no hygiene: cold p50 2.05 s, restore p50 0.98 s, ratio 2.09x (passes the registered 2x point rule, bootstrap interval 1.99 to 2.15). But a restore with no hygiene is a clone: identical Python random state, identical V8 Math.random, the same cookie, the same hostname (all shown in clone_hygiene.json). With the reseed that makes a restore usable, measured restore p50 is 1.19 s against cold 2.10 s, ratio 1.76x, below the 2x bar; memory is 1.038x of cold (passes the 1.05x bar, costs more than cold). The sibling-sharing audit C3 fails its literal wording on two host device nodes (/dev/null, /dev/urandom) that cold sessions share too, and passes an amended rule registered afterwards. Status: failed (the usable restore misses the bar); the no-hygiene 2.09x is preliminary and must not be quoted as the Drums restore speedup. "Drums" here means the L0 prototype: namespaces, overlayfs on the sandbox's own Ubuntu 24.04 root, Xvfb, openbox. No shared-runtime (L1) result exists, and no incumbent snapshot-revert latency was measured.
Setup
Host. One development sandbox: 4 vCPU, 16 GiB, kernel 6.18, cgroup v1, no KVM; hostname and provider not recorded.
Session. The L0 session of EXP-004 in CRIU-friendly mode: unshare PID/mount/IPC/UTS/net; overlayfs full root (lower: template and the sandbox's own /), pivot_root; private Xvfb, D-Bus, AT-SPI, openbox; the real OSWorld guest server; Chromium 141 --no-sandbox on a data-URL page titled drums-ready. Around 19 processes per session.
CRIU. Version 4.2 from private extracts of Ubuntu archive packages; nothing installed system-wide. CRIU 4.1.1 failed on this kernel: getsockopt of SO_PASSCRED/SO_PASSSEC on a TCP socket returns EOPNOTSUPP on kernel 6.18, so any session with a listening socket would not dump.
Arms.
| Arm | What starts | Agent-ready when |
|---|---|---|
| COLD | create the session, launch Chromium, load the page | a screenshot differs from a blank desktop in more than 10% of pixels AND the page title is present |
| RESTORE_FULL | criu restore of a checkpoint taken from a COLD session in that state, into a fresh network and mount namespace | same |
| RESTORE_SERVER_ONLY (partial, by definition) | restore a checkpoint without the browser, then cold-start Chromium | same |
Pre-registered bars (design values, commit 8f5ed06, 2026-10-24 07:02 UTC): C1 the public task e0df059f scores 0.0 then 1.0 from a restored session; C2 the restored screenshot shows the browser (more than 10% pixels differ from blank, title present); C3 two siblings restored from one checkpoint share no PID, no pipe, socket or eventfd inode, no writable regular file, no lock entry, with distinct Xvfb sockets and network namespaces ("any shared writable object fails C3"); latency RESTORE p50 at most 0.50 x COLD p50 and p95 not worse; memory restored at most 1.05 x cold.
Revision 2a of C3 (registered in 080a893 and narrowed in fc1d88d, both before the retest): a shared writable object is tolerated only if it is exactly /dev/null or /dev/urandom, a cold-pair control in the same pass shares the same object, and nothing else is shared. The original FAIL stays on the record.
Clone hygiene (registered in section 16.3 before any run): per restored sibling, set a per-sibling hostname; have the guest-server process reseed Python random (and numpy if loaded) from fresh os.urandom via a signal handler installed before the checkpoint; for a checkpoint declared clean, clear the default browser context's cookies and storage and open a new context (new renderer, new V8 isolate). A checkpoint not declared clean keeps its browser state by design, because erasing a real task's cookies would change the task.
sequenceDiagram participant Sup as Supervisor participant Cold as cold session participant CRIU as criu 4.2 (private) participant Sib as restored sibling Sup->>Cold: create (criu_friendly), browser up, page loaded Sup->>CRIU: dump -t init (0.81 s, 253 MiB) Sup->>Sup: keep the session's upper tmpfs as a read-only checkpoint layer loop each sibling Sup->>Sup: ledger mounts, link, veth; mount a fresh overlay with an empty upper Sup->>CRIU: restore --root overlay, external veth, X11 dir, device nodes CRIU->>Sib: processes resume Sup->>Sib: re-address veth, reseed hostname and RNG, wait for the guest server end
What ran
envd_linux.l1proto.criu_probe ran the registered ladder on 2026-10-24 (results commit ca07330, 07:41 UTC): R1 criu check; R3 dump and restore of a session with only the guest server and desktop; R4 the same with Chromium; R5 restore into fresh network and mount namespaces; then C1, C2, C3, 20 interleaved starts per arm with a warm page cache, and 3 memory trials. The same probe ran again with 10 starts per arm after dropping the page cache before every start (criu_probe_dropcache.json, not pre-registered), and again with the reseed on (probe_reseeded.json, commit ba21252, 08:41 UTC). tests/test_criu_clone_hygiene.py produced clone_hygiene.json and clone_task_semantics.json. 793 non-volatile /proc/sys entries were snapshotted before and after each probe.
Four changes were needed before a session would dump at all, each a finding: the session init had to re-exec itself after pivot_root so no mapping points into the detached old root; unshare --kill-child had to be cleared (the restoring CRIU's exit would otherwise SIGKILL the session; consequence: the kernel no longer kills orphans, only the ownership sweep does, see EXP-022); the overlay upper had to sit on a host-visible tmpfs; and CRIU needed --force-irmap, --ghost-limit, --file-locks, --external veth[...], external mounts for the X11 directory and six /dev nodes, plus a private ip.
Results
Mechanism C: CRIU 4.2 checkpoint and restore of a warm Drums L0 session with Chromium, without and with the clone-hygiene reseed failed2026-10-24 · dev-sandbox
| Metric | Value | Unit | n | Note |
|---|---|---|---|---|
| no hygiene: COLD time to agent-ready | 2.05 (p95 2.19) | s | 20 | |
| no hygiene: RESTORE_FULL time to agent-ready | 0.98 (p95 1.22) | s | 20 | preliminary: a restore that skips hygiene is not a usable restore (clones share RNG state, cookies, hostname) |
| no hygiene: RESTORE_SERVER_ONLY time to agent-ready (partial arm) | 1.22 (p95 1.28) | s | 20 | |
| no hygiene: COLD p50 / RESTORE_FULL p50 | 2.09 | x | design target 2x (RESTORE p50 at most 0.50 x COLD p50); passes the point rule narrowly | |
| no hygiene: host memory per added session, COLD | 229.4 | MiB | 3 | |
| no hygiene: host memory per added session, RESTORE | 221.88 | MiB | 3 | ratio 0.9672; cold minus restore 95% interval [6.09, 8.94] MiB |
| server-only dump (R3) | 0.21 | s | 1 | image 74.78 MiB |
| Chromium dump (R4) | 0.806 | s | 1 | image 253.09 MiB; a one-time cost per checkpoint |
| Chromium restore call (R4), total | 0.763 | s | 1 | of which criu restore 0.319 s; browser title drums-ready present |
| C2 screenshot pixels changed vs blank desktop | 0.998 | fraction | threshold 0.10; PASS | |
| C3 sibling audit, literal pre-registered wording | 0 / 1 | checks | FAIL: two host character devices shared, /dev/null and /dev/urandom; the cold-pair control shares the same two | |
| C3 sibling audit, revision 2a (exactly those two devices, cold control, nothing else) | 1 / 1 | checks | PASS; the original FAIL stays on the record | |
| C1 task e0df059f score from restored sessions, before acting | 0 | score | ||
| C1 task e0df059f score from restored sessions, after the scripted action | 1 | score | shell getter + exact_match path only; librosa stubbed and unreached; stub list not saved for this run | |
| page cache dropped (NOT pre-registered): COLD time to agent-ready | 2.59 (p95 2.74) | s | 10 | |
| page cache dropped (NOT pre-registered): RESTORE_FULL time to agent-ready | 1.38 (p95 1.47) | s | 10 | ratio 1.874; the 2x target is not met with cold cache |
| reseeded: COLD time to agent-ready | 2.1 (p95 2.24) | s | 20 | |
| reseeded: RESTORE_FULL time to agent-ready | 1.19 (p95 1.29) | s | 20 | the usable restore: hostname, server RNG and browser context reseeded |
| reseeded: RESTORE_SERVER_ONLY time to agent-ready (partial arm) | 1.24 (p95 1.37) | s | 20 | |
| reseeded: COLD p50 / RESTORE_FULL p50 | 1.76 | x | below the 2x target: FAIL | |
| reseeded: host memory per added session, COLD | 229.59 | MiB | 3 | |
| reseeded: host memory per added session, RESTORE | 238.32 | MiB | 3 | ratio 1.0380; cold minus restore 95% interval [-9.86, -7.42] MiB (restore costs more); passes the 1.05x bar, fails 'no more than cold' |
| reseed cost per sibling | 0.198 to 0.215 | s | 2 | clone_hygiene.json reseed_s |
| reseeded: C1 task score before / after | 0 to 1 | score | clone_task_semantics.json: reseeded true, sessions_were_restored true, session clock minus host clock -0.185 s |
Host: 4 vCPU, 16 GiB, cgroup v1, no KVM, Ubuntu 24.04, kernel 6.18 (sandbox). one development sandbox; hostname and provider not recorded; not a VM or cloud host. Workload: Drums L0 session (namespaces + overlayfs full root + Xvfb + D-Bus + AT-SPI + openbox) with the real OSWorld guest server and Playwright Chromium 141 (--no-sandbox) on a data-URL page titled drums-ready; public OSWorld task e0df059f for the correctness check. Method: COLD = create the session, launch the browser, load the page. RESTORE_FULL = criu restore of a checkpoint taken from a COLD session in that ready state, into a fresh network and mount namespace. RESTORE_SERVER_ONLY = restore of a checkpoint without the browser, then a cold browser start (partial arm). Time to agent-ready ends when a screenshot differs from blank in more than 10% of pixels and the page title is present. 20 starts per arm, interleaved, page cache warm (10 starts with the page cache dropped). Memory: slope of host used memory against session count at 1, 4, 8, 12, 16, 20, 3 trials. Private CRIU 4.2 extracted from Ubuntu archive packages. Thresholds fixed in 8f5ed06; no-hygiene results in ca07330; C3 revision 2/2a registered in 080a893 and fc1d88d; reseeded results in ba21252.. Source: branch claude/linux-comparison-prep: docs/data/l1-criu/criu_probe.json (criu, stages.R3_server_only_dump, stages.R4_chromium_dump, stages.R4_chromium_restore, stages.C2_screenshot_shows_browser, stages.C3_sibling_audit, stages.C1_public_task_from_restored_session, latency, memory_summary); branch claude/linux-comparison-prep: docs/data/l1-criu/probe_reseeded.json (latency, memory_summary, stages.C3_sibling_audit.PASS_revision_2a, stages.C1_public_task_from_restored_session); branch claude/linux-comparison-prep: docs/data/l1-criu/criu_probe_dropcache.json (latency, stages); branch claude/linux-comparison-prep: docs/data/l1-criu/clone_hygiene.json; branch claude/linux-comparison-prep: docs/data/l1-criu/clone_task_semantics.json; branch claude/linux-comparison-prep: docs/l1-shared-runtime-design.md sections 14 to 17.
- Drums here is the L0 prototype restored by CRIU; no shared-runtime (L1) result exists; no container, OCI image, VM or incumbent is in any of these files.
- The 2.09x figure is a restore with no hygiene and is labelled preliminary; the usable (reseeded) restore is 1.76x and misses the 2x target.
- The 1.87x drop-cache figure was not pre-registered (10 starts).
- Residual identical state across clones: Python string-hash seed and address-space layout; a checkpoint not declared clean keeps identical cookies and V8 Math.random sequences by design.
- One task, one checkpoint image per arm, --no-sandbox Chromium, one host and kernel; restore on another host untested.
- The CRIU POSIX-lock-after-restore failure recorded elsewhere on the site stays unresolved; this probe's --file-locks flag is a dump-time flag, not a repair.
Ladder and correctness (measured, criu_probe.json)
| Rung or check | Result |
|---|---|
R1 criu check | pass in 0.049 s (CRIU 4.2; libnl "no version information" warnings) |
| R3 server-only dump | 0.210 s, image 74.78 MiB; restore 0.659 s total (criu 0.219 s) |
| R4 Chromium dump | 0.806 s, image 253.09 MiB; restore 0.763 s total (criu 0.319 s); browser title drums-ready |
C1 task e0df059f from restored sessions | score 0.0 before, 1.0 after the scripted action; sessions_were_restored: true; 34.0 s; PASS |
| C2 screenshot shows the browser | changed_fraction_vs_blank: 0.99745 (bar 0.10); PASS |
| C3 sibling audit, literal | 19 processes each; pid sets disjoint; PID, mount, net, IPC, UTS namespaces distinct; no shared pipe, socket, lock or regular file; Xvfb socket inodes and addresses differ; a write in one sibling invisible to the other; shared_character_devices: ["chr:6:3:/dev/null", "chr:6:7:/dev/urandom"]; PASS_literal: false |
| C3 vs the cold-pair control | the cold pair shares the same two devices; introduced_by_restore is empty; PASS_vs_cold_control: true; under revision 2a, PASS_revision_2a: true (reseeded run) |
Latency and memory, no hygiene (measured; preliminary, not a usable restore)
| Arm | p50 | p95 | vs COLD p50 |
|---|---|---|---|
| COLD | 2.046 s | 2.189 s | |
| RESTORE_FULL | 0.980 s | 1.220 s | 2.09x (author-reported bootstrap interval 1.99 to 2.15) |
| RESTORE_SERVER_ONLY | 1.215 s | 1.282 s | 1.68x, below the bar as predicted |
Memory per added session: COLD 229.40 MiB, RESTORE 221.88 MiB, ratio 0.967, cold minus restore interval [6.09, 8.94] MiB. Of the 0.98 s restore p50, the CRIU restore itself is about 0.32 s and the restore call as a whole about 0.76 s; the rest is the readiness check.
Not pre-registered, page cache dropped before every start (10 starts per arm): COLD p50 2.587 s, RESTORE_FULL 1.380 s, ratio 1.87x; server-only 1.55x. With the 253 MiB image read from disk the 2x target is not met. The sandbox disk is a virtual device; a real host's would differ and was not measured.
The clone problem, shown (measured, clone_hygiene.json)
With the reseed off, two siblings from one checkpoint reported the same hostname, the same next random.random() from the guest server (0.6463347644353902 in both), the same next three page Math.random() values (0.49652, 0.49981, 0.91877), the same pre-checkpoint cookie (test string secret-token-pre-checkpoint), and the same Python string-hash seed. Contrary to what was registered, numpy's global generator differed (it is seeded lazily) and crypto.getRandomValues differed (kernel-backed). With the reseed on: hostnames differ, Python draws differ (0.6995 vs 0.6990), cookies are gone in both, the new-context page's Math.random() differs; reseed cost 0.198 and 0.215 s. For a checkpoint not declared clean (stand-in cookie task-login-token), the cookie is preserved in both siblings and the page's Math.random() sequence stays identical: the stated residual. clone_task_semantics.json: the public task scores 0.0 then 1.0 from a reseeded restored session; session clock minus host clock -0.185 s.
Latency and memory with the reseed (measured, probe_reseeded.json; the usable restore)
| Arm | p50 | p95 | vs COLD p50 |
|---|---|---|---|
| COLD | 2.096 s | 2.236 s | |
| RESTORE_FULL, reseeded | 1.188 s | 1.290 s | 1.76x (author-reported interval 1.73 to 1.83): FAIL of the 2x bar |
| RESTORE_SERVER_ONLY, reseeded | 1.245 s | 1.374 s | 1.68x |
Memory per added session: COLD 229.59 MiB, RESTORE 238.32 MiB, ratio 1.038; cold minus restore interval [-9.86, -7.42] MiB, so restored sessions cost 7.4 to 9.9 MiB more (the extra renderer of the new browser context). Against the registered numeric bar (at most 1.05x) this passes; against the plain statement "no more than cold" it does not. Both are reported. 793 /proc/sys entries before and after were identical.
Failures and surprises
- The headline failure. The usable restore is 1.76x, not 2x. The reseed costs about 0.2 s per start, dominated by the new browser-context round trip. The author predicted 1.5 to 1.7x before running it.
- C3 fails its literal wording. Two host device nodes are shared by every L0 session, restored or cold. Whether that is acceptable is a design judgement about L0, not something restore changed; the amended rule 2a was registered before the retest and the original FAIL stands.
- The expectation was wrong in the good direction. The author gave Chromium checkpointing a 30% chance; every rung passed after the four fixes.
- A real leak, kept as a failed run. The first reseeded run (
probe_reseeded_run1_serveronly_leak.json) raised in the reseed step after a restored session was registered;destroy()read asession.jsonthat is written last, found nothing, and skipped the kill. The leaked processes and veth links made the next run's restores fail withCan't restore link: -17 (File exists); 19 of the server-only arm's 20 starts in that file recordslot ... already running. Fixed with regression tests (an induced reseed failure and an induced failure at the last step both leave no process, link or mount). - Three transient test failures were a harness bug, not a product one:
pgrep -f envd_linux.session_initmatched the shell running the tests. They now match on argv structure.
Limitations
- No increment over an incumbent. The comparison is restore against Drums' own cold L0 start (about 2 s, already fast). A tuned incumbent's snapshot-revert latency was not measured because the sandbox has no KVM. VM snapshot resume is prior art.
- The absolute saving is about 1.07 s per start (no hygiene) or about 0.9 s (reseeded). The author's arithmetic, not a measurement: about 1.8% of valid rollouts per hour for a 60 s rollout, 0.2% for 600 s.
- Residual identical state across clones: Python's string-hash seed and the address-space layout; for non-clean checkpoints, cookies and V8
Math.randomby design. - One task (
e0df059f, a shell getter plusexact_match;librosais stubbed and unreached; the stub list was not saved for these runs), one checkpoint per arm,--no-sandboxChromium, no GPU, one host and kernel; CRIU images are kernel- and CPU-specific and restore on a second host was not tested; a checkpoint cannot yet be reused across supervisor runs. - The CRIU POSIX-lock-after-restore failure recorded in other records on this site stays unresolved;
--file-lockshere is a dump flag, not a repair, and no lock-after-restore check was part of this probe.
Reproduce
PYTHONPATH=<OSWorld checkout>:envd-linux:compare DRUMS_CRIU_HOME=<private criu dir> \
<osworld venv python> -m envd_linux.l1proto.criu_probe --out probe.json \
--task <e0df059f.json> --starts 20 --trials 3 # add --drop-caches for the cache-cold run
pytest envd-linux/tests/test_criu_clone_hygiene.py
DRUMS_CRIU_HOME holds x-4.2/usr/sbin/criu and its private libraries; needs root, overlayfs and a kernel CRIU 4.2 supports. Check latency.ratio_cold_over_restore_full_p50 and memory_summary.restore_over_cold against section 14's bars.
Sources
docs/data/l1-criu/criu_probe.json,criu_probe.log(no hygiene; commitca07330, 2026-10-24 07:41:17 UTC)docs/data/l1-criu/criu_probe_dropcache.json(not pre-registered)docs/data/l1-criu/probe_reseeded.json,probe_reseeded.log(commitba21252, 2026-10-24 08:41:03 UTC)docs/data/l1-criu/probe_reseeded_run1_serveronly_leak.json(the failed first reseeded run)docs/data/l1-criu/clone_hygiene.json,clone_task_semantics.jsondocs/data/l1-criu/sysctl_before.json,sysctl_after.json,sysctl_before_reseeded.json,sysctl_after_reseeded.jsondocs/l1-shared-runtime-design.mdsections 14 (pre-registration,8f5ed06), 15 (results), 16 (revision 2/2a,080a893,fc1d88d), 17 (follow-up results)docs/mechanism-c-criu-restore.mdenvd-linux/envd_linux/l1proto/criu_probe.py,envd_linux/criu.py,envd_linux/clone.py,tests/test_criu_clone_hygiene.py,tests/test_criu_restore.py- Branch
claude/linux-comparison-prep; result filedata/results/exp-005-criu-restore.jsonon this site