Skip to content

Commit e8ece3c

Browse files
author
Roy Lin
committed
feat(runtime): prove multi-container lifecycle isolation
1 parent d55ca0d commit e8ece3c

21 files changed

Lines changed: 2645 additions & 38 deletions

File tree

.github/scripts/native-linux-smoke.sh

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ sudo apt-get install --yes busybox-static jq
1111
cargo build -p a3s-oci-agent -p a3s-oci-cli
1212

1313
bundle="$runner_temp/a3s-native-bundle"
14+
bundle_b="$runner_temp/a3s-native-bundle-b"
1415
work_parent="$runner_temp/a3s-native-work"
15-
mkdir -p "$bundle/rootfs/bin" "$work_parent"
16-
cp fixtures/native-linux/config.json "$bundle/config.json"
17-
cp "$(command -v busybox)" "$bundle/rootfs/bin/busybox"
18-
ln -s busybox "$bundle/rootfs/bin/sh"
16+
mkdir -p "$bundle/rootfs/bin" "$bundle_b/rootfs/bin" "$work_parent"
17+
for candidate in "$bundle" "$bundle_b"; do
18+
cp fixtures/native-linux/config.json "$candidate/config.json"
19+
cp "$(command -v busybox)" "$candidate/rootfs/bin/busybox"
20+
ln -s busybox "$candidate/rootfs/bin/sh"
21+
done
1922

