diff --git a/codex-rs/linux-sandbox/src/daemon_mounts.rs b/codex-rs/linux-sandbox/src/daemon_mounts.rs index 713688b59c..fbe724c7f0 100644 --- a/codex-rs/linux-sandbox/src/daemon_mounts.rs +++ b/codex-rs/linux-sandbox/src/daemon_mounts.rs @@ -39,14 +39,19 @@ fn check_mounts( let [_, _, mount_device, root, destination] = fields.as_slice() else { return Err(invalid()); }; - let root = mount_path(root)?; let destination = mount_path(destination)?; - if *mount_device == device.as_bytes() + // Only roots on the socket filesystem can identify aliases. Other + // filesystems can use non-path roots such as nsfs `mnt:[inode]`, but + // their destinations still matter for nested-mount checks. + let root = (*mount_device == device.as_bytes()) + .then(|| mount_path(root)) + .transpose()?; + if let Some(root) = &root && let Ok(relative) = directory.strip_prefix(&destination) { locations.insert(root.join(relative)); } - mounts.push((*mount_device, root, destination)); + mounts.push((root, destination)); } // Overmounts can leave hidden entries in mountinfo. Require every possible // containing mount to agree instead of guessing which root is visible. @@ -54,10 +59,10 @@ fn check_mounts( return Err(invalid()); } let location = locations.into_iter().next().ok_or_else(invalid)?; - for (mount_device, root, destination) in &mounts { + for (root, destination) in &mounts { // Nested mounts can introduce another filesystem (or an individual socket) under the mask. let nested = destination != directory && destination.starts_with(directory); - let alias = if *mount_device == device.as_bytes() { + let alias = if let Some(root) = root { if let Ok(relative) = location.strip_prefix(root) { Some(destination.join(relative)) } else if root.starts_with(&location) { diff --git a/codex-rs/linux-sandbox/src/daemon_mounts_tests.rs b/codex-rs/linux-sandbox/src/daemon_mounts_tests.rs index 04e0fe5d25..de0f3c208b 100644 --- a/codex-rs/linux-sandbox/src/daemon_mounts_tests.rs +++ b/codex-rs/linux-sandbox/src/daemon_mounts_tests.rs @@ -29,6 +29,44 @@ fn rejects_only_mounts_that_compromise_the_directory(root: &str, destination: &s ); } +#[test_case("0:2", "mnt:[4026532835]", "/run/snapd/ns/example.mnt", Ok(()); "unrelated mount namespace")] +#[test_case("0:2", "net:[4026531840]", "/run/netns/example", Ok(()); "unrelated network namespace")] +#[test_case("0:2", "mnt:[4026532835]", "/tmp/codex-daemon-1000/ns", Err(io::ErrorKind::PermissionDenied); "nested namespace mount")] +#[test_case("0:1", "mnt:[4026532835]", "/run/snapd/ns/example.mnt", Err(io::ErrorKind::Other); "non-path root on socket filesystem")] +#[test_case("0:2", "mnt:[4026532835]", "relative/ns", Err(io::ErrorKind::Other); "relative destination")] +#[test_case("0:2", "mnt:[4026532835]", "/run/snapd/ns/\\invalid", Err(io::ErrorKind::Other); "invalid destination escape")] +fn validates_namespace_mounts_by_device_and_destination( + device: &str, + root: &str, + destination: &str, + expected: Result<(), io::ErrorKind>, +) { + let mounts = format!( + "1 0 0:1 / / rw - ext4 disk rw\n2 1 {device} {root} {destination} rw - nsfs nsfs rw\n" + ); + assert_eq!( + check_mounts( + Path::new("/tmp/codex-daemon-1000"), + "0:1", + mounts.as_bytes() + ) + .map_err(|error| error.kind()), + expected + ); +} + +#[test] +fn unrelated_namespace_mount_does_not_hide_a_socket_alias() { + let mounts = b"1 0 0:1 / / rw - ext4 disk rw\n\ + 2 1 0:2 net:[4026531840] /run/netns/example rw - nsfs nsfs rw\n\ + 3 1 0:1 /tmp/codex-daemon-1000 /alias rw - ext4 disk rw\n"; + assert_eq!( + check_mounts(Path::new("/tmp/codex-daemon-1000"), "0:1", mounts) + .map_err(|error| error.kind()), + Err(io::ErrorKind::PermissionDenied) + ); +} + #[test] fn rejects_alias_when_tmp_is_itself_a_bind_mount() { let mounts = b"1 0 0:1 / / rw - ext4 disk rw\n2 1 0:1 /backing/tmp /tmp rw - ext4 disk rw\n";