containerd Runtime V2

August 27, 2026 · View on GitHub

Current support matrix

The shim is a development adapter and does not make any runtime driver supported. Tags produced by the current release workflow are covered by SHA256SUMS and signed SLSA build provenance, but exact published-package qualification remains open.

containerdHostRuntime profileStatusRetained gate
2.2.2Ubuntu arm64Native Linux, shared-host-kernelDevelopment-qualifiedThree consecutive same-Host real lifecycle matrices with guest-journal reclamation, exec, deleted exec-ID reuse, exact DeleteProcess response replay, FIFO/PTY I/O, repeated controls and signals, daemon restart, live shim replacement with exact input and output continuation, in-flight Create, committed init-Start, exec-Start, live init-Kill, terminal init-Kill and exec-SignalProcess exit adoption, Pause, Resume, Update, WriteStdin, CloseStdin, SignalProcess, and ResizePty rehydration, post-commit Create/Start/Kill/Delete/Exec/SignalProcess/control cleanup, four-state shim SIGKILL, identity replacement, and four-task parallel cleanup
2.0, 2.1, other 2.2 releasesLinuxAnyNot yet qualified2.2.1 and 2.2.3 observations retained; range qualification pending
1.7 and earlierLinuxAnyNot qualifiedNo compatibility claim
AnyUtility-VM profilededicated-vmNot yet qualified through containerdDriver-specific gate pending

The implementation may interoperate with an unlisted release because the runtime-v2 contract is stable. That is not a support claim. Add a release to the table only after the same ignored real-host qualification passes against the packaged shim, SDK, host service, agent, and selected driver.

The canonical machine-readable record is compat/containerd-runtime-v2.json. It mirrors the code-owned claims and retains the exact source revisions, protocol ranges, artifact digests, environments, pass durations, and cleanup results behind them. Its August 28, 2026 containerd 2.2.1 WSL2 x86_64 run is explicitly observation-only: three consecutive 91.96, 91.89, and 91.92-second passes of the static-musl CLI, agent, shim, and qualification executable ran through unchanged Host PID 1566. Each pass crossed all 23 daemon-restart and post-commit rehydration boundaries, including the current forced-cleanup ResizePty gate. The default containerd remained active at PID 184; final audits found zero tasks, containers, live Runtime containers, matching shim or workload processes, mounts, or cgroups, and both isolated roots were removed. This evidence does not extend the exact 2.2.2 Ubuntu arm64 development claim.

Contract v1 owns this matrix in code and exposes it through the shim's version output and RuntimeInfo annotations. The accepted ttrpc service is containerd.task.v2.Task. State, Create, Start, Delete, Pids, Pause, Resume, Kill, Exec, ResizePty, CloseIO, Update, Wait, Stats, Connect, and Shutdown are implemented, including Checkpoint and checkpoint-backed Create. Checkpoint and Restore remain per-driver optional operations: a request returns Unimplemented when its selected isolation driver does not advertise the exact v1 operation, without preventing ordinary tasks from using the endpoint. The contract also freezes the OCI Features, Process, LinuxResources, containerd runc checkpoint, and versioned A3S CreateOptions type URLs consumed by those methods.

The latest August 24, 2026 x86_64 regression qualification used source revision 9726719e5a66156cd61f8be36ca00998bbcfc871, containerd 2.2.3, Ubuntu 24.04, and Linux 6.8.0-49-generic. Three complete 117.37, 119.36, and 118.94-second matrices ran consecutively through unchanged Host PID 2678296. The release-built shim SHA-256 was 99bacac7a308e4830ca55101ef8148a511526722cf9006d8a37ef9cba89dbf50. The CLI, agent, and qualification executable SHA-256 values were 80d0b69686c73516fc3a507f2545af77b405918584176bdb0a96ab3bcf067102, 68219e592a061b9dba7f491d54716354195cd8f8005fa792ab367681dda5352e, and 3e752abc8ada3b8e3dae9d86e370feb7d17bf04c2245f13888d52ba7537b2fd2. The Cargo.lock SHA-256 was c31f4bb3ea8394cbb05adcb25051994e75c8592b53be7b7d3b5e82f74cfd1727. Each matrix retained the full 2.2.2 lifecycle/restart/I/O suite, exact task Delete response replay, and the committed WriteStdin and CloseStdin shim-death cleanup boundaries. The close boundary delivered CloseIO through the task shim's advertised ttrpc endpoint, retained Closing in schema-v9 metadata, committed the same exec-scoped close directly to the Runtime, observed normal exec exit 29 without changing init, and then removed only the exact Runtime generation after shim SIGKILL. The suite used a dedicated private containerd root, state directory, socket, and systemd unit. The production containerd service was never restarted and remained at PID 2485480. Independent audits after the probe and every pass reported zero tasks, containers, task bundles, live Runtime records, workload cgroups, qualification snapshots, shim processes, qualification processes, or workload processes. This source-build evidence does not extend contract v1's advertised 2.2.2 compatibility claim; the 2.2.3 published-artifact record remains open.

