Skip to content

Commit 893ef07

Browse files
committed
sandlock-oci: mount host procfs and ro binds read-only so the sandbox gets a full /proc
Signed-off-by: Cong Wang <cwang@multikernel.io>
1 parent 7ad3c38 commit 893ef07

1 file changed

Lines changed: 46 additions & 10 deletions

File tree

crates/sandlock-oci/src/policy.rs

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ pub struct OciPolicy {
3333
/// Explicit bind mounts: (dest_inside_rootfs, host_source_path).
3434
pub fs_mount: Vec<(PathBuf, PathBuf)>,
3535

36+
/// Destinations (subset of `fs_mount`) mounted read-only: the host procfs
37+
/// mount and `ro` bind mounts. Writes are denied even with a writable root.
38+
pub fs_mount_ro: Vec<PathBuf>,
39+
3640
/// Initial working directory (relative to rootfs if set).
3741
pub cwd: Option<PathBuf>,
3842

@@ -81,6 +85,7 @@ impl OciPolicy {
8185
let mut fs_read = Vec::new();
8286
let mut fs_write = Vec::new();
8387
let mut fs_mount = Vec::new();
88+
let mut fs_mount_ro = Vec::new();
8489
let mut scratch_dirs = Vec::new();
8590

8691
if rootfs.is_some() {
@@ -100,7 +105,7 @@ impl OciPolicy {
100105
if let Some(mounts) = spec.mounts() {
101106
map_mounts(
102107
mounts, bundle, &rootfs, id,
103-
&mut fs_mount, &mut fs_read, &mut fs_write, &mut scratch_dirs,
108+
&mut fs_mount, &mut fs_mount_ro, &mut fs_read, &mut fs_write, &mut scratch_dirs,
104109
);
105110
}
106111

@@ -192,6 +197,7 @@ impl OciPolicy {
192197
fs_read,
193198
fs_write,
194199
fs_mount,
200+
fs_mount_ro,
195201
cwd,
196202
env,
197203
max_memory,
@@ -224,7 +230,11 @@ impl OciPolicy {
224230
builder = builder.fs_write(path);
225231
}
226232
for (virt, host) in &self.fs_mount {
227-
builder = builder.fs_mount(virt, host);
233+
if self.fs_mount_ro.iter().any(|d| d == virt) {
234+
builder = builder.fs_mount_ro(virt, host);
235+
} else {
236+
builder = builder.fs_mount(virt, host);
237+
}
228238
}
229239

230240
if let Some(ref cwd) = self.cwd {
@@ -330,16 +340,20 @@ fn rootfs_path(spec: &Spec, bundle: &Path) -> Result<Option<PathBuf>> {
330340
/// - **tmpfs** writable scratch (`/tmp`, `/run`, `/dev/shm`, …): backed by a
331341
/// host directory under the sandbox state dir and bind-mounted read-write,
332342
/// so it works on a read-only root and stays isolated from the rootfs.
333-
/// - **tmpfs at `/dev`, proc, sysfs**: passed through read-only so sandlock's
334-
/// `/dev` interception and `/proc` virtualization service them; an empty
335-
/// backing dir would shadow `/dev/null`, `/proc/*`, etc.
343+
/// - **proc**: `fs_mount(dest, /proc)` — the host procfs, so the sandbox gets a
344+
/// full `/proc` like a container; the seccomp `/proc` handler synthesizes the
345+
/// limit-aware files on top and blocks foreign PIDs / sensitive paths.
346+
/// - **tmpfs at `/dev`, sysfs**: passed through read-only so sandlock's `/dev`
347+
/// interception services them; an empty backing dir would shadow `/dev/null`,
348+
/// `/sys/*`, etc.
336349
/// - **devpts/mqueue/cgroup**: skipped (no safe namespace-less equivalent).
337350
fn map_mounts(
338351
mounts: &[oci_spec::runtime::Mount],
339352
bundle: &Path,
340353
rootfs: &Option<PathBuf>,
341354
id: &str,
342355
fs_mount: &mut Vec<(PathBuf, PathBuf)>,
356+
fs_mount_ro: &mut Vec<PathBuf>,
343357
fs_read: &mut Vec<PathBuf>,
344358
fs_write: &mut Vec<PathBuf>,
345359
scratch_dirs: &mut Vec<PathBuf>,
@@ -349,9 +363,22 @@ fn map_mounts(
349363
let mount_type = mount.typ().as_deref().unwrap_or("bind");
350364

351365
match mount_type {
352-
// Kernel interfaces: pass the host's through read-only. sandlock
353-
// virtualizes /proc and intercepts /dev/{null,urandom,random,…}.
354-
"proc" => fs_read.push(PathBuf::from("/proc")),
366+
// /proc: mount the host procfs READ-ONLY at the requested
367+
// destination so the sandbox gets a full /proc (version, stat,
368+
// self/*, <pid>/*, the directory listing, …) like a real container;
369+
// an empty rootfs /proc would otherwise leave it bare. The seccomp
370+
// /proc handler runs before this chroot mount, so it still (a)
371+
// synthesizes the cgroup/limit-aware files (meminfo, loadavg,
372+
// mounts, net/*) on top, and (b) blocks foreign PIDs and sensitive
373+
// paths. Read-only is essential: a writable host procfs would let
374+
// the sandbox write host-global controls like /proc/sys/* and
375+
// /proc/sysrq-trigger (a host-escape vector).
376+
"proc" => {
377+
fs_mount.push((dest.to_path_buf(), PathBuf::from("/proc")));
378+
fs_mount_ro.push(dest.to_path_buf());
379+
}
380+
// sysfs: pass the host's through read-only; sandlock's sensitive-path
381+
// filter still gates it.
355382
"sysfs" => fs_read.push(PathBuf::from("/sys")),
356383

357384
"tmpfs" => {
@@ -399,6 +426,7 @@ fn map_mounts(
399426
fs_mount.push((dest.to_path_buf(), src));
400427
if read_only {
401428
fs_read.push(dest.to_path_buf());
429+
fs_mount_ro.push(dest.to_path_buf());
402430
} else {
403431
fs_write.push(dest.to_path_buf());
404432
}
@@ -644,8 +672,16 @@ mod tests {
644672
assert!(policy.scratch_dirs.iter().any(|d| d.ends_with("ctr1/tmpfs/tmp")));
645673
assert!(policy.fs_write.contains(&PathBuf::from("/tmp")));
646674

647-
// /proc and /dev are passed through read-only, not emulated.
648-
assert!(policy.fs_read.contains(&PathBuf::from("/proc")));
675+
// /proc mounts the host procfs so the sandbox gets a full /proc; the
676+
// seccomp /proc handler synthesizes the limit-aware files on top.
677+
assert!(policy
678+
.fs_mount
679+
.iter()
680+
.any(|(d, h)| d == Path::new("/proc") && h == Path::new("/proc")));
681+
// ...and it is read-only, so the sandbox can't write host-global
682+
// controls like /proc/sys/* through the mount.
683+
assert!(policy.fs_mount_ro.contains(&PathBuf::from("/proc")));
684+
// /dev is passed through read-only, not emulated.
649685
assert!(policy.fs_read.contains(&PathBuf::from("/dev")));
650686
assert!(!policy.fs_mount.iter().any(|(d, _)| d == Path::new("/dev")));
651687
}

0 commit comments

Comments
 (0)