From bbbda446a0862fd4fe71c38f073a3d59bbfcc502 Mon Sep 17 00:00:00 2001 From: pash-openai Date: Fri, 18 Sep 2026 19:01:52 +0000 Subject: [PATCH] Allow unrelated namespace mounts in Linux sandbox socket checks (#46535) Namespace mounts can have roots such as `mnt:[inode]` or `net:[inode]` that are not filesystem paths. Treating every mount root as a path rejects these unrelated mounts and prevents sandbox startup. Parse mount roots as paths only for the daemon socket's filesystem. Continue validating every mount destination and checking mount ancestry, nested mounts, and socket aliases. Add regression cases for namespace mounts, invalid destinations, and socket aliases, both with and without a mount ID. Extend the socket-isolation integration fixture with an unrelated network namespace mount to exercise successful startup and rejection of a real socket alias. GitOrigin-RevId: 9a1d2a69e5bd3f8d09b1de7cf998ee6d9c6bb980 --- codex-rs/linux-sandbox/src/daemon_mounts.rs | 15 +++++--- .../linux-sandbox/src/daemon_mounts_tests.rs | 38 +++++++++++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) 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";