2023
run_smoke() {
2124
local expected_kvm_present="$1"
@@ -31,6 +34,69 @@ run_smoke() {
3134
<<<"$output" >/dev/null
3235
}
3336

37+
run_multi_container_smoke() {
38+
local expected_kvm_present="$1"
39+
local output
40+
output="$(sudo "$PWD/target/debug/a3s-oci" \
41+
native-linux-multi-container-smoke \
42+
--agent "$PWD/target/debug/a3s-oci-agent" \
43+
--bundle-a "$bundle" \
44+
--bundle-b "$bundle_b" \
45+
--work-parent "$work_parent")"
46+
printf '%s\n' "$output"
47+
jq --exit-status \
48+
--argjson expected "$expected_kvm_present" \
49+
'.schema_version == "a3s.oci.native-linux-multi-container-smoke.v1"
50+
and .platform == "linux" and .status == "available"
51+
and .kvm_device_present == $expected
52+
and .bundles_loaded
53+
and .service_operations
54+
== ["features", "create", "state", "start", "kill", "delete"]
55+
and .lifecycle.distinct_bundle_directories
56+
and .lifecycle.distinct_rootfs_directories
57+
and .lifecycle.both_created_before_start
58+
and .lifecycle.initial_generation_a == 1
59+
and .lifecycle.initial_generation_b == 1
60+
and .lifecycle.recreated_generation_a == 2
61+
and (.lifecycle.created_pid_a > 0)
62+
and (.lifecycle.created_pid_b > 0)
63+
and .lifecycle.distinct_created_pids
64+
and .lifecycle.create_replays_exact
65+
and .lifecycle.both_markers_absent_before_start
66+
and .lifecycle.start_a_replayed
67+
and .lifecycle.marker_a_verified
68+
and .lifecycle.b_unchanged_after_a_start
69+
and .lifecycle.marker_b_absent_after_a_start
70+
and .lifecycle.kill_a_replayed
71+
and .lifecycle.a_stopped
72+
and .lifecycle.b_unchanged_after_a_kill
73+
and .lifecycle.marker_b_absent_after_a_kill
74+
and .lifecycle.delete_a_replayed
75+
and .lifecycle.a_missing_after_delete
76+
and .lifecycle.b_unchanged_after_a_delete
77+
and .lifecycle.stale_generation_rejected
78+
and .lifecycle.generation_a_monotonic
79+
and .lifecycle.recreate_a_replayed
80+
and .lifecycle.marker_a_absent_after_recreate
81+
and .lifecycle.cross_container_operation_rejected
82+
and .lifecycle.b_unchanged_after_replay_conflict
83+
and .lifecycle.recreated_a_deleted
84+
and .lifecycle.start_b_replayed
85+
and .lifecycle.marker_b_verified
86+
and .lifecycle.kill_b_replayed
87+
and .lifecycle.b_stopped
88+
and .lifecycle.delete_b_replayed
89+
and .lifecycle.b_missing_after_delete
90+
and .markers_removed
91+
and .executor_runtime_clean
92+
and .session_root_clean
93+
and (.reason == null)' \
94+
<<<"$output" >/dev/null
95+
test ! -e "$bundle/rootfs/.a3s-oci-native-smoke"
96+
test ! -e "$bundle_b/rootfs/.a3s-oci-native-smoke"
97+
test -z "$(find "$work_parent" -mindepth 1 -print -quit)"
98+
}
99+
34100
run_fault_cleanup() {
35101
local phase
36102
local output
@@ -94,6 +160,8 @@ if [[ -e /dev/kvm || -L /dev/kvm ]]; then
94160
fi
95161

96162
run_smoke false
163+
run_multi_container_smoke false
97164
run_fault_cleanup
98165
sudo mkdir /dev/kvm
99166
run_smoke true
167+
run_multi_container_smoke true

.github/workflows/ci.yml

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,136 @@ jobs:
449449
)"
450450
test "$endpoint_after" = "$endpoint_baseline"
451451
test ! -e "$marker"
452+
- if: runner.os == 'macOS'
453+
name: Multi-container macOS OCI lifecycle or fail-closed worker
454+
shell: bash
455+
run: |
456+
rootfs_dir="$RUNNER_TEMP/a3s-oci-alpine-aarch64"
457+
rootfs_archive="$RUNNER_TEMP/alpine-minirootfs-3.22.5-aarch64.tar.gz"
458+
signed_dir="$RUNNER_TEMP/a3s-oci-agent-vm-signed"
459+
bundle_a="$rootfs_dir/var/lib/a3s-oci-smoke/bundle"
460+
bundle_b="$rootfs_dir/var/lib/a3s-oci-smoke/bundle-b"
461+
marker_a="$bundle_a/rootfs/.a3s-oci-create-start-smoke"
462+
marker_b="$bundle_b/rootfs/.a3s-oci-create-start-smoke"
463+
console="$RUNNER_TEMP/a3s-oci-multi-container.log"
464+
mkdir -p "$bundle_b/rootfs"
465+
cp fixtures/utility-vm/config.json "$bundle_b/config.json"
466+
tar -xzf "$rootfs_archive" -C "$bundle_b/rootfs"
467+
468+
endpoint_baseline="$(
469+
find /private/tmp -maxdepth 1 \
470+
-name 'a3s-oci-agent-*' -print | sort
471+
)"
472+
runtime_baseline="$(
473+
find "$rootfs_dir/run" -maxdepth 1 \
474+
-name 'a3s-oci-agent-*' -print | sort
475+
)"
476+
set +e
477+
output="$(
478+
target/debug/a3s-oci oci-vm-multi-container-smoke \
479+
--shim "$signed_dir/a3s-oci-krun-shim" \
480+
--vm-rootfs "$rootfs_dir" \
481+
--bundle-a "$bundle_a" \
482+
--bundle-b "$bundle_b" \
483+
--console "$console"
484+
)"
485+
status=$?
486+
set -e
487+
printf '%s\n' "$output"
488+
489+
support="$(sysctl -n kern.hv_support 2>/dev/null || printf unavailable)"
490+
if [[ "$support" == "1" ]]; then
491+
test "$status" -eq 0
492+
jq --exit-status \
493+
'.schema_version == "a3s.oci.oci-vm-multi-container-smoke.v1"
494+
and .platform == "macos" and .status == "available"
495+
and .bundles_loaded
496+
and .lifecycle.distinct_bundle_directories
497+
and .lifecycle.distinct_rootfs_directories
498+
and .lifecycle.both_created_before_start
499+
and .lifecycle.initial_generation_a == 1
500+
and .lifecycle.initial_generation_b == 1
501+
and .lifecycle.recreated_generation_a == 2
502+
and (.lifecycle.created_pid_a > 0)
503+
and (.lifecycle.created_pid_b > 0)
504+
and .lifecycle.distinct_created_pids
505+
and .lifecycle.create_replays_exact
506+
and .lifecycle.both_markers_absent_before_start
507+
and .lifecycle.start_a_replayed
508+
and .lifecycle.marker_a_verified
509+
and .lifecycle.b_unchanged_after_a_start
510+
and .lifecycle.marker_b_absent_after_a_start
511+
and .lifecycle.kill_a_replayed
512+
and .lifecycle.a_stopped
513+
and .lifecycle.b_unchanged_after_a_kill
514+
and .lifecycle.marker_b_absent_after_a_kill
515+
and .lifecycle.delete_a_replayed
516+
and .lifecycle.a_missing_after_delete
517+
and .lifecycle.b_unchanged_after_a_delete
518+
and .lifecycle.stale_generation_rejected
519+
and .lifecycle.generation_a_monotonic
520+
and .lifecycle.recreate_a_replayed
521+
and .lifecycle.marker_a_absent_after_recreate
522+
and .lifecycle.cross_container_operation_rejected
523+
and .lifecycle.b_unchanged_after_replay_conflict
524+
and .lifecycle.recreated_a_deleted
525+
and .lifecycle.start_b_replayed
526+
and .lifecycle.marker_b_verified
527+
and .lifecycle.kill_b_replayed
528+
and .lifecycle.b_stopped
529+
and .lifecycle.delete_b_replayed
530+
and .lifecycle.b_missing_after_delete
531+
and .markers_removed and .guest_runtime_clean
532+
and .bridge.platform == "macos"
533+
and .bridge.status == "available"
534+
and .bridge.protocol_negotiated
535+
and .bridge.advertised_operations
536+
== ["create", "state", "start", "kill", "delete"]
537+
and .bridge.shim_report_verified
538+
and .bridge.shim_exit_code == 0
539+
and .bridge.macos_cleanup.endpoint_removed
540+
and .bridge.macos_cleanup.shim_reaped
541+
and .bridge.macos_cleanup.bridge_reaped
542+
and (.bridge.macos_cleanup.open_descriptors_before > 0)
543+
and (.bridge.macos_cleanup.open_descriptors_before
544+
== .bridge.macos_cleanup.open_descriptors_after)
545+
and .bridge.macos_cleanup.descriptor_inventory_restored
546+
and (.bridge.macos_cleanup.reason == null)
547+
and (.reason == null)' \
548+
<<<"$output" >/dev/null
549+
else
550+
test "$status" -eq 2
551+
jq --exit-status \
552+
'.schema_version == "a3s.oci.oci-vm-multi-container-smoke.v1"
553+
and .platform == "macos" and .status == "unavailable"
554+
and .bundles_loaded
555+
and .bridge.platform == "macos"
556+
and .bridge.status == "unavailable"
557+
and (.bridge.protocol_negotiated | not)
558+
and .bridge.macos_cleanup.endpoint_removed
559+
and .bridge.macos_cleanup.shim_reaped
560+
and .bridge.macos_cleanup.bridge_reaped
561+
and (.bridge.macos_cleanup.open_descriptors_before > 0)
562+
and (.bridge.macos_cleanup.open_descriptors_before
563+
== .bridge.macos_cleanup.open_descriptors_after)
564+
and .bridge.macos_cleanup.descriptor_inventory_restored
565+
and (.bridge.macos_cleanup.reason == null)
566+
and (.reason | length > 0)' \
567+
<<<"$output" >/dev/null
568+
fi
569+
570+
endpoint_after="$(
571+
find /private/tmp -maxdepth 1 \
572+
-name 'a3s-oci-agent-*' -print | sort
573+
)"
574+
runtime_after="$(
575+
find "$rootfs_dir/run" -maxdepth 1 \
576+
-name 'a3s-oci-agent-*' -print | sort
577+
)"
578+
test "$endpoint_after" = "$endpoint_baseline"
579+
test "$runtime_after" = "$runtime_baseline"
580+
test ! -e "$marker_a"
581+
test ! -e "$marker_b"
452582
- if: runner.os == 'macOS'
453583
name: Fault-injected macOS OCI cleanup
454584
shell: bash