The August 24, 2026 arm64 requalification used source revision 5a6d5f2d817d5951929c2394dff57ef925dd5822, containerd 2.2.2, and Linux 7.0.11-orbstack-00360-gc9bc4d96ac70. Three complete 65.15, 66.76, and 64.11-second matrices ran consecutively through unchanged Host PID 436920. The release-built shim SHA-256 was 801c6ebd6bb6a41f1049dbd64d6ae60165a0914254edb953b2eaf633c6c368f2. The Host CLI, agent, and qualification executable SHA-256 values were 53bf14d72adb347b35d19f936bf91d15adcc3cce65aa88f63886746f07f5ddb2, 28dad74972b28b400a9e5e9f9b38ba59aeaf6662532dfefc7dd5527ff17d6b48, and fa3a513bf2f5aba01a511bc953dcfc5cb1bb05080fbd58bb993d9a0a44a10363. The Cargo.lock SHA-256 was c31f4bb3ea8394cbb05adcb25051994e75c8592b53be7b7d3b5e82f74cfd1727. Every matrix ran an exec to exit 7, deleted it, reused the same containerd exec ID, restarted containerd while the replacement was Added, and observed exit 23 from the new process. It also passed two distinct resource updates, two complete pause/resume cycles, durable terminal stdin before and after live shim replacement, and replay of remotely committed WriteStdin, CloseStdin, SignalProcess, and ResizePty operations while schema-v9 metadata still held the corresponding pending request. The replacement proved the real process moved through SIGSTOP→SIGCONT→SIGSTOP→SIGCONT with four distinct identities, suppressed an identical resize retry, and proved a real A→B→A PTY transition with distinct identities. Each durable Native Linux Host outcome then released its guest replay record, including all derived identities for chunked stdin. The same matrices retained live replacement of committed init Start, exec Start, live init Kill, terminal init Kill, terminal exec SignalProcess, Pause, Resume, and Update. For terminal exec SignalProcess, every pass committed exact sequence 1 SIGTERM and normal exit 29 while the original shim could not observe the response. Replacement-shim and restarted-containerd Wait returned exit 29 without another SignalProcess. The first DeleteProcess then retained a durable identity- and generation-bound receipt; after another shim replacement and containerd restart, a retry returned the identical PID, exit status, and nanosecond exit timestamp while init stayed Running at its original PID. Independent audits after every pass reported zero tasks, containers, task bundles, workload cgroups, matching mounts, live Runtime container records, exact shim, agent, or qualification processes, Host child processes, zombies, and prepared operations; containerd remained active and the one expected Host service remained live. Completed global operation records and generation fences remained only inside the dedicated test Runtime root. After the audit, the original installed shim was restored at SHA-256 a0e7dce493308ebea0b4642dd81a9e489109a8b3709f2a1ede62b015cc123482, and the Runtime root, 1.2 GiB release target, checkout, and logs were removed.

Runtime type and package layout

The containerd runtime type is:

io.containerd.a3s-oci.v2

containerd resolves that type to this executable name:

containerd-shim-a3s-oci-v2

The binary must be root-owned, executable, and visible in the containerd daemon's PATH. The qualified development host currently uses /usr/local/bin/containerd-shim-a3s-oci-v2. No containerd plugin block is required when standard runtime-v2 binary discovery is available; callers can select it with --runtime io.containerd.a3s-oci.v2.

Tagged Linux host archives have this contract-v1 layout:

a3s-oci-runtime-v<version>-linux-<architecture>/
├── a3s-oci
├── a3s-oci-agent
├── containerd-shim-a3s-oci-v2
├── README.md
├── CHANGELOG.md
├── LICENSE
├── compat/
│   └── containerd-runtime-v2.json
└── docs/
    └── containerd-runtime-v2.md

The archive entry is installed at /usr/local/bin/containerd-shim-a3s-oci-v2; it is not renamed or wrapped. Linux x86_64 and arm64 archives build the CLI, agent, and shim for the exact musl target with Rust's bundled linker. Packaging rejects an executable with an ELF interpreter or NEEDED dependency, so these entries do not inherit a host glibc requirement. Static packaging is a portability property, not a driver or containerd support claim. The release workflow includes the complete archive in SHA256SUMS and the signed provenance described in release verification.

The shim does not execute a driver directly. It connects to the long-lived A3S OCI host service through the SDK endpoint. The default Unix socket is:

/run/a3s-oci/runtime.sock

A3S_OCI_RUNTIME_ENDPOINT overrides that path. The legacy A3S_OCI_RUNTIME_SOCKET name is accepted only as a fallback. The host service, static agent, and their immutable assets are separate package entries; the runtime socket and containerd task bundles are runtime state and must not be shipped in a package.

The layout is frozen, but published-artifact qualification remains open. A release is not qualified until it records at least the containerd version, shim checksum, OCI Runtime commit, Cargo lock digest, SDK protocol, agent protocol, driver, kernel, and host architecture.

