Skip to content

Commit c48e79a

Browse files
author
Roy Lin
committed
feat(executor): join existing Linux namespaces
1 parent 7854e97 commit c48e79a

25 files changed

Lines changed: 1558 additions & 222 deletions

File tree

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ run_multi_container_smoke() {
189189
fi
190190
jq --exit-status \
191191
--argjson expected "$expected_kvm_present" \
192-
'.schema_version == "a3s.oci.native-linux-multi-container-smoke.v2"
192+
'.schema_version == "a3s.oci.native-linux-multi-container-smoke.v3"
193193
and .platform == "linux" and .status == "available"
194194
and .kvm_device_present == $expected
195195
and .bundles_loaded
@@ -235,6 +235,14 @@ run_multi_container_smoke() {
235235
and .lifecycle.wait_b_replayed
236236
and .lifecycle.delete_b_replayed
237237
and .lifecycle.b_missing_after_delete
238+
and (.namespace_join.donor_pid > 0)
239+
and .namespace_join.wrong_type_rejected_before_state
240+
and .namespace_join.joined_non_mount_namespaces
241+
and .namespace_join.joined_pid_time_workload_verified
242+
and .namespace_join.joined_mount_namespace
243+
and .namespace_join.retained_rootfs_verified
244+
and .namespace_join.donor_unchanged_after_joins
245+
and .namespace_join.all_state_removed
238246
and .markers_removed
239247
and .executor_runtime_clean
240248
and .session_root_clean

.github/workflows/ci.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ jobs:
496496
if [[ "$support" == "1" ]]; then
497497
test "$status" -eq 0
498498
jq --exit-status \
499-
'.schema_version == "a3s.oci.oci-vm-multi-container-smoke.v2"
499+
'.schema_version == "a3s.oci.oci-vm-multi-container-smoke.v3"
500500
and .platform == "macos" and .status == "available"
501501
and .bundles_loaded
502502
and .lifecycle.distinct_bundle_directories
@@ -541,6 +541,14 @@ jobs:
541541
and .lifecycle.wait_b_replayed
542542
and .lifecycle.delete_b_replayed
543543
and .lifecycle.b_missing_after_delete
544+
and (.namespace_join.donor_pid > 0)
545+
and .namespace_join.wrong_type_rejected_before_state
546+
and .namespace_join.joined_non_mount_namespaces
547+
and .namespace_join.joined_pid_time_workload_verified
548+
and .namespace_join.joined_mount_namespace
549+
and .namespace_join.retained_rootfs_verified
550+
and .namespace_join.donor_unchanged_after_joins
551+
and .namespace_join.all_state_removed
544552
and .markers_removed and .guest_runtime_clean
545553
and .bridge.platform == "macos"
546554
and .bridge.status == "available"
@@ -563,7 +571,7 @@ jobs:
563571
else
564572
test "$status" -eq 2
565573
jq --exit-status \
566-
'.schema_version == "a3s.oci.oci-vm-multi-container-smoke.v2"
574+
'.schema_version == "a3s.oci.oci-vm-multi-container-smoke.v3"
567575
and .platform == "macos" and .status == "unavailable"
568576
and .bundles_loaded
569577
and .bridge.platform == "macos"

ROADMAP.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ Completed:
7070
- real native Linux create/state/start/kill/wait/delete SDK evidence on x86_64
7171
and aarch64, including exact repeated SIGKILL status and bounded running
7272
wait, repeated with `/dev/kvm` absent and present but unusable;
73+
- type-checked joins for existing UTS, mount, IPC, network, cgroup, PID, user,
74+
and time namespaces, including retained rootfs execution after a mount join,
75+
three-pass user-namespace permission recovery, and shared native Linux/macOS
76+
utility-VM lifecycle evidence;
7377
- real native Linux no-delete cleanup after create, start, and kill on x86_64
7478
and aarch64, including init-PID reap and executor, durable-state, marker, and
7579
session-root removal;
@@ -305,7 +309,11 @@ then may HVF become `experimental`.
305309
UID/GID mappings through the authenticated parent, apply and verify
306310
monotonic/boottime offsets before the first child, and prove the path through
307311
native Linux and the macOS utility VM.
308-
- [ ] Join existing namespaces.
312+
- [x] Open and type-check all existing namespace descriptors before mutation,
313+
join non-user namespaces around the user-namespace capability transition,
314+
preserve PID/time next-child semantics, and prove UTS, mount, IPC, network,
315+
cgroup, PID, user, and time joins through native Linux and the macOS
316+
utility-VM path.
309317
- [ ] Mount-target creation, rootfs propagation overrides, idmapped and
310318
recursive-attribute mounts, masked paths, read-only paths, and read-only
311319
rootfs.

crates/agent/src/executor/init.rs

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::ffi::{CString, OsStr};
2+
use std::fs::File;
23
use std::io::{self, Read};
34
use std::mem::MaybeUninit;
45
use std::os::linux::net::SocketAddrExt;
@@ -57,7 +58,7 @@ fn run_container_init(
5758
format!("failed to connect abstract prepared init control socket: {error}"),
5859
)
5960
})?;
60-
let (plan, canonical_bundle, rootfs) =
61+
let (plan, canonical_bundle, rootfs, rootfs_file) =
6162
match prepare_container_init(config_snapshot, bundle_directory) {
6263
Ok(prepared) => prepared,
6364
Err(error) => return reject_before_ready(&mut control, error),
@@ -66,7 +67,7 @@ fn run_container_init(
6667
return reject_before_ready(&mut control, error);
6768
}
6869
if plan.namespaces.requires_child_process() {
69-
return run_namespaced_init(&plan, &canonical_bundle, &rootfs, control);
70+
return run_namespaced_init(&plan, &canonical_bundle, &rootfs, &rootfs_file, control);
7071
}
7172
if let Err(error) = prepare_create_environment(&plan, &canonical_bundle, &rootfs) {
7273
return reject_before_ready(&mut control, error);
@@ -75,10 +76,10 @@ fn run_container_init(
7576
// PID namespace that changes the runtime-visible process.
7677
let pid = unsafe { libc::getpid() };
7778
write_ready(&mut control, pid)?;
78-
wait_for_start_and_exec(&plan, &rootfs, control)
79+
wait_for_start_and_exec(&plan, &rootfs_file, control)
7980
}
8081

81-
fn wait_for_start_and_exec(plan: &InitPlan, rootfs: &Path, mut control: UnixStream) -> Result<()> {
82+
fn wait_for_start_and_exec(plan: &InitPlan, rootfs: &File, mut control: UnixStream) -> Result<()> {
8283
let mut start = [0_u8; 1];
8384
control.read_exact(&mut start).map_err(|error| {
8485
init_error(
@@ -100,6 +101,7 @@ fn run_namespaced_init(
100101
plan: &InitPlan,
101102
bundle_directory: &Path,
102103
rootfs: &Path,
104+
rootfs_file: &File,
103105
mut control: UnixStream,
104106
) -> Result<()> {
105107
match pid::fork_namespaced_init() {
@@ -113,7 +115,7 @@ fn run_namespaced_init(
113115
return reject_before_ready(&mut control, error);
114116
}
115117
write_ready(&mut control, runtime_pid)?;
116-
wait_for_start_and_exec(plan, rootfs, control)
118+
wait_for_start_and_exec(plan, rootfs_file, control)
117119
}
118120
Err(error) => reject_before_ready(&mut control, error),
119121
}
@@ -133,7 +135,7 @@ fn reject_before_ready(control: &mut UnixStream, error: Error) -> Result<()> {
133135
fn prepare_container_init(
134136
config_snapshot: PathBuf,
135137
bundle_directory: PathBuf,
136-
) -> Result<(InitPlan, PathBuf, PathBuf)> {
138+
) -> Result<(InitPlan, PathBuf, PathBuf, File)> {
137139
let config_json = read_bounded_config(&config_snapshot)?;
138140
let bundle = OciBundle::from_json(bundle_directory, config_json)?;
139141
let plan = InitPlan::from_bundle(&bundle, &null_io())?;
@@ -164,7 +166,34 @@ fn prepare_container_init(
164166
),
165167
));
166168
}
167-
Ok((plan, canonical_bundle, rootfs))
169+
let rootfs_file = File::open(&rootfs).map_err(|error| {
170+
init_error(
171+
ErrorCode::InvalidArgument,
172+
format!(
173+
"failed to retain the container rootfs {} before namespace entry: {error}",
174+
rootfs.display()
175+
),
176+
)
177+
})?;
178+
if !rootfs_file
179+
.metadata()
180+
.map_err(|error| {
181+
init_error(
182+
ErrorCode::InvalidArgument,
183+
format!(
184+
"failed to inspect retained container rootfs {}: {error}",
185+
rootfs.display()
186+
),
187+
)
188+
})?
189+
.is_dir()
190+
{
191+
return Err(init_error(
192+
ErrorCode::InvalidArgument,
193+
format!("container rootfs is not a directory: {}", rootfs.display()),
194+
));
195+
}
196+
Ok((plan, canonical_bundle, rootfs, rootfs_file))
168197
}
169198

170199
fn prepare_create_environment(
@@ -173,10 +202,10 @@ fn prepare_create_environment(
173202
rootfs: &Path,
174203
) -> Result<()> {
175204
if let Some(hostname) = &plan.hostname {
176-
if !plan.namespaces.new_uts() {
205+
if !plan.namespaces.has_uts() {
177206
return Err(init_error(
178207
ErrorCode::FailedPrecondition,
179-
"refusing to change hostname outside a new UTS namespace",
208+
"refusing to change hostname outside a configured UTS namespace",
180209
));
181210
}
182211
// SAFETY: the byte slice remains live for the call and its exact
@@ -186,10 +215,10 @@ fn prepare_create_environment(
186215
}
187216
}
188217
if let Some(domainname) = &plan.domainname {
189-
if !plan.namespaces.new_uts() {
218+
if !plan.namespaces.has_uts() {
190219
return Err(init_error(
191220
ErrorCode::FailedPrecondition,
192-
"refusing to change domainname outside a new UTS namespace",
221+
"refusing to change domainname outside a configured UTS namespace",
193222
));
194223
}
195224
// SAFETY: the byte slice remains live for the call and its exact
@@ -299,7 +328,7 @@ fn read_bounded_config(path: &Path) -> Result<String> {
299328
})
300329
}
301330

302-
fn enter_rootfs_and_exec(plan: &InitPlan, rootfs: &Path) -> Result<()> {
331+
fn enter_rootfs_and_exec(plan: &InitPlan, rootfs: &File) -> Result<()> {
303332
let cwd = CString::new(plan.cwd.as_bytes()).map_err(|error| {
304333
init_error(
305334
ErrorCode::InvalidArgument,

0 commit comments

Comments
 (0)