README.md

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ Workload calls require an explicitly supplied launch-ready `RuntimeDriver`.
8484
quarantine, backed by exhaustive durable-write and driver-boundary fault
8585
matrices
8686
- **Shared Linux Executor**: Reuse one fail-closed namespace, mount, process,
87-
and cleanup implementation directly on Linux and through the guest agent
87+
and cleanup implementation directly on Linux and through the guest agent,
88+
with independently fenced per-container generations
8889
- **Cross-Platform Drivers**: Inspect native Linux, KVM, HVF, and WHPX
8990
prerequisites without silently weakening requested isolation
9091
- **Typed SDK and IPC**: Expose an async `Send + Sync` Rust contract with
@@ -163,6 +164,27 @@ observation, idempotent mutation replay, marker verification, post-delete
163164
[Native Linux Development](docs/linux-native.md) for the accepted profile and
164165
remaining production gates.
165166

167+
Create a second distinct bundle to run the multi-container gate:
168+
169+
```sh
170+
bundle_b="$demo_root/bundle-b"
171+
mkdir -p "$bundle_b/rootfs/bin"
172+
cp fixtures/native-linux/config.json "$bundle_b/config.json"
173+
cp "$(command -v busybox)" "$bundle_b/rootfs/bin/busybox"
174+
ln -s busybox "$bundle_b/rootfs/bin/sh"
175+
176+
sudo target/debug/a3s-oci native-linux-multi-container-smoke \
177+
--agent "$PWD/target/debug/a3s-oci-agent" \
178+
--bundle-a "$bundle" \
179+
--bundle-b "$bundle_b" \
180+
--work-parent "$work_parent"
181+
```
182+
183+
The versioned report retains two simultaneous create barriers, distinct
184+
positive PIDs, operation replay isolation, A/B lifecycle independence,
185+
generation-1 rejection after A is recreated as generation 2, and complete
186+
process, marker, executor-root, and durable-session cleanup.
187+
166188
The separate fault-cleanup diagnostic deliberately stops before OCI delete and
167189
requires executor shutdown to reclaim the live process and all scoped state:
168190