Guest replay-journal lifetime

The Native Linux executor keeps at most 4,096 completed or in-flight mutation records so a lost Host response can replay an effect without running it twice. The Host acknowledges Create, Start, Kill, Delete, Exec, Pause, Resume, Update, WriteStdin, CloseStdin, Resize, and SignalProcess only after success or a terminal failure is durably committed. It repeats the acknowledgement when a completed Host result is replayed. Retryable failures, prepared operations, and asynchronous unit operations that are still executing are never released. An acknowledgement containing any pending operation fails atomically, and an unknown identity succeeds because it is already absent.

A Host stdin request may cross the 4 MiB guest payload boundary and become several deterministic guest operations. The Native Linux driver retains the complete parent-to-child identity set until the Host outcome commits and then acknowledges the whole set as one batch. A failed acknowledgement restores that set for retry.

Native Linux releases replay records locally. Protocol-v10 utility-VM drivers carry the same boundary through the bounded acknowledge-operations maintenance request; protocol-v1 through protocol-v9 peers retain the compatibility no-op. Host File upload and Filesystem mkdir/move/remove now use v3 durable operation records and join this commit boundary. A lost acknowledgement is retried from the completed Host journal without redispatching the workload mutation.

Identity mapping

containerd and the SDK keep separate identity domains:

InputA3S identity
containerd namespace + task IDctrd- plus a length-framed SHA-256 digest; stable and bounded
New containerd task incarnationRandom 32-byte value stored as 64 lowercase hexadecimal characters in the shim bundle
Runtime createMonotonic runtime generation returned by the host service
namespace + task ID + exec ID + exec incarnationexec- plus a length-framed SHA-256 digest
Mutationctrd-op- plus namespace, task ID, task incarnation, optional exec ID and exec incarnation, and action digest

The named encoding is sha256-length-framed-u64be-v1. For each component, the shim feeds an unsigned 64-bit big-endian byte length followed by the exact component bytes into SHA-256, then adds the identity-domain prefix outside the digest. Fixed compatibility vectors are:

InputOutput
namespace k8s.io, task ID task/actrd-9e87b4d0ad12d991219bcd3bb40312c1e1abce101b71be36752f3b7de9550106
namespace k8s.io, task ID task, exec ID shell, exec incarnation 1exec-3e8eaef01bc980653a2a276d5b11463dc2714fbe1673bd871108180d4d6473b2

Generation mapping is runtime-assigned-monotonic-exact. Create sends the stable derived container ID and receives a monotonic generation from the Host; the shim stores that returned generation in its metadata. It never derives a generation from containerd input, looks up an unqualified current generation, or changes generation on a later task request.

Recreating the same namespace and task ID intentionally keeps the derived SDK container ID while allocating a new incarnation and runtime generation. The new incarnation prevents a replay from the deleted task from matching a new mutation. Every live request carries the exact runtime generation.

Each successful containerd Exec allocation increments a durable per-task sequence and assigns that value as the exec incarnation. DeleteProcess removes the current exec record but retains the sequence. Reusing the same containerd exec ID therefore produces a different SDK process identity and a fresh set of exec-scoped mutation identities, including after shim or containerd restart.

DeleteProcess response durability is separate from schema-v10 task metadata. Before removing a stopped exec from that metadata, the shim atomically writes a3s-oci-shim-exec-delete-v1.json with the task identity, Runtime generation, bundle, exec incarnation, PID, exit status, and nanosecond exit time. Presence of the exec in main metadata means the receipt is only a pre-commit intent and rehydration discards it. Absence of the exec is the commit marker, so a replacement can replay the exact response. A new exec with the same ID first commits its fresh incarnation to main metadata and then consumes the old receipt; task Delete and DeleteShim remove the entire journal.

Task Delete response durability uses a separate a3s-oci-shim-task-delete-v1.json receipt. Before dispatching Runtime delete, the shim records the namespace, task ID and incarnation, derived container identity, exact Runtime generation, bundle, PID, exit status, and nanosecond exit time. Retained main metadata plus a live exact generation proves the receipt is only an uncommitted intent and rehydration removes it. After task removal, a metadata-free replacement validates the serving namespace, task ID, and bundle and can reproduce the first Delete response exactly. The replay-only service signals its ExitSignal after returning that response; without this transition, containerd 2.2.3 can consume the response while the manually launched replacement survives indefinitely as an unowned shim.

