rsrun's hot path is in rsrun-core (the lifecycle: clone3, namespaces,
mounts, caps, exec). Heavier features live in rsrun-ext, all gated
as default-on Cargo features so the binary can be slimmed by opting
out.
Rollout filesystem/state extensions are gated separately behind the
rollout Cargo feature and exported through rsrun_core::rollout.
The Docker/OCI runtime surface must remain usable without that feature;
future rollout storage work should keep using this boundary instead of
adding dependencies to the lifecycle hot path.
There are two roadmap tracks:
- Agent rollout runtime: SPEC.md is the primary
direction for rollout workloads. It prioritizes repeated
exec, bounded failure, structured step results, and filesystem state primitives. - OCI / crun compatibility: this file tracks drop-in runtime production-readiness. For a feature-by-feature comparison against crun, see gaps-vs-crun.md.
When these tracks conflict, optimize for the agent-runtime hot path unless a real Docker / containerd / Kubernetes workload proves the compatibility gap is blocking.
These are the next implementation milestones for large-scale agent rollout use. They intentionally sit ahead of broad crun parity.
- First-class
rsrun exec <id> --timeout ... --json -- <cmd> .... - Separate stdout/stderr capture with deterministic truncation.
- Whole-process-tree timeout cleanup, including cgroup cleanup.
- JSON result with exit/signal, timeout, duration, CPU, memory, OOM, and output truncation fields.
- Runtime-level timeouts for
create,start,delete, unmount, and cleanup paths. - Failed cleanup state that is visible to callers and recoverable by a later cleanup pass.
validate-bundle <bundle> --jsonto reject unsupported bundles before a rollout starts.- ✅ Overlay-backed writable rootfs mode for cheap reset: rsrun should own the per-episode overlay upperdir/workdir and state metadata, while the lower rootfs remains a prepared OCI bundle path. Model this as a writable-rootfs backend so overlayfs is the first implementation, not the only possible filesystem strategy.
- ✅
changed-files,diff, andexport-diff --format tar|jsonfor overlayfs-backed patch extraction. - ✅ Filesystem-level
snapshot,restore, andforkby cloning the overlay upperdir with reflink when available and copy fallback; CRIU remains a later optional path. - ✅ High-fanout filesystem checkpoints:
checkpointfreezes the current upperdir as a read-only lower layer, whilefork-checkpointcreates a new branch with an empty writable upperdir over the checkpoint chain. - ✅ Portable local checkpoint layers: checkpoints can be stored in a runtime-owned overlay2-style layer store, exported as a flattened portable tar artifact, imported on another host, and forked without editing absolute metadata paths.
- ✅ Prepared checkpoint/fork states can be activated into live
containers with init/cgroup/namespaces and then used as rollout
exec --json -- <step>targets. - ✅
markandeffects --since <marker> --jsoncompare current overlayfs effects against a named filesystem marker for rollout control and debugging. - ✅ Opt-in
clone3(CLONE_INTO_CGROUP)for cgroup-v2 placement viaRSRUN_CLONE_INTO_CGROUP=1, with the faster default path retaining the previouscgroup.procswrite.
Near-term rsrun work should implement overlayfs as the first writable-rootfs backend. It should support per-episode upperdir/workdir state, cheap reset, diff, and export while keeping the lower rootfs as a prepared OCI bundle path.
The implementation should keep a small backend boundary around mount, diff, export, import, resolve, and cleanup operations so other filesystem strategies can be added later. Storage optimizations such as built-in compression, multi-layer deduplicated transfer, erofs, and chain compaction stay below the functional parallel-rollout API in priority unless a deployment proves storage cost is the blocker.
Build boundary: all rollout-specific filesystem state commands
(reset, diff, export-diff, snapshot, checkpoint,
export-checkpoint, import-checkpoint, fork-checkpoint,
activate, mark, effects) belong behind rollout.
The OCI lifecycle commands (create, start, state, kill,
delete, exec -p, pause, resume, update, stats, events)
must continue to compile and run without it.
✅ Rollout filesystem/checkpoint implementation lives in
rsrun_core::rollout. runtime.rs only calls small rollout hooks
needed by create/delete; state operations (reset, diff,
export-diff, snapshot, checkpoint, export-checkpoint,
import-checkpoint, fork-checkpoint, activate, mark, effects)
stay behind the rollout module boundary. Keep the feature flag coarse
and avoid spreading per-function cfg logic through the lifecycle hot
path.
Honest framing of what would have to land before a Docker / containerd
/ Kubernetes operator could safely use --runtime=rsrun in production.
Items reference the per-feature detail in the Tier sections below.
After M1, rsrun is safe to run on a single host where the operator can monitor it. Without these, real users will hit hangs or silently-wrong behavior with no diagnostic.
- ✅ Hook timeout enforcement.
pidfd_open+pollwaits for the hook subprocess; on deadline itSIGKILLs and reaps. Implemented in both parent (run_hooks) and in-container (run_hooks_unsafe) paths. Verified end-to-end: apoststophook ofsleep 30withtimeout: 1exits the runtime in ~1s instead of hanging for 30s. Tier 2 #6. - ✅
process.scheduler.sched_setattr(2)from the parent on the init pid, after clone3. All six policies + 7 flag bits + nice + priority + DEADLINE runtime/deadline/period. Verified end-to-end:chrt -preports the requested policy;/proc/<pid>/statconfirms the kernel applied it. Tier 2 #9. - ✅ Crash recovery between
createandstart.init.pidand a"creating"-statusstate.jsonare now written before any post-clone3 step that can fail. Failures after clone3 SIGKILL the init and tear down the state dir; survival of a parent kill leaves a recoverable orphan thatstatereports as"creating"and thatdelete -fcleans up. Verified end-to-end. Tier 2 #14. - ✅ Multi-arch verification on x86_64. Seccomp x86_64 syscall
table populated from kernel
syscall_64.tblv6.6 (365 names). All other direct-syscall sites uselibc::SYS_*(arch-correct) or generic-table numbers shared between aarch64 and x86_64 (clone3,open_tree,move_mount,mount_setattr)..github/workflows/ci.ymlruns unit tests + lifecycle smoke + runtime-tools validation on bothubuntu-24.04(x86_64) andubuntu-24.04-arm(aarch64) on every PR.
After M2, rsrun is a defensible drop-in on the install bases that matter today: RHEL 8, Amazon Linux 2, K8s clusters with non-default scheduler classes, SELinux-enforcing hosts.
- cgroup v1. ~25-40 % of running fleets depending on the survey. Tier 1 #5.
linux.mountLabelpropagation. Bind volumes on RHEL/Fedora fail today. Tier 2 #10.linux.sysctlvalidation. Misconfigured sysctls silently get partial application. Tier 2 #7.- Stats accuracy.
cpu.statparsing is partial;docker statsshows wrong CPU%. ~50 LOC. New. - Race-free
docker exec --detach. Parent currently can return before the child has fully execve'd. CI systems checking liveness via--pid-filesee false negatives. ~30 LOC. New. - Richer structured logging.
--log-format jsonemits Docker-compatible error lines today; production operators will want structured warning/info/debug events with stable fields. Tier 3.
After M3, rsrun can claim parity with crun for everything Docker exercises in practice. Beyond M3 is parity with crun's full surface, which includes niche features that rarely matter in production (Intel RDT, NUMA memoryPolicy, personality, ioPriority).
- CRIU integration (live migration / checkpoint).
- AppArmor profile stacking (container-in-container).
- Custom seccomp argument matching (per-syscall
args). tmpcopyupmount option (some K8s ConfigMap patterns).- Recursive mount propagation flags (
rro,rrw, …). - Distro packaging, signed releases, supply-chain attestation.
- youki
contestharness alongside runtime-tools in CI (currently only the runtime-tools subset underscripts/oci_validation.shruns).
The README's "Status" section currently says "Not production-ready; some features are not yet thoroughly tested." Once the M1 list is clear, we can replace it with something specific:
rsrun runs the OCI lifecycle correctly on a single cgroup-v2 host with Docker. M1 is complete on aarch64 and x86_64 in CI; outstanding before production-on-containerd: cgroup-v1 hosts (RHEL 8, AL2), SELinux mount labels, sysctl validation, stats accuracy, and race-free detached exec.
These don't affect the bench numbers because none of them touch the
create + start + delete hot path of an empty bundle unless their
corresponding OCI fields or feature flags are enabled. The current
protected hot path uses a read-only cloned /proc/self/exe fd and
falls back to sealed memfd only on older kernels.
- Full
create/start/delete/state/kill/exec/list/featureslifecycle. pause/resume(cgroup-v2cgroup.freeze).updatere-tunes cgroup-v2 limits on a running container.stats/eventsstream cgroup-v2 metrics fordocker stats.
- All seven namespaces; rootful and rootless (single user namespace).
- Opt-in overlayfs writable rootfs backend via
rsrun.rootfs.backend = "overlayfs"plusrsrun reset <id>for stopped containers. The runtime records upper/work/merged paths under container state and refuses to reset arbitrary host paths. - Overlayfs-native
changed-files,diff, andexport-diffwalk only the recorded upperdir. Deletions are exported as portable tar whiteouts; optional text patch generation remains future work. - Filesystem
snapshot,restore, andforkclone only the recorded overlay upperdir, preserve overlay metadata, and mount a new stopped overlay state without sharing writable files. - Filesystem
checkpointandfork-checkpointavoid per-branch checkpoint copies by turning each checkpoint delta into a read-only lower layer and giving each rollout branch a fresh empty upperdir. Checkpoint metadata records backend, storage format, local layer path, and the effective lowerdir chain so another read-only layer store can be wired in later. - Filesystem
markandeffectspersist named overlay baselines and report post-marker changed paths, sensitive path touches, and approximate written bytes. Process, network, and IO accounting remain explicit future extensions. - Local overlay2-style checkpoint layers are implemented for portable filesystem state. More compact read-only image packing is a future storage optimization, not a blocker for the first controller path.
linux.namespaces[].path— joining a pre-existing namespace viasetns(2). PID-ns join works (post-clone3 child re-forks once when joiningpid, mirroring crun).linux.rootfsPropagationmodes (shared / slave / private / unbindable +r*recursive).linux.sysctlwrites inside the new namespaces.
Goal: make rsrun usable as the low-level controller target for
parallel rollout. Storage portability is now sufficient for a first
controller: checkpoints can be written into a runtime-owned
overlay2-style layer store, exported as a flattened portable tar,
imported on another host, and forked without editing absolute metadata
paths.
The remaining work above rsrun is controller policy: deciding when to export/import checkpoints, how many branches to fan out, and when to delete or retain activated branches. The rsrun-level portable layer use path is now available.
Branch lifecycle is the next controller concern. The controller should distinguish at least these states:
- Prepared: a stopped branch produced by
fork-checkpoint; it has overlay state but no live init process. - Active:
activate+startcompleted, so rollout steps can run throughexec --json -- <step>. - Committed: a useful branch was saved as a new checkpoint for later fanout or transfer.
- Disposed: a losing or expired branch was removed with
delete -f.
This matters because massive rollout may create many more candidate branches than can run at once. Keeping idle candidates as stopped filesystem states avoids paying process, namespace, cgroup, and memory cost until a branch is scheduled. After a step, the controller can keep the branch active for the next step, checkpoint it as a new breakpoint, or delete it immediately.
Proposed commands:
rsrun checkpoint <id> <checkpoint-id> --pack directory|overlay2
rsrun export-checkpoint <checkpoint-id> --format tar > checkpoint.tar
rsrun import-checkpoint <checkpoint-id> checkpoint.tar
rsrun fork-checkpoint <checkpoint-id> <new-id>
rsrun activate <id>
Implementation steps:
- ✅ Add
LayerRefmetadata withformat = overlay-upperdir | flattened-rootfs,store = local-directory | overlay2-directory, an optional content digest, and a resolver path. - ✅ Keep
--pack directoryas the current default, and add--pack overlay2using a runtime-owned overlay2-style layout: each layer has adiff/directory, alinkfile, optionallowermetadata, and a runtime-ownedl/directory with short symlinks for lowerdir mount strings. rsrun does not write into Docker's/var/lib/docker/overlay2. - ✅ Implement
export-checkpoint/import-checkpointfor portabletarartifacts. The first artifact format is flattened and self-contained so it can import on another host without identical absolute base-layer paths. - ✅ Add a layer resolver that turns
LayerRefentries into local read-only directories or short overlay2 links forfork-checkpoint. - ✅ Add an activation path for prepared states. It should reuse the
existing OCI create path as much as possible, but take the already
resolved overlay rootfs from state instead of remounting a fresh
bundle rootfs. After activation,
stateshould report a live init andexec --json -- <step>should work. - ✅ Add controller-facing cleanup for activated branches: bounded delete,
cgroup cleanup, and overlay unmount must work for imported/forked
states exactly like normal
createstates.
Acceptance criteria:
- Branch writes never target read-only checkpoint layers.
- Reads from older checkpoint layers do not copy data into the branch upperdir.
- Deleted paths survive pack, resolve, fork, and import through the
same whiteout semantics used by
export-diff. - ✅ A checkpoint exported on one host can be imported on another host and forked without editing metadata by hand.
- ✅ A branch forked from an imported checkpoint can be activated and used
as the target of rollout
exec --json -- <step>commands. - The default OCI lifecycle path does not mount or inspect checkpoint layers.
These are useful for scale and cost, but they are not required before using rsrun as a basic parallel-rollout runtime target:
- Built-in
tar.zstartifacts. Today callers can compress the exported tar outside rsrun. - Stronger content digests for portable artifacts and layer store entries. The current local store uses a lightweight internal content identifier; external integrity should use a real digest before remote distribution at scale.
- Multi-layer export/import that preserves layer sharing when the destination already has base layers. Current export is flattened and portable, but not storage-optimal.
- Packed read-only checkpoint images for immutable checkpoint deltas. The current overlay2-style directory layers are enough for functional branching; image packing should be added when storage density or transfer cost becomes the limiting factor.
compact-checkpointto materialize a long checkpoint chain and pack it into one new read-only image when lowerdir count or mount-option length crosses a threshold.- erofs or another read-only image backend. The resolver is shaped so this can be wired in later, but overlay2-style directories are enough for the first portable rollout path.
- Docker storage-driver integration. rsrun should continue to own its
layer store rather than writing into Docker's
/var/lib/docker.
- Capabilities (all five sets), rlimits, default
/dev, masked + readonly paths,noNewPrivileges,process.useruid/gid + supp gids. - seccomp profile compilation + install (via
seccompiler). - AppArmor / SELinux profile staging via
/proc/self/attr/...for the next execve. execwith full OCI semantics: honorsprocess.json'suser,capabilities,noNewPrivileges,apparmorProfile,selinuxLabel,terminal+--console-socket. Order matcheschild_run(groups → caps → user → NNP → LSM).
- cgroup-v2 limits:
memory.{max,swap.max,low},cpu.{max,weight},cpuset.{cpus,mems},pids.max, per-deviceio.max. - Device cgroup BPF (
linux.resources.devices): hand-rolled eBPF emitter compiles allow/deny rules to aBPF_PROG_TYPE_CGROUP_DEVICEprogram. OCI defaults +linux.devicesentries are implicitly allowed. ~250 LOC, zero new crate deps. --systemd-cgroupdelegates cgroup creation tosystemd-run(transient.scopeslice).
- OCI hooks: all six phases.
prestart/createRuntime/poststart/poststopfire from the parent;createContainer/startContainerfire from inside the container's namespaces. - TTY /
console-socket: PTY pair + SCM_RIGHTS forward — makesdocker run -itanddocker exec -itwork end-to-end. - Cargo features: every optional capability gated; default = full
set,
--no-default-featuresproduces a 753 KB minimum binary.
Items 1-4 below are now in tree. The remaining item is cgroup v1. See gaps-vs-crun.md for crun source references.
-
✅
--preserve-fds— fds 3..N+2 inherited viafcntl(F_SETFD, !CLOEXEC)before clone3. -
✅
--no-pivot— child takes the chroot(2) branch instead of pivot_root + umount. -
✅
process.oomScoreAdj— written to/proc/<init>/oom_score_adjfrom the parent after clone3 returns the host pid. -
✅ Idmapped mounts (
linux.mounts[].uidMappings/gidMappings, kernel 5.12+). Helper task per idmap-mount sets up a userns with the required mapping; parent opens/proc/<helper>/ns/userand passes the fd to the child via clone3 fd inheritance; child doesopen_tree(OPEN_TREE_CLONE) → mount_setattr(MOUNT_ATTR_IDMAP) → move_mounton each idmapped entry. Used by Docker 25+ rootless remapping and the K8s user-namespace feature gate. -
cgroup v1 — RHEL 8, Amazon Linux 2, older Debian. Deferred: ~600 LOC duplicating cgroup-v2 logic for v1's per-controller layout. Tracked as a separate effort once Tier 2 lands.
Items affect real workloads but only under specific configurations.
-
✅ Hook timeout enforcement —
pidfd_open+pollwith the spec-suppliedhooks[i].timeout; SIGKILL + reap on deadline. Implemented for both parent-side (run_hooks) and post-clone3 in-container (run_hooks_unsafe) phases. Falls back to blocking wait on kernels withoutpidfd_open(< 5.3). -
linux.sysctlconflict validation — reject conflicts with thehostnamefield, namespace-required sysctls without the matching namespace, etc. crun does this in the spec parser. ~30 LOC. -
process.consoleSize—TIOCSWINSZafter PTY allocation. PTY currently inherits the kernel default rows × cols. ~10 LOC. -
✅
process.scheduler—sched_setattr(2)on the init pid from the parent after clone3. All six policies, 7 flag bits, plus nice / priority / DEADLINE timing fields. Spec rejects unknown policies and flags at parse time. -
linux.mountLabelpropagation — choosecontext=mount option vssetxattr(security.selinux)per fstype. SELinux hosts currently see denied access on bind-mounted volumes. ~40 LOC. -
process.rlimits[].soft > hardvalidation — reject at parse time instead of letting the kernel error. ~5 LOC. -
tmpcopyupmount option — copy directory contents into tmpfs before bind. Used by some K8s ConfigMap / Secret mounts. ~25 LOC. -
Recursive mount propagation flags (
rro,rrw,rnoexec,rsuid…) viamount_setattr(MOUNT_ATTR_*, AT_RECURSIVE). Linux 5.12+. ~30 LOC. -
✅ Crash recovery between
createandstart.init.pid+state.json("creating")written immediately after the host pid is known, before any fallible post-clone3 step. Failures after that point SIGKILL+reap the init and remove the state dir.cmd_stateon a state-less container synthesizes a"creating"-or-"stopped"response from init.pid;delete -fcleans up either way. -
Stats accuracy.
cmd_statsreadscpu.statbut parses onlyusage_usec;system_usecanduser_usecare dropped, sodocker statspercent-CPU computations are wrong under load. Also missing:memory.events,pids.events. ~50 LOC. M2 item. -
Race-free
docker exec --detach. Today the parent returns afterfork()regardless of execve completion; if the child's apply_capabilities or NNP step fails, the parent's caller sees "success" with a child that's about to_exit. crun uses an extra sync pipe so the parent only returns afterexecveis in flight. ~30 LOC. M2 item. -
Richer structured logging.
--log-format jsonsupports Docker-compatible error output today. Add stable structured fields for warning/info/debug events and internal operation timing. M2 item.
specsubcommand — generate a defaultconfig.json. Currently returns "not implemented".- Custom seccomp argument matching — OCI seccomp's per-syscall
argsfield. Used for filters like "allowclone()only withoutCLONE_NEWUSER." - AppArmor profile stacking —
change_profile(stacked) vschange_onexec; for container-in-container scenarios. - More cgroup-v2 knobs —
cpu.idle,cpu.uclamp.*,memory.peak,hugetlb.<size>.max,rdma.max,misc.max. - Intel RDT (
linux.intelRdt) — HPC / trading workloads. linux.memoryPolicy— NUMAMPOL_BIND/MPOL_INTERLEAVE.linux.personality— 32-bit ABI emulation.process.ioPriority—ioprio_set(2).linux.timeOffsets/ time namespace (CLONE_NEWTIME).- CRIU checkpoint / restore.
- WASM workloads — youki has it; out of scope for now.
- Annotations passthrough into hook env vars.
- Richer log levels / structured logging — rsrun emits plain or Docker-compatible JSON error lines today; broader structured events are still missing.
- Per-container network setup (CNI / built-in bridge) — engine territory; rsrun sets the netns flag and stops there.
- ✅ Multi-arch CI:
.github/workflows/ci.ymlexercises bothubuntu-24.04(x86_64) andubuntu-24.04-arm(aarch64) for unit tests, the M1 lifecycle smoke (scripts/smoke.sh), and OCI runtime-tools validation. - Static musl build: release binary links dynamically against glibc.
- Distro packaging: Debian/Ubuntu .deb, Fedora/RHEL .rpm, AUR.
These are speculative. None are blocked on a missing OCI feature.
mimallocas global allocator. Slight startup improvement.pidfd-based wait indelete(avoid/proc/<pid>polling).- Plan cache. Compile
config.jsononce, mmap at create — for deployments that launch many copies of the same bundle.