Skip to content

Commit 1ddb790

Browse files
authored
Merge pull request #13 from A3S-Lab/feat/user-time-namespaces
feat(executor): create user and time namespaces
2 parents 41d3786 + 7854e97 commit 1ddb790

27 files changed

Lines changed: 1557 additions & 364 deletions

File tree

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

Lines changed: 137 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,45 @@
22

33
set -euo pipefail
44

5-
runner_temp="${RUNNER_TEMP:?RUNNER_TEMP must identify the CI job temporary directory}"
6-
run_id="${GITHUB_RUN_ID:?GITHUB_RUN_ID must identify the CI run}"
7-
run_attempt="${GITHUB_RUN_ATTEMPT:?GITHUB_RUN_ATTEMPT must identify the CI attempt}"
5+
qualification_root=""
6+
saved_kvm="/dev/a3s-oci-kvm-$$"
7+
kvm_original_moved=false
8+
kvm_test_directory_created=false
9+
10+
restore_host() {
11+
local command_status=$?
12+
local cleanup_status=0
13+
local status
14+
trap - EXIT
15+
set +e
16+
17+
if [[ "$kvm_test_directory_created" == true && -d /dev/kvm ]]; then
18+
sudo rmdir /dev/kvm
19+
status=$?
20+
if ((status != 0)); then
21+
cleanup_status=$status
22+
fi
23+
fi
24+
if [[ -n "$qualification_root" && -d "$qualification_root" ]]; then
25+
sudo rm -rf --one-file-system "$qualification_root"
26+
status=$?
27+
if ((status != 0)); then
28+
cleanup_status=$status
29+
fi
30+
fi
31+
if [[ "$kvm_original_moved" == true ]]; then
32+
sudo mv "$saved_kvm" /dev/kvm
33+
status=$?
34+
if ((status != 0)); then
35+
cleanup_status=$status
36+
fi
37+
fi
38+
if ((command_status != 0)); then
39+
exit "$command_status"
40+
fi
41+
exit "$cleanup_status"
42+
}
43+
trap restore_host EXIT
844

945
sudo apt-get update
1046
sudo apt-get install --yes busybox-static jq
@@ -23,24 +59,83 @@ jq --exit-status \
2359
)' \
2460
<<<"$features" >/dev/null
2561

26-
bundle="$runner_temp/a3s-native-bundle"
27-
bundle_b="$runner_temp/a3s-native-bundle-b"
28-
work_parent="$runner_temp/a3s-native-work"
29-
mkdir -p "$bundle/rootfs/bin" "$bundle_b/rootfs/bin" "$work_parent"
62+
qualification_root="$(mktemp -d /var/tmp/a3s-oci-native.XXXXXXXX)"
63+
bundle="$qualification_root/bundle"
64+
bundle_b="$qualification_root/bundle-b"
65+
work_parent="$qualification_root/work"
66+
mkdir -p \
67+
"$bundle/rootfs/bin" "$bundle/rootfs/proc" \
68+
"$bundle_b/rootfs/bin" "$bundle_b/rootfs/proc" \
69+
"$work_parent"
3070
for candidate in "$bundle" "$bundle_b"; do
3171
cp fixtures/native-linux/config.json "$candidate/config.json"
3272
cp "$(command -v busybox)" "$candidate/rootfs/bin/busybox"
3373
ln -s busybox "$candidate/rootfs/bin/sh"
3474
done
75+
sudo chown -R 0:0 "$qualification_root"
76+
sudo chmod 0755 "$qualification_root"
77+
78+
report_native_failure() {
79+
local rootfs="$1"
80+
local status
81+
82+
printf '%s\n' 'Native Linux host diagnostics:'
83+
uname -a || true
84+
printf 'LSM stack: '
85+
cat /sys/kernel/security/lsm 2>/dev/null || printf '%s\n' unavailable
86+
printf 'Runner profile: '
87+
cat /proc/self/attr/current 2>/dev/null || printf '%s\n' unavailable
88+
findmnt --target / --output TARGET,SOURCE,FSTYPE,OPTIONS --noheadings || true
89+
findmnt --target "$rootfs" \
90+
--output TARGET,SOURCE,FSTYPE,OPTIONS --noheadings || true
91+
namei --long "$rootfs" || true
92+
sudo sh -c \
93+
'grep -E "^(NoNewPrivs|Seccomp|Cap(Inh|Prm|Eff|Bnd|Amb)):" /proc/self/status' ||
94+
true
95+
if sudo timeout 10s unshare \
96+
--user --map-root-user --mount --fork -- \
97+
sh -c \
98+
'mount --make-rprivate / && mount --rbind "$1" "$1"' \
99+
sh "$rootfs"; then
100+
printf '%s\n' 'Combined user/mount namespace rbind probe: succeeded'
101+
else
102+
status=$?
103+
printf 'Combined user/mount namespace rbind probe: failed (%s)\n' "$status"
104+
fi
105+
106+
if sudo timeout 10s unshare \
107+
--user --map-root-user --fork -- \
108+
unshare --mount --fork -- \
109+
sh -c \
110+
'mount --make-rprivate / && mount --rbind "$1" "$1"' \
111+
sh "$rootfs"; then
112+
printf '%s\n' 'Sequential user-then-mount namespace rbind probe: succeeded'
113+
else
114+
status=$?
115+
printf 'Sequential user-then-mount namespace rbind probe: failed (%s)\n' \
116+
"$status"
117+
fi
118+
119+
sudo dmesg --ctime 2>/dev/null | tail -n 120 || true
120+
}
35121