The shim stores its incarnation and generation-bound metadata in the containerd-owned task bundle. Rehydration verifies namespace, task ID, generation, driver, and isolation against the host service and fails closed on any drift. Metadata schema v10 records the last allocated task exec sequence and every current exec incarnation. It also retains the last completed init and exec stdin sequence, an optional in-flight sequence plus its exact bounded payload, the Open, Closing, or Closed state of each stdin stream, and the last output byte cursor after each kernel-accepted FIFO prefix is durably committed. For each terminal process it records a separate completed resize sequence, one pending size, and the last committed size. Init and every exec also have an independent completed signal sequence plus one pending signal and the init-only all flag. The task record stores the last completed per-task control sequence, an optional in-flight Pause, Resume, or Update, and the last completed Update request digest. A pending schema-v9 Update also retains its exact LinuxResources body and verifies that body against the canonical digest before replay. Rehydration replays pending Pause, Resume, and body-complete Update operations before accepting task work. A schema-v3 through schema-v8 digest-only pending Update remains readable but is not guessed or dispatched; the first matching caller retry supplies the body, upgrades the journal, and uses the original operation identity. Schema-v1 records default input sequences, output cursors, and control state to empty. Schema-v2 records preserve output cursors, schema-v3 adds the control journal, schema-v4 adds sequenced writes, schema-v5 adds durable stdin close state, schema-v6 adds durable terminal resize state, schema-v7 adds durable init and exec signal state, schema-v8 adds exec incarnations, schema-v9 adds pending Update resource bodies, and schema-v10 adds the durable checkpoint-restore Start barrier. Schemas v1 through v4 default stdin close state to Open. Schemas v1 through v5 default resize state to empty. Schemas v1 through v6 default signal state to empty, and schemas v1 through v7 default exec incarnations to zero. Incarnation zero preserves the legacy process and operation identity encoding. Every legacy schema is rewritten as schema v10 on the next metadata commit, except that a digest-only pending Update remains encoded as schema v8 until a matching caller retry supplies its resource body.

Before dispatching Create, the shim separately commits a schema-v2 create intent containing the exact incarnation, isolation request, bundle, I/O shape, rootfs ownership, and optional immutable checkpoint artifact/reference pair. Schema v1 remains readable for ordinary Create. If the shim dies before it can record the returned generation, DeleteShim replays the same digest-bound Create or Restore with the same operation identity. The runtime either joins the request still in progress or returns its completed result, after which DeleteShim kills and force-deletes that exact generation. It never guesses a current generation from the stable container ID.

API mapping

The adapter uses the public a3s-oci-sdk; it does not call A3S Box or import a driver implementation.

A code-owned translation table freezes 24 exact Task and FIFO-pump routes. The 18-operation required union gates endpoint admission, while Checkpoint and Restore form a separate two-operation optional union negotiated against the driver owning the selected isolation class. RuntimeInfo publishes the required union under dev.a3s.oci.containerd-sdk-operations, and --version prints every route. Manifest contract tests prohibit dependencies on A3S Box, the Host Runtime implementation, the Agent, or Core internals.

Four implemented routes remain local to the shim. Exec(stage) validates and durably allocates an exec incarnation without dispatching an SDK operation; Start(exec) first durably advances Added to Starting before opening a Runtime adapter, then calls processes and dispatches the public SDK exec operation only when the exact incarnation is absent. Delete(exec) removes a stopped exec incarnation locally, while Connect and Shutdown only coordinate the shim process. All 17 Task methods are implemented; optional Checkpoint and Restore do not enter the required SDK-operation union.

containerd Tasks operationSDK/runtime action
CreateValidate the OCI bundle and typed create options, mount the supplied rootfs, then create; when checkpoint names a committed A3S package, call restore and persist a pending Start barrier
Startstart for a normal init; resume a restored paused generation exactly once before advancing its durable barrier; for exec, inspect processes and dispatch exec for the exact staged incarnation
StateExact-generation state or process inventory plus durable exit evidence
Waitwait or wait_process
Killkill for init, signal_process for exec
Delete / DeleteProcessStopped lifecycle delete, or durable removal of the current exec while retaining its allocation sequence and an exact replayable response receipt
ExecDecode the OCI Process and durably allocate a fresh incarnation; defer the SDK exec call until Start
ResizePtyExact-process resize
CloseIODrain the FIFO and call close_stdin once
Pause / Resumepause / resume
UpdateDecode OCI LinuxResources, then update
PidsExact-generation processes
Statsstats, encoded as containerd cgroup-v2 metrics
Connect / ShutdownShim process coordination; no second lifecycle state
CheckpointRequire a paused live task, negotiate the selected driver's checkpoint@1, write a3s-oci-checkpoint-v1.bin, and atomically commit a3s-oci-checkpoint-v1.json

The checkpoint request path is a caller-owned directory, matching containerd's runtime-v2 packaging contract. The manifest binds the fixed artifact filename to the complete immutable SDK reference, size, and SHA-256 digest. A committed package replays locally without another SDK mutation; source drift, a different manifest, symlinks, oversized manifests, and artifact tampering fail closed. The runc CheckpointOptions payload is accepted only when every option has neutral v1 semantics and image_path, when present, equals the Task path. Incremental parent_checkpoint, exit-after-checkpoint, open-socket, namespace, cgroup-mode, and work-path semantics remain unsupported.