@@ -300,6 +322,29 @@ running and stopped observation, marker verification, post-delete `NotFound`,
300322
and nominal endpoint, process, marker, and guest-runtime cleanup. This remains
301323
a fixed development profile rather than an arbitrary-workload driver.
302324

325+
The multi-container gate places two distinct bundles in the same guest and
326+
keeps both init processes behind the create barrier:
327+
328+
```sh
329+
bundle_b="$rootfs_dir/rootfs/var/lib/a3s-oci-smoke/bundle-b"
330+
mkdir -p "$bundle_b/rootfs"
331+
cp fixtures/utility-vm/config.json "$bundle_b/config.json"
332+
tar -xzf "$rootfs_archive" -C "$bundle_b/rootfs"
333+
334+
target/debug/a3s-oci oci-vm-multi-container-smoke \
335+
--shim "$smoke_dir/a3s-oci-krun-shim" \
336+
--vm-rootfs "$rootfs_dir/rootfs" \
337+
--bundle-a "$bundle" \
338+
--bundle-b "$bundle_b" \
339+
--console "$rootfs_dir/oci-multi-container.log"
340+
```
341+
342+
`a3s.oci.oci-vm-multi-container-smoke.v1` requires that starting, killing, and
343+
deleting A never changes B; recreating A advances generation 1 to 2; stale and
344+
cross-container replay requests fail; B then completes independently; and VM
345+
shutdown restores guest-runtime, endpoint, descriptor, shim, and worker
346+
inventories.
347+
303348
Fault cleanup reuses the same signed shim and bundle but stops after each
304349
successful lifecycle boundary:
305350

@@ -362,6 +407,12 @@ Matching retries reproduce the original result. Stale generations, reused
362407
operation IDs with different payloads, invalid isolation, and unsupported
363408
configuration fail before driver mutation.
364409

410+
The shared Linux executor independently indexes every live `(container ID,
411+
generation)` pair, retains a highest-generation fence after delete, and gives
412+
each container a private runtime slot. The native Linux and macOS utility-VM
413+
multi-container gates keep two slots live concurrently and verify that every
414+
state transition and replay remains container-scoped.
415+
365416
### Linux executor boundary
366417

367418
The current executor implements a reviewed bootstrap vertical slice:
@@ -401,9 +452,9 @@ boundary.
401452

402453
| Host | Execution path | Retained evidence | Current readiness |
403454
| --- | --- | --- | --- |
404-
| Linux x86_64/aarch64 | Native Linux executor | Real rootful core lifecycle plus shutdown cleanup after create, start, and kill without delete; `/dev/kvm` absent and present-but-unusable | Default inventory `probe-only`; explicitly opened development instance `experimental` |
455+
| Linux x86_64/aarch64 | Native Linux executor | Real rootful core lifecycle, two-container generation and mutation isolation, plus shutdown cleanup after create, start, and kill without delete; `/dev/kvm` absent and present-but-unusable | Default inventory `probe-only`; explicitly opened development instance `experimental` |
405456
| Linux x86_64/aarch64 | libkrun + KVM utility VM | Device access, ioctl result, and KVM API version | `probe-only`; VM driver not implemented |
406-
| macOS arm64 | libkrun + HVF utility VM | Direct HVF VM create/destroy, checksum-pinned context lifecycle, authenticated static arm64 guest agent, fixed OCI core lifecycle, and no-delete cleanup after create, start, and kill | `probe-only`; complete enforcement and recovery pending |
457+
| macOS arm64 | libkrun + HVF utility VM | Direct HVF VM create/destroy, checksum-pinned context lifecycle, authenticated static arm64 guest agent, fixed and two-container OCI lifecycles, and no-delete cleanup after create, start, and kill | `probe-only`; complete enforcement and recovery pending |
407458
| Windows x86_64 | libkrun + WHPX utility VM | Partition, context, guest command, authenticated agent, and fixed OCI core lifecycle | `probe-only`; complete enforcement and recovery pending |
408459

409460
Linux installation, feature inspection, and the native SDK path must work when

ROADMAP.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,11 @@ then may HVF become `experimental`.
270270

271271
### R3 — Shared Linux Executor And Guest Agent
272272

273-
- [ ] Multi-container guest registry with per-container generations.
273+
- [x] Multi-container guest registry with per-container generations, proven
274+
with two distinct bundles, simultaneous create barriers, independent
275+
start/kill/delete, exact replay isolation, generation-1 fencing after
276+
generation-2 recreation, and complete cleanup through native Linux and the
277+
macOS utility VM.
274278
- [x] Create a new UTS namespace and apply the configured hostname and
275279
domainname before the created barrier.
276280
- [x] Create a new mount namespace, make the inherited mount tree recursively

0 commit comments

Comments
 (0)