Skip to content

Commit cab2ae3

Browse files
committed
sandlock-core: resolve /proc/self in chroot chdir to the workload pid
Signed-off-by: Cong Wang <cwang@multikernel.io>
1 parent e176246 commit cab2ae3

3 files changed

Lines changed: 88 additions & 1 deletion

File tree

crates/sandlock-core/src/chroot/dispatch.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1640,7 +1640,7 @@ pub(crate) async fn handle_chroot_chdir(
16401640

16411641
// Build the full virtual path from AT_FDCWD + path.
16421642
let was_absolute = Path::new(&path).is_absolute();
1643-
let full_path = if was_absolute {
1643+
let virtual_path = if was_absolute {
16441644
path
16451645
} else {
16461646
match std::fs::read_link(format!("/proc/{}/cwd", notif.pid)) {
@@ -1651,6 +1651,17 @@ pub(crate) async fn handle_chroot_chdir(
16511651
Err(_) => return NotifAction::Errno(libc::EACCES),
16521652
}
16531653
};
1654+
// Canonicalize /proc/self and /proc/thread-self to the child's own PID,
1655+
// exactly as the open path does (build_virtual_path). The on-behalf
1656+
// openat2 below runs in the supervisor task, so the kernel would resolve a
1657+
// literal "self" against the supervisor and point the child's cwd at a
1658+
// non-sandbox process's /proc/<pid> dir. Rewriting to the numeric child PID
1659+
// makes "self" resolve to the real caller and keeps every /proc spelling
1660+
// subject to the same numeric per-PID filter. For a same-path /proc mount
1661+
// this also lets the native-chdir fast path below fire: the resolved fd's
1662+
// host path then equals full_path, so the kernel re-runs the original
1663+
// "/proc/self" in the child's context (where it resolves correctly).
1664+
let full_path = canon_proc_self(&virtual_path, notif.pid);
16541665

16551666
// Open directly via openat2(RESOLVE_IN_ROOT), routing to mount target if applicable.
16561667
let confined = confine(&full_path);

crates/sandlock-core/tests/integration/test_chroot.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,50 @@ async fn test_chroot_chdir_proc_readonly_buffer() {
234234
cleanup_rootfs(&rootfs);
235235
}
236236

237+
/// chdir("/proc/self") must land on the CHILD's own /proc/<pid> dir, not the
238+
/// supervisor's. sandlock services /proc via an on-behalf openat2 in the
239+
/// supervisor task, so a literal "self" would resolve against the supervisor.
240+
/// The chdir handler canonicalizes /proc/self to the child's numeric PID (same
241+
/// as the open path), so getcwd() after the chdir renders the child's own
242+
/// /proc/<pid> and its basename matches the child's getpid(). Before the fix the
243+
/// cwd pointed at the supervisor's /proc/<pid> and the basename mismatched.
244+
#[tokio::test]
245+
async fn test_chroot_chdir_proc_self_resolves_to_child() {
246+
let rootfs = build_test_rootfs("chdir-proc-self");
247+
248+
let policy = minimal_exec_policy(&rootfs)
249+
.fs_mount("/proc", "/proc")
250+
.build()
251+
.unwrap();
252+
253+
let result = policy
254+
.clone()
255+
.with_name("test")
256+
.run(&["rootfs-helper", "chdir-self", "/proc/self"])
257+
.await;
258+
match result {
259+
Ok(r) => {
260+
assert!(
261+
r.success(),
262+
"chdir(/proc/self) should succeed, stderr: {}",
263+
r.stderr_str().unwrap_or("")
264+
);
265+
// The helper checks getcwd()'s basename == its own getpid(); "OK"
266+
// means "self" resolved to the child's own /proc/<pid>, not the
267+
// supervisor's.
268+
let out = r.stdout_str().unwrap_or("").trim().to_string();
269+
assert!(
270+
out.starts_with("OK /proc/"),
271+
"self must resolve to the child's own /proc/<pid>, got: {:?}",
272+
out
273+
);
274+
}
275+
Err(e) => eprintln!("Chroot test skipped: {}", e),
276+
}
277+
278+
cleanup_rootfs(&rootfs);
279+
}
280+
237281
/// A dirfd-relative /proc read under a chroot with NO /proc mount must still
238282
/// hit the synthesis shim. Regression: resolve_open_target took the proc
239283
/// dirfd's real symlink target (`<chroot>/proc`) as the base, so

tests/rootfs-helper.c

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,37 @@ static int cmd_chdir(int argc, char **argv) {
611611
return 0;
612612
}
613613

614+
/* ── chdir-self (chdir into a /proc/self path, confirm it is OUR dir) ─ */
615+
/*
616+
* chdir(argv[0]) (e.g. "/proc/self"), then getcwd() and check its final
617+
* component equals our own getpid(). This proves "self" resolved to the CHILD,
618+
* not the supervisor that services /proc on-behalf: if it mis-resolved, the cwd
619+
* would render as the supervisor's /proc/<pid> and the basename would not match
620+
* our pid. Robust to the exec-via-/proc/self/fd/N comm artifact and to pid
621+
* namespaces (getcwd and getpid share the child's own view). Prints "OK <cwd>"
622+
* on match, "MISMATCH ..." otherwise (returns 0 either way so the caller can
623+
* assert on stdout).
624+
*/
625+
static int cmd_chdir_self(int argc, char **argv) {
626+
if (argc < 1) { fprintf(stderr, "chdir-self: missing operand\n"); return 1; }
627+
if (chdir(argv[0]) != 0) {
628+
fprintf(stderr, "chdir-self: chdir %s: %s\n", argv[0], strerror(errno));
629+
return 1;
630+
}
631+
char buf[4096];
632+
if (!getcwd(buf, sizeof(buf))) { perror("chdir-self: getcwd"); return 1; }
633+
const char *slash = strrchr(buf, '/');
634+
const char *base = slash ? slash + 1 : buf;
635+
char expect[32];
636+
snprintf(expect, sizeof(expect), "%d", getpid());
637+
if (strcmp(base, expect) == 0) {
638+
printf("OK %s\n", buf);
639+
} else {
640+
printf("MISMATCH cwd=%s pid=%s\n", buf, expect);
641+
}
642+
return 0;
643+
}
644+
614645
/* ── proc-dirfd (non-standard: read /proc/<name> via a proc dirfd) ─ */
615646
/*
616647
* Opens /proc as a directory, then openat()s <name> RELATIVE to that fd and
@@ -660,6 +691,7 @@ static int cmd_write_fd_link(int argc, char **argv) {
660691

661692
static int dispatch(const char *cmd, int argc, char **argv) {
662693
if (strcmp(cmd, "chdir") == 0) return cmd_chdir(argc, argv);
694+
if (strcmp(cmd, "chdir-self") == 0) return cmd_chdir_self(argc, argv);
663695
if (strcmp(cmd, "proc-dirfd") == 0) return cmd_proc_dirfd(argc, argv);
664696
if (strcmp(cmd, "write-fd-link") == 0) return cmd_write_fd_link(argc, argv);
665697
if (strcmp(cmd, "echo") == 0) return cmd_echo(argc, argv);

0 commit comments

Comments
 (0)