Checkpoint-backed Create validates that package before dispatching SDK Restore. SDK v1 returns a paused running generation, but the shim reports containerd CREATED until the first Task Start commits a stable Resume. Schema v10 persists PendingStart or Started, serializes that barrier with task controls and checkpoint capture, and reconciles a committed Resume after a shim crash before metadata advance. Pause, Resume, Update, Kill, Exec, and a new Checkpoint cannot bypass a pending Start; DeleteShim can replay a schema-v2 restore intent and clean the exact generation.

Create without A3S options selects shared-host-kernel. The versioned dev.a3s.oci.runtime.v1.CreateOptions payload can request shared-host-kernel or dedicated-vm. shared-guest-kernel, unknown fields, unknown versions, and foreign option types fail closed. The dedicated-VM route is not containerd-qualified yet.

Pause, Resume, and Update share one monotonically increasing per-task control sequence. Their SDK operation identities include that sequence, so a later pause cycle or resource update cannot replay an earlier result. A task-scoped async gate serializes concurrent control requests without blocking controls for another task. Before dispatch, the shim durably records the sequence and operation kind; Update also records a canonical JSON SHA-256 fingerprint whose object ordering is stable after a shim or host-process restart. Retryable errors retain that pending identity, terminal errors close it, and a completed request commits the returned runtime record and sequence atomically. An identical completed retry is answered without a second SDK dispatch. The host Runtime writes canonical fingerprints as durable operation schema v2 while continuing to load and validate schema-v1 journals with their original encoding.

Init and exec stdin use separate durable sequences. Before each FIFO chunk is sent, the shim stores the next sequence and exact bytes; it clears that pending entry only after the Runtime accepts the matching SDK operation. A replacement shim first replays any pending entry with the same OperationId and payload, then continues at the next sequence. Reusing a sequence with different bytes, skipping a sequence, attaching journal state to a process without stdin, or loading an oversized pending payload fails closed. The retained arm64 gate proves that a live terminal exec receives input before and after manual shim replacement without duplicating the first remote effect. It also freezes the original shim after the next exact payload is durable but before dispatch can finish, commits that same operation through the Runtime, and replaces the shim while its journal still records the payload as pending. The replacement must join the completed operation, emit the input effect exactly once, clear the pending entry, and continue from the following sequence.

Init and exec signals use independent durable sequences and process-local serialization gates. The shim stores the next sequence, exact signal, and the init-only all flag before dispatch. SDK identities are kill-{sequence} for init and signal-{sequence} for exec, so a repeated signal after an intervening mutation is a new operation. Retryable failures retain the pending request; terminal failures close its sequence; replacement rehydration replays an unsettled request with the original identity before serving new signals. The retained arm64 gate freezes the Runtime with exec sequence 1 SIGSTOP pending, freezes and kills the original shim after committing that exact Runtime request, and requires the replacement to join the completed operation without another signal effect. It then dispatches SIGCONT, SIGSTOP, and SIGCONT as sequences 2 through 4 and reads /proc/<pid>/status after every request to prove the actual workload transitions instead of journal-only success.

The retained init-Kill replacement gate covers the task-scoped half of the same contract. It freezes the Runtime with sequence 1 SIGSTOP and all=true pending, freezes the original shim, commits the exact kill-1 Runtime request, and replaces the shim before that response can be observed. The replacement must join the operation without another signal effect, preserve the incarnation, generation, init PID, exec PID, and exact shim ownership, and prove both processes are stopped. Sequences 2 through 4 then deliver SIGCONT with all=true, SIGSTOP with all=false, and SIGCONT with all=false; /proc/<pid>/status must show fanout to both processes only for all=true. A unit boundary also proves that a pending terminal init signal is settled from durable exit evidence without redispatch. The August 24, 2026 three-pass Native Linux/containerd 2.2.2 matrix retains this boundary.

The terminal init-Kill replacement gate covers the stopped outcome directly. It freezes the Host after schema-v10 sequence 1 SIGTERM is durable, freezes the original shim, resumes the Host, and commits the exact kill-1 request. The workload trap exits 42, and the Runtime retains both its exact Stopped record and durable Wait result before the original shim can observe the Kill response. A replacement first imports that exit with one bounded exact-generation Wait, persists it, and then settles the pending signal without dispatching another Kill. It must retain the generation and exact shim ownership, serve exit 42 through its own Wait, and preserve exit 42 through the restarted containerd daemon's Wait and Delete calls.

Init and exec terminal resize use separate durable sequences and process-local serialization gates. The shim stores the next sequence and exact dimensions before dispatch. Its SDK operation identity is derived from that sequence; the Runtime request fingerprint independently binds the process and dimensions. This distinction is required for A→B→A: the second A must not reuse the first A's cached Runtime result while the real terminal remains at B. A successful response atomically commits the sequence and size. Retryable failures retain the pending operation for exact replay; a confirmed process exit settles it without leaving recovery blocked. A completed same-size request is a no-op. The retained arm64 gate freezes the Host Runtime with sequence 3 pending, freezes the original shim, commits the exact resize directly, kills that shim, and requires its replacement to join the completed Runtime operation and clear the pending record. It then proves same-size suppression and real terminal dimensions across sequences 4 and 5 for the full A→B→A transition.