36122
run_smoke() {
37123
local expected_kvm_present="$1"
38124
local output
39-
output="$(sudo "$PWD/target/debug/a3s-oci" native-linux-smoke \
40-
--agent "$PWD/target/debug/a3s-oci-agent" \
41-
--bundle "$bundle" \
42-
--work-parent "$work_parent")"
125+
local status
126+
if output="$(sudo "$PWD/target/debug/a3s-oci" native-linux-smoke \
127+
--agent "$PWD/target/debug/a3s-oci-agent" \
128+
--bundle "$bundle" \
129+
--work-parent "$work_parent")"; then
130+
status=0
131+
else
132+
status=$?
133+
fi
43134
printf '%s\n' "$output"
135+
if ((status != 0)); then
136+
report_native_failure "$bundle/rootfs"
137+
return "$status"
138+
fi
44139
jq --exit-status \
45140
--argjson expected "$expected_kvm_present" \
46141
'.schema_version == "a3s.oci.native-linux-smoke.v2"
@@ -76,13 +171,22 @@ run_smoke() {
76171
run_multi_container_smoke() {
77172
local expected_kvm_present="$1"
78173
local output
79-
output="$(sudo "$PWD/target/debug/a3s-oci" \
80-
native-linux-multi-container-smoke \
81-
--agent "$PWD/target/debug/a3s-oci-agent" \
82-
--bundle-a "$bundle" \
83-
--bundle-b "$bundle_b" \
84-
--work-parent "$work_parent")"
174+
local status
175+
if output="$(sudo "$PWD/target/debug/a3s-oci" \
176+
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")"; then
181+
status=0
182+
else
183+
status=$?
184+
fi
85185
printf '%s\n' "$output"
186+
if ((status != 0)); then
187+
report_native_failure "$bundle/rootfs"
188+
return "$status"
189+
fi
86190
jq --exit-status \
87191
--argjson expected "$expected_kvm_present" \
88192
'.schema_version == "a3s.oci.native-linux-multi-container-smoke.v2"
@@ -144,13 +248,22 @@ run_multi_container_smoke() {
144248
run_fault_cleanup() {
145249
local phase
146250
local output
251+
local status
147252
for phase in after-create after-start after-kill; do
148-
output="$(sudo "$PWD/target/debug/a3s-oci" native-linux-fault-cleanup \
149-
--agent "$PWD/target/debug/a3s-oci-agent" \
150-
--bundle "$bundle" \
151-
--work-parent "$work_parent" \
152-
--fault-after "$phase")"
253+
if output="$(sudo "$PWD/target/debug/a3s-oci" native-linux-fault-cleanup \
254+
--agent "$PWD/target/debug/a3s-oci-agent" \
255+
--bundle "$bundle" \
256+
--work-parent "$work_parent" \
257+
--fault-after "$phase")"; then
258+
status=0
259+
else
260+
status=$?
261+
fi
153262
printf '%s\n' "$output"
263+
if ((status != 0)); then
264+
report_native_failure "$bundle/rootfs"
265+
return "$status"
266+
fi
154267
jq --exit-status --arg phase "$phase" \
155268
'.schema_version == "a3s.oci.native-linux-fault-cleanup.v2"
156269
and .platform == "linux" and .status == "available"
@@ -188,24 +301,15 @@ run_fault_cleanup() {
188301
done
189302
}
190303

191-
saved_kvm="/dev/a3s-oci-kvm-${run_id}-${run_attempt}"
192-
restore_kvm() {
193-
if [[ -d /dev/kvm ]]; then
194-
sudo rmdir /dev/kvm
195-
fi
196-
if [[ -e "$saved_kvm" || -L "$saved_kvm" ]]; then
197-
sudo mv "$saved_kvm" /dev/kvm
198-
fi
199-
}
200-
trap restore_kvm EXIT
201-
202304
if [[ -e /dev/kvm || -L /dev/kvm ]]; then
203305
sudo mv /dev/kvm "$saved_kvm"
306+
kvm_original_moved=true
204307
fi
205308

206309
run_smoke false
207310
run_multi_container_smoke false
208311
run_fault_cleanup
209312
sudo mkdir /dev/kvm
313+
kvm_test_directory_created=true
210314
run_smoke true
211315
run_multi_container_smoke true

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ jobs:
325325
mkdir -p "$container_rootfs"
326326
cp fixtures/utility-vm/config.json "$bundle_dir/config.json"
327327
tar -xzf "$rootfs_archive" -C "$container_rootfs"
328+
sudo chown -R 0:0 "$container_rootfs"
328329
329330
endpoint_baseline="$(
330331
find /private/tmp -maxdepth 1 \
@@ -468,6 +469,7 @@ jobs:
468469
mkdir -p "$bundle_b/rootfs"
469470
cp fixtures/utility-vm/config.json "$bundle_b/config.json"
470471
tar -xzf "$rootfs_archive" -C "$bundle_b/rootfs"
472+
sudo chown -R 0:0 "$bundle_b/rootfs"
471473
472474
endpoint_baseline="$(
473475
find /private/tmp -maxdepth 1 \

README.md

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,11 @@ cargo build -p a3s-oci-agent -p a3s-oci-cli
148148
demo_root="$(mktemp -d)"
149149
bundle="$demo_root/bundle"
150150
work_parent="$demo_root/work"
151-
mkdir -p "$bundle/rootfs/bin" "$work_parent"
151+
mkdir -p "$bundle/rootfs/bin" "$bundle/rootfs/proc" "$work_parent"
152152
cp fixtures/native-linux/config.json "$bundle/config.json"
153153
cp "$(command -v busybox)" "$bundle/rootfs/bin/busybox"
154154
ln -s busybox "$bundle/rootfs/bin/sh"
155+
sudo chown -R 0:0 "$bundle/rootfs"
155156

156157
sudo target/debug/a3s-oci native-linux-smoke \
157158
--agent "$PWD/target/debug/a3s-oci-agent" \
@@ -170,10 +171,11 @@ Create a second distinct bundle to run the multi-container gate:
170171

171172
```sh
172173
bundle_b="$demo_root/bundle-b"
173-
mkdir -p "$bundle_b/rootfs/bin"
174+
mkdir -p "$bundle_b/rootfs/bin" "$bundle_b/rootfs/proc"
174175
cp fixtures/native-linux/config.json "$bundle_b/config.json"
175176
cp "$(command -v busybox)" "$bundle_b/rootfs/bin/busybox"
176177
ln -s busybox "$bundle_b/rootfs/bin/sh"
178+
sudo chown -R 0:0 "$bundle_b/rootfs"
177179

178180
sudo target/debug/a3s-oci native-linux-multi-container-smoke \
179181
--agent "$PWD/target/debug/a3s-oci-agent" \
@@ -314,6 +316,7 @@ bundle="$rootfs_dir/rootfs/var/lib/a3s-oci-smoke/bundle"
314316
mkdir -p "$bundle/rootfs"
315317
cp fixtures/utility-vm/config.json "$bundle/config.json"
316318
tar -xzf "$rootfs_archive" -C "$bundle/rootfs"
319+
sudo chown -R 0:0 "$bundle/rootfs"
317320

318321
target/debug/a3s-oci oci-vm-smoke \
319322
--shim "$smoke_dir/a3s-oci-krun-shim" \
@@ -337,6 +340,7 @@ bundle_b="$rootfs_dir/rootfs/var/lib/a3s-oci-smoke/bundle-b"
337340
mkdir -p "$bundle_b/rootfs"
338341
cp fixtures/utility-vm/config.json "$bundle_b/config.json"
339342
tar -xzf "$rootfs_archive" -C "$bundle_b/rootfs"
343+
sudo chown -R 0:0 "$bundle_b/rootfs"
340344

341345
target/debug/a3s-oci oci-vm-multi-container-smoke \
342346
--shim "$smoke_dir/a3s-oci-krun-shim" \
@@ -425,17 +429,22 @@ state transition and replay remains container-scoped.
425429

426430
The current executor implements a reviewed bootstrap vertical slice:
427431

428-
- new UTS, mount, IPC, network, cgroup, and PID namespaces;
432+
- new UTS, mount, IPC, network, cgroup, PID, user, and time namespaces;
433+
- parent-authenticated rootful UID/GID mappings plus read-back verification,
434+
with normalized monotonic and boottime offsets applied before the first
435+
time-namespace child;
429436
- hostname and domain name configuration;
430437
- recursively private mount propagation and `pivot_root`;
431438
- ordered existing-target OCI mounts with bind/rbind and common VFS options;
432-
- PID-authenticated create/start barrier;
439+
- PID- and namespace-authenticated create/start barrier;
433440
- credentials, umask, `no_new_privileges`, `execve`, PID-reuse-safe pidfd
434441
signaling, exact normal-or-signal exit status, repeated wait, observation,
435442
and scoped cleanup.
436443

437-
Unimplemented OCI fields are rejected instead of ignored. User and time
438-
namespaces, namespace joins, complete mount semantics, cgroup resources,
444+
The supported user-namespace slice is rootful: it requires both UID and GID
445+
mappings, coverage for every configured process ID, and an `allow` setgroups
446+
policy. Unimplemented OCI fields are rejected instead of ignored. Rootless
447+
mapping policy, namespace joins, complete mount semantics, cgroup resources,
439448
capabilities, hooks, seccomp, full I/O, recovery, and the remaining SDK
440449
operations are still release gates.
441450

@@ -475,24 +484,37 @@ runtime prerequisite.
475484

476485
## Architecture
477486

478-
The control plane is platform-neutral. Platform-specific isolation and native
479-
libraries stay behind explicit driver and shim boundaries:
480-
481-
```mermaid
482-
flowchart TB
483-
consumers["Consumers<br/>A3S Box · a3s-oci CLI · planned containerd shim"]
484-
control["Platform-neutral control plane<br/>RuntimeClient → OciRuntimeService → HostRuntimeService<br/>validation · lifecycle · reconciliation"]
485-
state[("Durable state<br/>exact bundle · generations · operation journal")]
486-
selection{"RuntimeDriver<br/>explicit isolation selection"}
487-
native["Native Linux<br/>NativeLinuxDriver · experimental opt-in"]
488-
utility["Utility VM host · qualification path<br/>isolated a3s-oci-krun-shim → libkrun<br/>KVM · HVF · WHPX"]
489-
agent["A3S Linux guest<br/>authenticated AF_VSOCK → a3s-oci-agent"]
490-
executor["Shared LinuxExecutor<br/>namespaces · mounts · PID 1 · pidfd process control"]
491-
492-
consumers --> control --> selection
493-
control <--> state
494-
selection --> native --> executor
495-
selection --> utility --> agent --> executor
487+
The platform-neutral control plane is independent of the host isolation
488+
mechanism. Native libraries and Linux-specific execution stay behind explicit
489+
driver, shim, and guest-agent boundaries:
490+
491+
```text
492+
A3S Box / a3s-oci CLI / Rust SDK consumers
493+
494+
RuntimeClient / OciRuntimeService
495+
496+
in-process call or bounded local IPC
497+
498+
OCI validation and lifecycle
499+
500+
HostRuntimeService
501+
exact bundle · generations · journal · reconciliation
502+
503+
RuntimeDriver
504+
┌─────────┴──────────┐
505+
│ │
506+
Native Linux host Utility VM qualification
507+
NativeLinuxDriver a3s-oci-krun-shim → libkrun
508+
│ KVM · HVF · WHPX
509+
│ │
510+
│ authenticated guest protocol
511+
│ │
512+
│ a3s-oci-agent
513+
│ │
514+
└─────────┬──────────┘
515+
516+
LinuxExecutor
517+
namespaces · mounts · PID 1 · pidfds · wait · cleanup
496518
```
497519

498520
The same `LinuxExecutor` is called directly on Linux and through the guest

0 commit comments

Comments
 (0)