In one paragraph
This arm is an ordinary container control, not the Drums runtime. It takes the same pinned OSWorld image, task, 4 vCPU / 4 GiB limits and 1920x1080 screen as the custom runtime, but lets runc 1.5.2 build the namespaces, mounts, devices and cgroup from an OCI config.json and drive CRIU 4.3 through its own checkpoint and restore commands. Its job is to answer "is the failure ours or the method's?". On the operator host, runc restore of the GNOME container reported success in 0.225 s (author-claimed, no log on branch), but inside the restored namespace the loopback interface was down with no address, and the restore log said No netns-10.img image. A layered probe, then a reading of runc's source, found the cause: runc always tells CRIU to treat the network namespace as empty, so CRIU neither dumps nor restores it; that is runc's design, not a CRIU or kernel fault. The documented way around it is for the harness to own the namespace through a small holder process, and a minimal sleep container was checkpointed and restored that way with the namespace identical before and after (author-claimed). The GNOME container has not been run on the holder path.
Setup
- runc: release binary
runc.amd64v1.5.2, sha256599f6f94ff8c5057241eff0d54c3c74f95c34935b6457b33fe545defc61e9488, 10,866,512 bytes, both the clearsignedrunc.sha256sumand the detached.ascverified against the keyC2428CD7 5720FACD CF76B6EA 17DE5ECB 75A1100Elisted in the repository's ownrunc.keyringat the tag (trust anchor is that keyring, no independent key verification). The binary reportsrunc version 1.5.2, commitv1.5.2-0-g29dd3dc, spec 1.3.0, go1.26.8 (author-reported from running it). - CRIU 4.3 from the same source build as EXP-008.
environmentrefuses to run if a CRIU default config file or$CRIU_CONFIG_FILEexists, because those can silently change checkpoint behaviour. - Bundle: rootfs is an overlayfs prepared by the driver (lower = the read-only qcow2 mount, upper per run); container init is the image's own
python3running the samesupervisor.pyas the custom runtime, as root, with the default Docker-style capability set and no seccomp or AppArmor profile; OCI resourcescpu.quota=400000/100000andmemory.limit=4 GiBwith no swap, applied through the systemd cgroup driver and read back before a slot is ready; the same staged Xorg dummy driver. - Gates: ownership markers and the
config.jsonhash on every verb, fresh state only (a slot is never re-provisioned in place), proven stops viarunc state, and after the network finding a network-fidelity gate: before the proxy,nsenter -t PID -n -- ip -j link|addr|route showmust showloUP with127.0.0.1/8at provision, is recorded before the checkpoint, and must be byte-equal after the restore or the restore fails closed with the diff. DECLARED_DIFFERENCESinoci_reference/__init__.pymust accompany any number from this arm; the last one added reads: "The network namespace is HARNESS-OWNED and is not part of the CRIU image: runc 1.5.2 always passes empty-ns for a namespace it creates (checkpoint.go), so each slot instead runs in a private namespace created and brought up (lo) by a small persistent holder process (oci_reference/holder.py) and named bypathin config.json; CRIU records only an external key and the same namespace must exist at restore. The holder's RAM and idle CPU are costs of the harness and are NOT inside the container's cgroup".
What ran
All author-claimed, from PROTOCOL.md sections 6 to 9 and commit messages, no artifact on the branch.
- Live restore of the GNOME container (operator,
ee208df, 16:59 UTC): "runc restorereported "Restore finished successfully. Tasks resumed." in 0.225 s; init pid identity and the cgroup/limits readback passed; but in the restored private netnslois DOWN with no 127.0.0.1 (the image server listens on 0.0.0.0:5000, a namespace-local curl says "Network unreachable"); the restore log containsNo netns-10.img image." At that point the doc said "No causal claim has been made." - Operator read-only host check (section 7): no CRIU config file,
CRIU_CONFIG_FILEempty, noexternalor empty-ns message, yet every network image is absent, and the dump log stops afterDump NET namespace info 10 via 1661/net: Dumping netns linkswith noLD: Got link. That ruled out the config-file explanation. - Layered netprobe (
091af65,ab51daf): three phases, each removing one layer.kernel: in a freshunshare --netsend CRIU's exactRTM_GETLINKrequest and decode replies the way CRIU does.criu: a plain child with its own netns andloup, dumped with plaincriu dump --leave-running -v4.oci: the samesleepas a runc container on the read-only image, checkpoint, delete, restore. Result (section 8): "plain CRIU (own netns,loup) dumps all network image types, logsLD: Got link..., and runsip addr save; the minimal runc container (same CRIU, same rootfs pin,/bin/sleep, no GNOME, no overlay) reproduces the full failure: no network images, restore rc 0,loDOWN, no addresses. Kernel raw request passes." - Minimal external-netns check (
0deada5, section 9): "checkpoint 0, restore 0, an external-keynetnsimage present (26 bytes), namespace before/after identical (loUP,127.0.0.1/8,::1/128), the restored container in the holder's namespace, cleanup clean." - The holder was wired into the driver (
a9b57b9, 17:49 UTC). The next operator step in the doc, "One GNOME provision -> snapshot -> restore (and reset), then pixels, input and reset verification", is not recorded as done.
Results
No measured result. The author-claimed numbers: restore reported in 0.225 s (step 1), a 26-byte external-key netns image (step 4). Not shown in a table.
The source-grounded cause
From PROTOCOL.md section 8, read at runc v1.5.2 and CRIU commit 2b7ac603: runc's checkpoint.go criuOptions(), used by both runc checkpoint and runc restore, "begins // runc doesn't manage network devices and their configuration. nsmask := unix.CLONE_NEWNET and ends opts.EmptyNs = uint32(nsmask): runc always asks CRIU for empty_ns = CLONE_NEWNET, whether or not --empty-ns is given." On the CRIU side, dump_net_ns then skips the whole links, addresses, routes, rules, iptables and netns-conf branch and only runs dump_nf_ct(), which is the function that logs Dumping netns links; image files are opened lazily so nothing is created. On restore, prepare_net_ns_first/second_stage return early when empty_ns has the NET bit, so the namespace is a fresh one with lo down. "So the "missing" images and the DOWN loopback are runc's documented-by-code design (the caller owns the container's network), not a CRIU, kernel or host fault, and no CRIU/kernel change would alter it. This also explains why the custom runtime (which called CRIU directly) is unaffected." A falsifiable check on the preserved logs is given: the runc dump log should contain exactly two Dumping netns links lines and no LD: Got link after Dump NET namespace info; the plain-CRIU dump log should contain four.
The holder path
runc's own mechanism for a caller-owned namespace: if config.json names a network namespace by path, runc passes --external net[inode]:extRootNetNS at checkpoint and --inherit-fd fd[N]:extRootNetNS at restore, and CRIU records only the key. The harness therefore runs one holder process per slot (holder.py).
sequenceDiagram participant D as Driver (oci_reference) participant H as Holder (per slot) participant R as runc 1.5.2 participant C as CRIU 4.3 D->>H: start at provision, before the bundle is written H->>H: unshare net, bring lo up, setsid, wait with no expiry H-->>D: handshake pid, start ticks, netns inode D->>D: verify identity, write slotN/holder.json, set config.json network path to proc/PID/ns/net D->>R: runc create and start (container joins the holder's namespace) D->>R: runc checkpoint R->>C: external net[inode]:extRootNetNS (CRIU records only the key) D->>D: re-verify holder, record ip -j link/addr/route D->>R: runc restore R->>C: inherit-fd fd[N]:extRootNetNS (same namespace must exist) D->>D: gate: lo UP, 127.0.0.1/8, readback byte-equal to the checkpointed state D->>H: cleanup: container proven gone, then holder stopped through a pidfd
Holder rules from section 9: the holder creates the namespace with unshare (no ip netns add, no host mount) and brings lo up itself; the driver proves the child's identity before writing holder.json and re-verifies it before any nsenter, snapshot, restore or ready check; a missing or changed holder fails closed before any destructive step; a failed restore keeps the holder so the restore can be retried; after a host restart the slot is unusable by design.
Failures and surprises
- A restore that reported success was not a successful restore. The first acceptance check (the guest server answering one request) was too weak; the network-fidelity gate replaced it, and "A restored container that merely answers one request is no longer accepted."
- The cause was in the maintained runtime's design, not in CRIU or the kernel. The probe was built to discriminate three layers and the minimal runc container reproduced the failure without GNOME or an overlay.
- The OCI control's clean restore says less than it looks:
CANDIDATE.mdnotes runc forwards--file-lockswith default false, "so the OCI control passing means no lock was held there, not that locks are ignored". The GNOME restore in step 1 happened with no file lock held and with a broken loopback. It is not a complete passing cycle.
Limitations
- No
runc.log,criu-evidence-*,netprobe-result.jsonorholder.jsonon the branch; the host is not identified.PROTOCOL.mdis still titled "(for review; nothing here has been run)" while its later sections describe live runs. - Known unknowns listed by the branch: runc 1.5.2 with CRIU 4.3 on this kernel is an untested pairing; whether CRIU accepts an overlay as the
--rootmount is unproven; GNOME without a seat or logind inside a runc container. - With the holder, connections inside the namespace are not assumed to survive a restore (TCP-established is not enabled); the holder's RSS and idle CPU sit outside the container cgroup.
- The graphics path is the staged Xorg dummy driver, not the VM's virtual GPU; "identical graphics are never claimed".
Reproduce
Netprobe, from PROTOCOL.md section 7 (root, new evidence directory, cheaper phases first):
sudo python3 -m oci_reference.netprobe --evidence /var/lib/drumsbench/netprobe-1 --phases kernel,criu --strace
sudo python3 -m oci_reference.netprobe --evidence /var/lib/drumsbench/netprobe-2 --phases oci --runc /opt/runc/runc-1.5.2 --rootfs <read-only mounted image dir>
sudo python3 -m oci_reference.netprobe --evidence <NEW dir> --phases oci --external-netns --runc /opt/runc/runc-1.5.2 --rootfs <read-only image dir>
Reference arm: host/install_runc.sh, then python -m oci_reference VERB [--slot N] with its own state directory (never the custom rootfs-state), provision, snapshot, restore, reset, cleanup. Publishing netprobe-result.json, the CRIU evidence directories and holder.json from a fresh-state GNOME cycle would move this record forward.
Sources
benchmark/oci_reference/PROTOCOL.md(sections 1, 5, 6, 7, 8, 9)benchmark/oci_reference/__init__.py(DECLARED_DIFFERENCES),benchmark/oci_reference/pins.py,benchmark/oci_reference/holder.py,benchmark/oci_reference/netprobe.py,benchmark/oci_reference/bundle.py,benchmark/oci_reference/host/install_runc.shbenchmark/sharedos_rootfs/CANDIDATE.md, sections 2 and 3b (the--file-locksobservation about the control)benchmark/README.md, section "OCI reference arm (maintained runc) and the paused custom restore"- Commits
b8c2cd3,ee208df,b231da9,091af65,ab51daf,0deada5,a9b57b9(2026-10-25)