CloseStdin uses a separate durable state machine. The shim commits Closing only after every FIFO byte and pending write has completed, then dispatches the stable process-scoped close-stdin operation. A successful response commits Closed. A replacement that loads Closing replays that exact operation without opening the FIFO and commits Closed; a replacement that loads Closed returns a completed CloseIO result without opening the FIFO or dispatching another SDK operation. The retained arm64 gate freezes the original shim in Closing, commits the Runtime effect while its response cannot be observed, replaces the shim, and requires one terminal EOF marker plus a successful repeated CloseIO.

Output reads request at most 64 KiB from the SDK, require a contiguous global byte cursor, and reject empty data chunks, data-bearing EOF, cursor gaps, and frames after a stream's EOF. Non-terminal stdout and stderr retain separate FIFOs; terminal output accepts only the merged stdout PTY stream. After every kernel-accepted FIFO write, including a partial write, the shim durably commits the exact delivered prefix before attempting the suffix. Cancellation stops without advancing over unwritten bytes, and the SDK's partial-frame pagination lets a replacement resume at the first undelivered byte. A configured output FIFO without durable cursor persistence fails closed.

During restoration, output replay can begin immediately when a pump is created. The shim therefore persists and publishes the validated task into its in-memory map before it starts init or exec pumps. The first delivered bytes can then advance the durable cursor against that exact task instead of racing a missing entry. A deterministic FIFO test supplies output before restoration starts and requires the metadata cursor, in-memory cursor, and FIFO bytes to converge. If a later exec pump cannot be created, restoration stops every pump already started before removing the published task.

Restart and cleanup contract

The real gate restarts containerd while init is Created, Running, and Stopped; while an exec is Added, Running, and Stopped; while a terminal exec is Running; and while four independent tasks are Running. PID, terminal mode, exit status, incarnation, and runtime generation must not drift.

The exec-reuse gate starts restart-exec, observes exit 7, deletes that exec, and adds restart-exec again. It restarts containerd before the second Start, then requires a new SDK process identity and exit 23 from the replacement. The deleted incarnation must not replay its Exec operation or publish a late exit into the replacement.

The DeleteProcess response gate verifies the exact receipt after deleting a normally exited exec, suspends containerd, kills and reaps the current shim, launches its replacement from the same bundle, and restarts containerd. A direct retry through the replacement must return the first response's exact PID, status, seconds, and nanoseconds. The receipt must remain bound to the same task incarnation and Runtime generation, while the init process and replacement-shim ownership remain unchanged.

The task Delete response gate stops a workload normally, invokes Task Delete directly so containerd cannot observe the response, and verifies the exact v1 receipt after main shim metadata has disappeared. With containerd suspended, it kills and reaps the original shim, launches a replacement against the same bundle and socket, and requires a direct retry to return the original PID, status, seconds, and nanoseconds. Restarted containerd must then consume the same response through Task Delete or DeleteShim, remove the bundle, and leave no task state. The manually launched replacement must exit within its bounded deadline after serving the response.

The gate also suspends containerd, kills the live shim, starts a replacement shim from the same bundle and socket, kills the suspended daemon, and restarts containerd. A live terminal exec must retain its workload PID, incarnation, runtime generation, replacement shim PID, completed stdin sequence, and output cursor. Output delivered before the replacement must not replay; new stdin must use the next durable operation identity and reach the original PTY; and a resize issued after replacement must produce only its new terminal dimensions. The committed-resize boundary repeats the replacement while ResizePty is durably pending but already complete in the Runtime. The replacement must use the same operation identity, avoid a second PTY effect, preserve the live PID, and commit the exact size before serving a same-size retry or the later A→B→A sequence.

containerd 2.2 treats an already-stopped shim as leaked during some daemon recovery paths. In that case it invokes DeleteShim. The shim replays durable exit evidence, removes only the exact runtime generation and bundle, and leaves caller-owned container metadata removable.

If the shim itself receives SIGKILL while init is Created or Running, or while an exec is Added or Running, containerd's leak handler must terminate the exact workload, force-delete its runtime generation, and remove the task bundle. It must retain the container metadata. Recreating the same task ID must produce a new incarnation and generation. Starting a standalone shim while containerd's event endpoint is unavailable is not a supported recovery path; the safe outcome is complete cleanup rather than an untracked live workload.

The same gate kills the shim while Create is in flight after its durable intent commit but before the RPC returns. The host service is suspended at that exact boundary and resumed only after the shim dies. Cleanup must converge the original operation, delete its one resulting generation, and leave no runtime state, task, workload process, bundle, or shim while preserving caller-owned container metadata.

