1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
//! L2-03 — private tmpfs workspace isolation.
//!
//! When `CELLOS_SUBPROCESS_UNSHARE=mnt` (or the default that includes `CLONE_NEWNS`) and the
//! spec sets `run.workingDirectory`, the supervisor mounts a fresh tmpfs over that path inside
//! the child's private mount namespace. Two sequential runs see a clean, empty workspace each
//! time; writes from run A are not visible to run B and do not appear on the host after exit.
//!
//! Requires Linux and root or a user namespace with `CAP_SYS_ADMIN` (or `newuidmap`/`newgidmap`).
//! On CI (ubuntu-latest) the runner has sufficient privileges.
#![cfg(target_os = "linux")]
mod linux {
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
fn supervisor_exe() -> PathBuf {
if let Some(p) = std::env::var_os("CARGO_BIN_EXE_cellos_supervisor") {
return PathBuf::from(p);
}
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("workspace root");
root.join("target/debug/cellos-supervisor")
}
fn write_spec(dir: &Path, spec_id: &str, argv: &[&str], working_dir: &str) -> PathBuf {
let argv_json: Vec<String> = argv.iter().map(|s| format!("\"{s}\"")).collect();
let json = format!(
r#"{{"apiVersion":"cellos.io/v1","kind":"ExecutionCell","spec":{{"id":"{spec_id}","authority":{{}},"lifetime":{{"ttlSeconds":60}},"run":{{"argv":[{argv}],"workingDirectory":"{wd}"}}}}}}"#,
spec_id = spec_id,
argv = argv_json.join(","),
wd = working_dir,
);
let path = dir.join(format!("{spec_id}.json"));
File::create(&path)
.and_then(|mut f| f.write_all(json.as_bytes()))
.expect("write spec");
path
}
/// Returns true when the running process can create a new mount namespace.
///
/// `CELLOS_SUBPROCESS_UNSHARE=mnt` calls `unshare(CLONE_NEWNS)` which requires
/// `CAP_SYS_ADMIN`. Guard each test with this probe so they skip on
/// github-hosted runners that lack the capability.
fn unshare_mnt_available() -> bool {
Command::new("unshare")
.args(["-m", "/bin/true"])
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn run_supervisor(spec: &Path, workspace: &str) -> std::process::ExitStatus {
Command::new(supervisor_exe())
.env("CELLOS_DEPLOYMENT_PROFILE", "portable")
.env("CELL_OS_USE_NOOP_SINK", "1")
.env("CELLOS_CELL_BACKEND", "stub")
.env("CELLOS_SUBPROCESS_UNSHARE", "mnt")
.env(
"CELLOS_RUN_ARGV0_ALLOW_PREFIXES",
"/usr/bin,/bin,/usr/local/bin,/bin/sh,/usr/bin/sh",
)
.arg(spec)
.current_dir(workspace)
.status()
.expect("spawn supervisor")
}
/// Run A writes a sentinel inside the declared workspace; run B asserts it is absent
/// (fresh tmpfs each time).
#[test]
fn each_run_gets_fresh_workspace() {
if !unshare_mnt_available() {
return; // CAP_SYS_ADMIN unavailable (e.g. github-hosted runner)
}
let dir = tempfile::tempdir().expect("tempdir");
let ws = dir.path().to_str().expect("UTF-8 path").to_string();
let host_sentinel = dir.path().join("sentinel.txt");
// Run A: write a sentinel into the workspace mount.
let spec_a = write_spec(
dir.path(),
"ws-run-a",
&[
"/bin/sh",
"-c",
"echo run-a-was-here > sentinel.txt && test -f sentinel.txt",
],
&ws,
);
let status_a = run_supervisor(&spec_a, &ws);
assert!(status_a.success(), "run A should succeed: {status_a:?}");
// The sentinel must NOT exist on the host after run A exits: the tmpfs mount should
// disappear with the private mount namespace, leaving the host workspace clean.
assert!(
!host_sentinel.exists(),
"sentinel leaked to host workspace — tmpfs mount did not isolate run A's writes"
);
// Run B: assert the sentinel is absent because its workspace tmpfs is fresh.
let spec_b = write_spec(
dir.path(),
"ws-run-b",
&["/bin/sh", "-c", "! test -f sentinel.txt"],
&ws,
);
let status_b = run_supervisor(&spec_b, &ws);
assert!(
status_b.success(),
"run B should see a clean workspace with no sentinel from run A: {status_b:?}"
);
}
/// A cell's working directory starts empty — no files from the host bleed through.
#[test]
fn workspace_is_empty_on_start() {
if !unshare_mnt_available() {
return; // CAP_SYS_ADMIN unavailable (e.g. github-hosted runner)
}
let dir = tempfile::tempdir().expect("tempdir");
// Create a file in the tempdir on the host so it would be visible without isolation.
std::fs::write(dir.path().join("host-file.txt"), b"host content").expect("write");
let ws = dir.path().to_str().expect("UTF-8 path").to_string();
// The cell's tmpfs overlays the working directory, so host-file.txt is hidden.
let spec = write_spec(
dir.path(),
"ws-empty",
&["/bin/sh", "-c", "! test -f host-file.txt"],
&ws,
);
let status = run_supervisor(&spec, &ws);
assert!(
status.success(),
"workspace should be empty (tmpfs hides host files): {status:?}"
);
// Verify the host file is still there after the cell exits.
assert!(
dir.path().join("host-file.txt").exists(),
"host file should be unchanged after cell exits"
);
}
}