The post-commit Create gate stops the Host before dispatch, waits for the complete create intent, and stops the shim before resuming the Host. It then loads the recorded bundle, derives the same attachment and process-I/O contract, and submits the exact namespace, task incarnation, container ID, isolation request, and stable Create operation identity directly through the public SDK. After the Runtime returns a Created record, the gate verifies its exact generation, driver, isolation, and PID, kills the still-stopped shim, and requires DeleteShim to join that committed result. Both the original exact generation and the current container identity must become NotFound, exposing any duplicate generation or reroute, while process, rootfs, bundle, shim, and task state disappear and caller-owned container metadata remains.

The post-commit Start gate submits the shim's exact stable Start identity while durable shim metadata still records Created, then kills the shim after the runtime reports that generation Running. DeleteShim must bound its kill/wait path, force-delete only that generation, and leave no process, runtime state, bundle, or shim while preserving containerd-owned metadata.

The committed Start rehydration gate exercises the live-workload outcome instead of DeleteShim cleanup. It suspends the Host with an init Start request pending, freezes the original shim, commits that task incarnation's exact Start identity directly through the public SDK, and replaces the shim before the original response can be observed. Because init lifecycle state already has one authoritative owner, the replacement does not add a second shim-side Start journal. It reads the exact runtime generation, adopts its Running record, and must preserve the PID, driver, isolation, configuration digest, and attachments digest. A repeated containerd Start then joins the same Host operation and returns the original PID without another driver mutation. The matching unit boundary also proves the other side of exec recovery: a durable Starting exec absent from runtime process inventory is dispatched exactly once with its persisted incarnation, while an existing process is adopted without another Exec call.

The committed exec-Start rehydration gate covers that adoption against a real process. An exec is durably allocated as schema-v10 incarnation 1 in Added, then Start commits Starting before any Runtime adapter connection. With the Host suspended at that connection boundary, the gate observes the exact metadata, freezes the original shim, resumes the Host, and submits the same incarnation-bound Runtime Exec directly. It then replaces the shim before the original Start response can be observed. Rehydration must adopt the existing generation-scoped process and PID, persist Started with the exact record, and serve a Start retry with the original PID. Runtime inventory must contain exactly one matching exec, the replacement shim PID must remain authoritative, and deleting the exec must leave init Running until its separate cleanup.

The post-commit Exec gate records the process as Added in shim metadata, then submits the exact stable Runtime Exec and verifies its generation-scoped process identity and live PID before killing the shim. DeleteShim must reap both the init and unrecorded exec PIDs and remove only that runtime generation, task bundle, and shim while preserving containerd-owned metadata.

The post-commit SignalProcess gate starts an exec, suspends the shim, then submits the exact stable exec-scoped SIGKILL mutation directly to the runtime. It requires the exact signal-9 exit while the init remains Running with its original PID before killing the stopped shim. DeleteShim must then remove the terminal exec, live init, exact generation, task bundle, and shim without touching containerd-owned metadata.

The post-commit WriteStdin gate creates a non-terminal exec that exits 23 only after receiving committed-before-cleanup. It stops the Host before the shim can dispatch those bytes, then requires schema-v10 metadata to retain exec incarnation 1, pending sequence 1, and the exact byte payload while the committed stdin sequence is still canonically omitted as zero. With the shim also stopped, the gate resumes the Host and submits the same incarnation-bound write-stdin-1 identity and exact process target directly through the public SDK. The exec must exit 23 from that one write while init remains Running at its original PID and generation. After shim SIGKILL, DeleteShim must not redispatch the locally pending bytes; it kills and force-deletes only the exact task generation, clears shim metadata and the bundle, and retains containerd-owned container metadata. A focused unit boundary begins with one recorded Runtime write and asserts that DeleteShim leaves the write count unchanged while fencing Kill and force Delete to that generation.

The post-commit CloseStdin gate creates a non-terminal exec that exits 29 only after stdin reaches EOF. It stops the Host, invokes CloseIO through the task shim's ttrpc address from its validated bootstrap.json, and requires schema-v10 metadata to retain exec incarnation 1, a canonically omitted zero stdin sequence, no pending write, and Closing before the request can reach the Runtime. With the shim also stopped, the gate resumes the Host and submits the same incarnation-bound close-stdin-1 identity and exact process target directly through the public SDK. The exec must exit 29 from that one EOF while init remains Running at its original PID and generation. After shim SIGKILL, DeleteShim must not replay the locally pending close; it kills and force-deletes only the exact task generation, clears shim metadata and the bundle, and retains containerd-owned container metadata. A focused unit boundary begins with one recorded Runtime close and asserts that DeleteShim leaves the close count unchanged while fencing Kill and force Delete to that generation.

The post-commit ResizePty gate creates a terminal exec, stops the Host, sends the resize through the shim's ttrpc address from its validated bootstrap.json, and requires schema-v10 metadata to retain exec incarnation 1 with pending sequence 1 at 166x52. With the shim also stopped, the gate resumes the Host and submits the same incarnation-bound resize-1 identity and exact process target directly through the public SDK. It verifies the live PTY size through /proc/<pid>/fd/0 and TIOCGWINSZ, kills the shim, and requires the original ttrpc response to be lost. DeleteShim must not replay the locally pending resize; it kills and force-deletes only the exact task generation, clears shim metadata and the bundle, and retains containerd-owned container metadata. A focused unit boundary begins with one recorded Runtime resize and asserts that DeleteShim leaves the resize count unchanged while fencing Kill and force Delete to that generation. The gate passes non-destructive CI and the machine-readable compatibility record retains three consecutive current real-daemon observations through one unchanged Host process.

The committed SignalProcess rehydration gate instead keeps the exec alive. It persists sequence 1 SIGSTOP, freezes the original shim and Runtime, commits the exact signal directly to the Runtime, then kills and replaces the shim before the local journal can observe the response. The replacement must replay the same operation identity, clear the pending record, retain the exec PID and generation, and continue with fresh sequences for SIGCONT→SIGSTOP→SIGCONT.

The terminal SignalProcess variant commits sequence 1 SIGTERM and its exact normal exec exit before replacement. Rehydration performs a zero-timeout, exact-generation WaitProcess before signal replay. If the Runtime already retains the exit, the shim persists Exited plus the observation time and settles the sequence without another SignalProcess; if the exec remains live, DeadlineExceeded leaves the existing replay path unchanged. The replacement shim and restarted containerd must return the same exit while init remains Running. DeleteProcess must retain that exit and its response receipt must survive the subsequent replacement boundary described above.

The committed init-Kill rehydration gate retains sequence 1 SIGSTOP with all=true, commits its exact kill-1 identity while the original shim cannot receive the response, and replaces that shim while the init and exec remain live. It requires unchanged process and runtime identities, exact replacement shim ownership, and real stopped states for both processes. Subsequent sequence-bound signals prove all=true continue fanout and all=false init-only stop/continue isolation. The August 24, 2026 three-pass Native Linux/containerd 2.2.2 matrix retains this boundary.

The post-commit Kill gate submits the exact stable SIGSTOP mutation to a running generation, verifies that the runtime retains the same live PID, and kills the shim before local state observes that mutation. DeleteShim must send the terminal signal, bound its wait, reap that exact PID, and remove the exact generation without touching containerd-owned metadata.

The committed control gates repeat the same lost-response boundary for Pause, Resume, and Update. They verify the real freezer state and the exact pids.max read-back before killing the shim, then require bounded deletion of the same generation with no process, cgroup, bundle, or shim residue. DeleteShim sends an exact force Delete directly for a paused generation instead of waiting on a terminal signal that a frozen process cannot complete; the runtime thaws and stops that generation inside the one deletion operation.

The post-commit Delete gate stops the task, submits the shim's exact stable StoppedOnly Delete identity directly to the runtime, and kills the shim while its durable local metadata and rootfs ownership still exist. DeleteShim must accept NotFound only for that exact generation and only after its stable normal or force Delete identity replays a committed deletion. It then finishes local cleanup and preserves containerd-owned container metadata. Missing replay evidence and generation conflicts remain hard failures, so state loss or a replacement can never be mistaken for the deleted task.

Run the real qualification

The test is ignored because it is destructive: it requires root, restarts containerd repeatedly, sends SIGKILL, and creates temporary tasks and containers with an a3s-r9- prefix.

cargo test -p a3s-oci-containerd-shim \
  --test containerd_runtime_v2 \
  --no-run

sudo env \
  A3S_OCI_CONTAINERD_QUALIFY=1 \
  A3S_OCI_CONTAINERD_ALLOW_RESTART=1 \
  A3S_OCI_CONTAINERD_SOCKET=/run/containerd/containerd.sock \
  CONTAINERD_ADDRESS=/run/containerd/containerd.sock \
  ./target/debug/deps/containerd_runtime_v2-<hash> \
  --ignored --exact real_containerd_runtime_v2_qualification --nocapture

The host service must already be running and the selected shim binary must be installed where containerd can resolve it. Cleanup is prefix-scoped and the test fails if any matching task or container remains. For an isolated daemon, set both A3S_OCI_CONTAINERD_SOCKET and CONTAINERD_ADDRESS to its socket: the gRPC/ttrpc clients consume the former, while spawned ctr commands inherit the latter. Set A3S_OCI_CONTAINERD_TTRPC_ADDRESS to that daemon's ttrpc socket, A3S_OCI_CONTAINERD_STATE_ROOT to its private io.containerd.runtime.v2.task state directory, A3S_OCI_RUNTIME_ENDPOINT to the selected Host service socket, and A3S_OCI_CONTAINERD_SERVICE to the isolated systemd unit before allowing the suite to restart containerd.

Open release gates

  • qualify the supported containerd version range from exact release packages;
  • qualify the exact signed and checksummed published shim, host-service, agent, and driver archives;
  • qualify Checkpoint, checkpoint-backed Create, restored Start, package handoff, containerd content-store import, restart, and leak cleanup with a production driver that explicitly advertises Checkpoint and Restore;
  • run the same suite for every driver profile advertised through containerd;
  • complete OCI conformance, security review, upgrade/rollback, and release soak gates.