mirror of
https://github.com/openai/codex.git
synced 2026-09-17 12:23:33 +00:00
Hide WSLg's duplicate root in restricted Linux sandboxes (#45837)
## Why WSLg's duplicate distro root can expose filesystem contents outside the sandbox's path masks. ## What changed - Detect the duplicate root using filesystem identity, with mount metadata as a fallback, and hide it after applying filesystem grants and denials. - Reject explicit grants, working directories, and executable paths that use the WSLg alias, directing users to the primary filesystem paths. - Hide host procfs when a fresh procfs cannot be mounted so process roots cannot restore access to the masked view. ## Testing Add regression tests for duplicate-root detection, mask ordering, existing ancestor masks, alias rejection, and unrestricted filesystem behavior. Add a WSLg runtime test covering masking with and without fresh procfs, continued access to an allowed file, and rejection of an executable using the alias. GitOrigin-RevId: 9ea31724c00fb24e3a504b6658339e8dfea35343
This commit is contained in:
@@ -6,6 +6,10 @@
|
||||
//! - sensitive subpaths such as `.git`, `.agents`, and `.codex` remain
|
||||
//! read-only even when their parent root is writable.
|
||||
//!
|
||||
//! Restricted execution also hides WSLg's duplicate distro root so it cannot
|
||||
//! expose filesystem contents outside the policy's path masks. Explicit grants and
|
||||
//! command paths using that alias are rejected before constructing mounts.
|
||||
//!
|
||||
//! The overall Linux sandbox is composed of:
|
||||
//! - seccomp + `PR_SET_NO_NEW_PRIVS` applied in-process, and
|
||||
//! - bubblewrap used to construct the filesystem view before exec.
|
||||
@@ -56,6 +60,7 @@ const LINUX_PLATFORM_DEFAULT_READ_ROOTS: &[&str] = &[
|
||||
|
||||
const MAX_UNREADABLE_GLOB_MATCHES: usize = 8192;
|
||||
pub(crate) const WSL_INTEROP_DIR: &str = "/run/WSL";
|
||||
pub(crate) const WSLG_DISTRO_ROOT: &str = "/mnt/wslg/distro";
|
||||
|
||||
/// Options that control how bubblewrap is invoked.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -69,6 +74,9 @@ pub(crate) struct BwrapOptions {
|
||||
pub network_mode: BwrapNetworkMode,
|
||||
/// Hide the WSL Windows interop socket from commands with restricted filesystem access.
|
||||
pub mask_wsl_interop: bool,
|
||||
/// Whether the host exposes WSLg's duplicate distro root, which must be hidden
|
||||
/// when constructing a restricted filesystem view.
|
||||
pub mask_wslg_distro: bool,
|
||||
/// Optional maximum depth for expanding unreadable glob patterns with ripgrep.
|
||||
///
|
||||
/// Keep this uncapped by default so existing nested deny-read matches are
|
||||
@@ -82,6 +90,7 @@ impl Default for BwrapOptions {
|
||||
mount_proc: true,
|
||||
network_mode: BwrapNetworkMode::FullAccess,
|
||||
mask_wsl_interop: false,
|
||||
mask_wslg_distro: false,
|
||||
glob_scan_max_depth: None,
|
||||
}
|
||||
}
|
||||
@@ -316,18 +325,29 @@ fn create_bwrap_flags(
|
||||
command_cwd: &Path,
|
||||
options: BwrapOptions,
|
||||
) -> Result<BwrapArgs> {
|
||||
if options.mask_wslg_distro {
|
||||
let granted_paths = file_system_sandbox_policy.entries.iter().flat_map(|entry| {
|
||||
FileSystemSandboxPolicy::restricted(vec![entry.clone()])
|
||||
.get_readable_roots_with_cwd(sandbox_policy_cwd)
|
||||
});
|
||||
let executable = command
|
||||
.first()
|
||||
.filter(|executable| executable.contains('/'))
|
||||
.map(|executable| AbsolutePathBuf::resolve_path_against_base(executable, command_cwd));
|
||||
for path in granted_paths
|
||||
.map(AbsolutePathBuf::into_path_buf)
|
||||
.chain([sandbox_policy_cwd.to_path_buf(), command_cwd.to_path_buf()])
|
||||
.chain(executable.map(AbsolutePathBuf::into_path_buf))
|
||||
{
|
||||
crate::wslg::ensure_supported_path(&path)?;
|
||||
}
|
||||
}
|
||||
let BwrapArgs {
|
||||
args: filesystem_args,
|
||||
preserved_files,
|
||||
synthetic_mount_targets,
|
||||
protected_create_targets,
|
||||
} = create_filesystem_args(
|
||||
file_system_sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
options
|
||||
.glob_scan_max_depth
|
||||
.or(file_system_sandbox_policy.glob_scan_max_depth),
|
||||
)?;
|
||||
} = create_filesystem_args(file_system_sandbox_policy, sandbox_policy_cwd, options)?;
|
||||
let normalized_command_cwd = normalize_command_cwd_for_bwrap(command_cwd);
|
||||
let mut args = Vec::new();
|
||||
args.push("--new-session".to_string());
|
||||
@@ -339,12 +359,12 @@ fn create_bwrap_flags(
|
||||
// the Linux filesystem policy by launching an unrestricted Windows process.
|
||||
args.push("--tmpfs".to_string());
|
||||
args.push(WSL_INTEROP_DIR.to_string());
|
||||
if !options.mount_proc {
|
||||
// Without a fresh procfs, the root bind retains the host procfs.
|
||||
// Hide it so /proc/<host-pid>/root cannot alias the interop sockets.
|
||||
args.push("--tmpfs".to_string());
|
||||
args.push("/proc".to_string());
|
||||
}
|
||||
}
|
||||
if (options.mask_wsl_interop || options.mask_wslg_distro) && !options.mount_proc {
|
||||
// Without a fresh procfs, the root bind retains the host procfs.
|
||||
// Hide it so host process roots cannot restore the masked WSL views.
|
||||
args.push("--tmpfs".to_string());
|
||||
args.push("/proc".to_string());
|
||||
}
|
||||
// Request a user namespace explicitly rather than relying on bubblewrap's
|
||||
// auto-enable behavior, which is skipped when the caller runs as uid 0.
|
||||
@@ -398,7 +418,7 @@ fn create_bwrap_flags(
|
||||
fn create_filesystem_args(
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
cwd: &Path,
|
||||
glob_scan_max_depth: Option<usize>,
|
||||
options: BwrapOptions,
|
||||
) -> Result<BwrapArgs> {
|
||||
let unreadable_globs = file_system_sandbox_policy.get_unreadable_globs_with_cwd(cwd);
|
||||
// Bubblewrap requires bind mount targets to exist. Skip missing writable
|
||||
@@ -461,9 +481,15 @@ fn create_filesystem_args(
|
||||
// to the existing matches we can see before constructing the mount overlay;
|
||||
// core tool helpers still evaluate the original patterns directly at read time.
|
||||
unreadable_roots.extend(
|
||||
expand_unreadable_globs_with_ripgrep(&unreadable_globs, cwd, glob_scan_max_depth)?
|
||||
.into_iter()
|
||||
.map(AbsolutePathBuf::into_path_buf),
|
||||
expand_unreadable_globs_with_ripgrep(
|
||||
&unreadable_globs,
|
||||
cwd,
|
||||
options
|
||||
.glob_scan_max_depth
|
||||
.or(file_system_sandbox_policy.glob_scan_max_depth),
|
||||
)?
|
||||
.into_iter()
|
||||
.map(AbsolutePathBuf::into_path_buf),
|
||||
);
|
||||
unreadable_roots.sort();
|
||||
unreadable_roots.dedup();
|
||||
@@ -673,6 +699,29 @@ fn create_filesystem_args(
|
||||
append_unreadable_root_args(&mut bwrap_args, &unreadable_root, &allowed_write_paths)?;
|
||||
}
|
||||
|
||||
if options.mask_wslg_distro
|
||||
&& !unreadable_roots.iter().any(|denied| {
|
||||
let distro = Path::new(WSLG_DISTRO_ROOT);
|
||||
distro.starts_with(denied)
|
||||
&& !allowed_write_paths.iter().any(|root| {
|
||||
root.starts_with(denied)
|
||||
&& (distro.starts_with(root) || root.starts_with(distro))
|
||||
})
|
||||
})
|
||||
{
|
||||
// Use the expanded masks, including symlink targets from deny globs.
|
||||
// An already hidden ancestor cannot accommodate a new child mount.
|
||||
// Otherwise apply this after every grant to exclude the duplicate view.
|
||||
bwrap_args.args.extend([
|
||||
"--perms".to_string(),
|
||||
"000".to_string(),
|
||||
"--tmpfs".to_string(),
|
||||
WSLG_DISTRO_ROOT.to_string(),
|
||||
"--remount-ro".to_string(),
|
||||
WSLG_DISTRO_ROOT.to_string(),
|
||||
]);
|
||||
}
|
||||
|
||||
Ok(bwrap_args)
|
||||
}
|
||||
|
||||
@@ -1342,6 +1391,10 @@ fn find_first_non_existent_component(target_path: &Path) -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "bwrap_wslg_tests.rs"]
|
||||
mod wslg_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1355,8 +1408,6 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
const NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH: Option<usize> = None;
|
||||
|
||||
#[test]
|
||||
fn default_unreadable_glob_scan_has_no_depth_cap() {
|
||||
assert_eq!(BwrapOptions::default().glob_scan_max_depth, None);
|
||||
@@ -1583,9 +1634,8 @@ mod tests {
|
||||
},
|
||||
]);
|
||||
|
||||
let args =
|
||||
create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect("filesystem args");
|
||||
let args = create_filesystem_args(&policy, temp_dir.path(), BwrapOptions::default())
|
||||
.expect("filesystem args");
|
||||
|
||||
assert!(args.args.windows(3).any(|window| {
|
||||
window == ["--bind", real_root_str.as_str(), real_root_str.as_str()]
|
||||
@@ -1627,9 +1677,8 @@ mod tests {
|
||||
missing_path_behavior: None,
|
||||
}]);
|
||||
|
||||
let args =
|
||||
create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect("filesystem args");
|
||||
let args = create_filesystem_args(&policy, temp_dir.path(), BwrapOptions::default())
|
||||
.expect("filesystem args");
|
||||
|
||||
assert!(args.args.windows(3).any(|window| {
|
||||
window
|
||||
@@ -1667,9 +1716,8 @@ mod tests {
|
||||
missing_path_behavior: None,
|
||||
}]);
|
||||
|
||||
let err =
|
||||
create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect_err("protected symlinked subpath should fail closed");
|
||||
let err = create_filesystem_args(&policy, temp_dir.path(), BwrapOptions::default())
|
||||
.expect_err("protected symlinked subpath should fail closed");
|
||||
let message = err.to_string();
|
||||
|
||||
assert!(
|
||||
@@ -1710,9 +1758,8 @@ mod tests {
|
||||
},
|
||||
]);
|
||||
|
||||
let err =
|
||||
create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect_err("deny-read path crossing writable symlink should fail closed");
|
||||
let err = create_filesystem_args(&policy, temp_dir.path(), BwrapOptions::default())
|
||||
.expect_err("deny-read path crossing writable symlink should fail closed");
|
||||
let message = err.to_string();
|
||||
|
||||
assert!(
|
||||
@@ -1743,9 +1790,8 @@ mod tests {
|
||||
},
|
||||
]);
|
||||
|
||||
let args =
|
||||
create_filesystem_args(&policy, Path::new("/"), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect("filesystem args");
|
||||
let args = create_filesystem_args(&policy, Path::new("/"), BwrapOptions::default())
|
||||
.expect("filesystem args");
|
||||
let registry_path = path_to_string(®istry_root);
|
||||
let temp_path = path_to_string(temp_root);
|
||||
let registry_bind = args
|
||||
@@ -1785,9 +1831,8 @@ mod tests {
|
||||
},
|
||||
]);
|
||||
|
||||
let args =
|
||||
create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect("filesystem args");
|
||||
let args = create_filesystem_args(&policy, temp_dir.path(), BwrapOptions::default())
|
||||
.expect("filesystem args");
|
||||
|
||||
assert_empty_file_bound_without_perms(&args.args, &blocked);
|
||||
assert_empty_directory_mounted_read_only(&args.args, &workspace.join(".git"));
|
||||
@@ -1825,9 +1870,8 @@ mod tests {
|
||||
missing_path_behavior: None,
|
||||
}]);
|
||||
|
||||
let args =
|
||||
create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect("filesystem args");
|
||||
let args = create_filesystem_args(&policy, temp_dir.path(), BwrapOptions::default())
|
||||
.expect("filesystem args");
|
||||
let dot_git_str = path_to_string(&dot_git);
|
||||
|
||||
assert_empty_file_bound_without_perms(&args.args, &dot_git);
|
||||
@@ -1873,7 +1917,7 @@ mod tests {
|
||||
missing_path_behavior: None,
|
||||
}]);
|
||||
|
||||
let args = create_filesystem_args(&policy, &workspace, NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
let args = create_filesystem_args(&policy, &workspace, BwrapOptions::default())
|
||||
.expect("filesystem args");
|
||||
assert_empty_directory_mounted_read_only(&args.args, &dot_git);
|
||||
assert_empty_directory_mounted_read_only(&args.args, &workspace.join(".agents"));
|
||||
@@ -1910,9 +1954,8 @@ mod tests {
|
||||
missing_path_behavior: None,
|
||||
}]);
|
||||
|
||||
let args =
|
||||
create_filesystem_args(&policy, &link_workspace, NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect("filesystem args");
|
||||
let args = create_filesystem_args(&policy, &link_workspace, BwrapOptions::default())
|
||||
.expect("filesystem args");
|
||||
assert_empty_directory_mounted_read_only(&args.args, &dot_git);
|
||||
assert_empty_directory_mounted_read_only(&args.args, &workspace.join(".agents"));
|
||||
assert_empty_directory_mounted_read_only(&args.args, &workspace.join(".codex"));
|
||||
@@ -1942,9 +1985,8 @@ mod tests {
|
||||
/*exclude_slash_tmp*/ true,
|
||||
);
|
||||
|
||||
let args =
|
||||
create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect("filesystem args");
|
||||
let args = create_filesystem_args(&policy, temp_dir.path(), BwrapOptions::default())
|
||||
.expect("filesystem args");
|
||||
let existing_root = path_to_string(&existing_root);
|
||||
let missing_root = path_to_string(&missing_root);
|
||||
|
||||
@@ -2001,9 +2043,8 @@ mod tests {
|
||||
},
|
||||
]);
|
||||
|
||||
let args =
|
||||
create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect("filesystem args");
|
||||
let args = create_filesystem_args(&policy, temp_dir.path(), BwrapOptions::default())
|
||||
.expect("filesystem args");
|
||||
let dot_git = path_to_string(&temp_dir.path().join(".git"));
|
||||
let dot_agents = path_to_string(&temp_dir.path().join(".agents"));
|
||||
let dot_codex = path_to_string(&temp_dir.path().join(".codex"));
|
||||
@@ -2057,9 +2098,8 @@ mod tests {
|
||||
},
|
||||
]);
|
||||
|
||||
let args =
|
||||
create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect("filesystem args");
|
||||
let args = create_filesystem_args(&policy, temp_dir.path(), BwrapOptions::default())
|
||||
.expect("filesystem args");
|
||||
let dot_vscode = path_to_string(&temp_dir.path().join(".vscode"));
|
||||
let dot_secrets = path_to_string(&temp_dir.path().join(".secrets"));
|
||||
|
||||
@@ -2075,12 +2115,8 @@ mod tests {
|
||||
/*exclude_slash_tmp*/ true,
|
||||
);
|
||||
|
||||
let args = create_filesystem_args(
|
||||
&sandbox_policy,
|
||||
Path::new("/"),
|
||||
NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH,
|
||||
)
|
||||
.expect("bwrap fs args");
|
||||
let args = create_filesystem_args(&sandbox_policy, Path::new("/"), BwrapOptions::default())
|
||||
.expect("bwrap fs args");
|
||||
assert!(args.preserved_files.is_empty());
|
||||
assert_eq!(
|
||||
synthetic_mount_target_paths(&args),
|
||||
@@ -2174,9 +2210,8 @@ mod tests {
|
||||
missing_path_behavior: None,
|
||||
}]);
|
||||
|
||||
let args =
|
||||
create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect("filesystem args");
|
||||
let args = create_filesystem_args(&policy, temp_dir.path(), BwrapOptions::default())
|
||||
.expect("filesystem args");
|
||||
|
||||
assert_eq!(args.args[0..4], ["--tmpfs", "/", "--dev", "/dev"]);
|
||||
|
||||
@@ -2202,9 +2237,8 @@ mod tests {
|
||||
missing_path_behavior: None,
|
||||
}]);
|
||||
|
||||
let args =
|
||||
create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect("filesystem args");
|
||||
let args = create_filesystem_args(&policy, temp_dir.path(), BwrapOptions::default())
|
||||
.expect("filesystem args");
|
||||
|
||||
assert!(
|
||||
args.args
|
||||
@@ -2244,9 +2278,8 @@ mod tests {
|
||||
},
|
||||
]);
|
||||
|
||||
let args =
|
||||
create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect("filesystem args");
|
||||
let args = create_filesystem_args(&policy, temp_dir.path(), BwrapOptions::default())
|
||||
.expect("filesystem args");
|
||||
|
||||
assert!(args.args.windows(3).any(|window| {
|
||||
window
|
||||
@@ -2322,9 +2355,8 @@ mod tests {
|
||||
},
|
||||
]);
|
||||
|
||||
let args =
|
||||
create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect("filesystem args");
|
||||
let args = create_filesystem_args(&policy, temp_dir.path(), BwrapOptions::default())
|
||||
.expect("filesystem args");
|
||||
let docs_str = path_to_string(docs.as_path());
|
||||
let docs_public_str = path_to_string(docs_public.as_path());
|
||||
let docs_ro_index = args
|
||||
@@ -2375,9 +2407,8 @@ mod tests {
|
||||
},
|
||||
]);
|
||||
|
||||
let args =
|
||||
create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect("filesystem args");
|
||||
let args = create_filesystem_args(&policy, temp_dir.path(), BwrapOptions::default())
|
||||
.expect("filesystem args");
|
||||
let blocked_str = path_to_string(blocked.as_path());
|
||||
let allowed_str = path_to_string(allowed.as_path());
|
||||
let blocked_none_index = args
|
||||
@@ -2443,9 +2474,8 @@ mod tests {
|
||||
},
|
||||
]);
|
||||
|
||||
let args =
|
||||
create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect("filesystem args");
|
||||
let args = create_filesystem_args(&policy, temp_dir.path(), BwrapOptions::default())
|
||||
.expect("filesystem args");
|
||||
let blocked_str = path_to_string(blocked.as_path());
|
||||
let allowed_dir_str = path_to_string(allowed_dir.as_path());
|
||||
let allowed_file_str = path_to_string(allowed_file.as_path());
|
||||
@@ -2521,9 +2551,8 @@ mod tests {
|
||||
},
|
||||
]);
|
||||
|
||||
let args =
|
||||
create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect("filesystem args");
|
||||
let args = create_filesystem_args(&policy, temp_dir.path(), BwrapOptions::default())
|
||||
.expect("filesystem args");
|
||||
let blocked_none_index = args
|
||||
.args
|
||||
.windows(4)
|
||||
@@ -2568,9 +2597,8 @@ mod tests {
|
||||
},
|
||||
]);
|
||||
|
||||
let args =
|
||||
create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect("filesystem args");
|
||||
let args = create_filesystem_args(&policy, temp_dir.path(), BwrapOptions::default())
|
||||
.expect("filesystem args");
|
||||
let blocked_str = path_to_string(blocked.as_path());
|
||||
|
||||
assert!(
|
||||
@@ -2612,9 +2640,8 @@ mod tests {
|
||||
},
|
||||
]);
|
||||
|
||||
let args =
|
||||
create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect("filesystem args");
|
||||
let args = create_filesystem_args(&policy, temp_dir.path(), BwrapOptions::default())
|
||||
.expect("filesystem args");
|
||||
let blocked_file_str = path_to_string(blocked_file.as_path());
|
||||
|
||||
assert_eq!(args.preserved_files.len(), 1);
|
||||
@@ -2645,8 +2672,15 @@ mod tests {
|
||||
let policy =
|
||||
default_policy_with_unreadable_glob(format!("{}/**/*.env", temp_dir.path().display()));
|
||||
|
||||
let args =
|
||||
create_filesystem_args(&policy, temp_dir.path(), Some(2)).expect("filesystem args");
|
||||
let args = create_filesystem_args(
|
||||
&policy,
|
||||
temp_dir.path(),
|
||||
BwrapOptions {
|
||||
glob_scan_max_depth: Some(2),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("filesystem args");
|
||||
|
||||
assert_file_masked(&args.args, &root_env);
|
||||
assert_file_masked(&args.args, &nested_env);
|
||||
@@ -2677,8 +2711,15 @@ mod tests {
|
||||
let policy =
|
||||
default_policy_with_unreadable_glob(format!("{}/**/*.env", link_root.display()));
|
||||
|
||||
let args =
|
||||
create_filesystem_args(&policy, temp_dir.path(), Some(2)).expect("filesystem args");
|
||||
let args = create_filesystem_args(
|
||||
&policy,
|
||||
temp_dir.path(),
|
||||
BwrapOptions {
|
||||
glob_scan_max_depth: Some(2),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("filesystem args");
|
||||
|
||||
assert_file_masked(&args.args, &real_secret);
|
||||
}
|
||||
|
||||
258
codex-rs/linux-sandbox/src/bwrap_wslg_tests.rs
Normal file
258
codex-rs/linux-sandbox/src/bwrap_wslg_tests.rs
Normal file
@@ -0,0 +1,258 @@
|
||||
//! Regression coverage for excluding WSLg's duplicate filesystem view.
|
||||
|
||||
use super::*;
|
||||
use codex_protocol::protocol::FileSystemSandboxEntry;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test_case::test_case(FileSystemAccessMode::Read; "read_root")]
|
||||
#[test_case::test_case(FileSystemAccessMode::Write; "write_root")]
|
||||
fn wslg_mask_follows_filesystem_grants_and_denials(root_access: FileSystemAccessMode) {
|
||||
let temp_dir = tempfile::TempDir::new().expect("temp dir");
|
||||
let denied = temp_dir.path().join("denied.txt");
|
||||
fs::write(&denied, "fixture").expect("write fixture");
|
||||
let denied = AbsolutePathBuf::from_absolute_path(denied).expect("absolute fixture");
|
||||
let workspace =
|
||||
AbsolutePathBuf::from_absolute_path(temp_dir.path()).expect("absolute workspace");
|
||||
|
||||
for denied_path in [
|
||||
FileSystemPath::from(denied.clone()),
|
||||
FileSystemPath::GlobPattern {
|
||||
pattern: format!("{}/*.txt", temp_dir.path().display()),
|
||||
},
|
||||
] {
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: root_access,
|
||||
missing_path_behavior: None,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: workspace.clone().into(),
|
||||
access: FileSystemAccessMode::Write,
|
||||
missing_path_behavior: None,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: denied_path,
|
||||
access: FileSystemAccessMode::Deny,
|
||||
missing_path_behavior: None,
|
||||
},
|
||||
]);
|
||||
for mount_proc in [true, false] {
|
||||
for network_mode in [BwrapNetworkMode::FullAccess, BwrapNetworkMode::Isolated] {
|
||||
let args = create_bwrap_command_args(
|
||||
vec!["/bin/true".to_string()],
|
||||
&policy,
|
||||
temp_dir.path(),
|
||||
temp_dir.path(),
|
||||
BwrapOptions {
|
||||
mount_proc,
|
||||
network_mode,
|
||||
mask_wslg_distro: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("create restricted bwrap args")
|
||||
.args;
|
||||
let mask = args
|
||||
.windows(6)
|
||||
.position(|args| {
|
||||
args == [
|
||||
"--perms",
|
||||
"000",
|
||||
"--tmpfs",
|
||||
WSLG_DISTRO_ROOT,
|
||||
"--remount-ro",
|
||||
WSLG_DISTRO_ROOT,
|
||||
]
|
||||
})
|
||||
.expect("unreadable, read-only WSLg mask");
|
||||
let last_grant = args
|
||||
.iter()
|
||||
.rposition(|arg| arg == "--bind" || arg == "--ro-bind")
|
||||
.expect("filesystem grant");
|
||||
let denial = args
|
||||
.iter()
|
||||
.rposition(|arg| arg == &path_to_string(denied.as_path()))
|
||||
.expect("denied file mask");
|
||||
assert!(last_grant < mask && denial < mask);
|
||||
assert!(args.windows(2).any(|args| {
|
||||
args == [if mount_proc { "--proc" } else { "--tmpfs" }, "/proc"]
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wslg_mask_is_omitted_when_policy_already_hides_an_ancestor() {
|
||||
for denied in ["/mnt", "/mnt/wslg", WSLG_DISTRO_ROOT] {
|
||||
let mut policy = FileSystemSandboxPolicy::read_only();
|
||||
policy.entries.push(FileSystemSandboxEntry {
|
||||
path: AbsolutePathBuf::from_absolute_path(denied)
|
||||
.expect("absolute denied directory")
|
||||
.into(),
|
||||
access: FileSystemAccessMode::Deny,
|
||||
missing_path_behavior: None,
|
||||
});
|
||||
let mut commands = Vec::new();
|
||||
for mask_wslg_distro in [false, true] {
|
||||
commands.push(
|
||||
create_bwrap_command_args(
|
||||
vec!["/bin/true".to_string()],
|
||||
&policy,
|
||||
Path::new("/"),
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
mask_wslg_distro,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("create bwrap args with denied ancestor")
|
||||
.args,
|
||||
);
|
||||
}
|
||||
assert_eq!(commands[0], commands[1]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrestricted_filesystem_preserves_wslg_with_or_without_network_isolation() {
|
||||
for network_mode in [BwrapNetworkMode::FullAccess, BwrapNetworkMode::Isolated] {
|
||||
let mut commands = Vec::new();
|
||||
for mask_wslg_distro in [false, true] {
|
||||
commands.push(
|
||||
create_bwrap_command_args(
|
||||
vec!["/bin/true".to_string()],
|
||||
&FileSystemSandboxPolicy::unrestricted(),
|
||||
Path::new("/"),
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
network_mode,
|
||||
mask_wslg_distro,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("create unrestricted bwrap args")
|
||||
.args,
|
||||
);
|
||||
}
|
||||
assert_eq!(commands[0], commands[1]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test_case::test_case("grant_read"; "read_grant")]
|
||||
#[test_case::test_case("grant_write"; "write_grant")]
|
||||
#[test_case::test_case("policy_cwd"; "policy_cwd")]
|
||||
#[test_case::test_case("command_cwd"; "command_cwd")]
|
||||
#[test_case::test_case("executable"; "executable")]
|
||||
fn explicit_wslg_alias_paths_are_rejected(source: &str) {
|
||||
let alias = Path::new(WSLG_DISTRO_ROOT).join("project");
|
||||
let mut policy = FileSystemSandboxPolicy::read_only();
|
||||
if source.starts_with("grant_") {
|
||||
policy.entries.push(FileSystemSandboxEntry {
|
||||
path: AbsolutePathBuf::from_absolute_path(&alias)
|
||||
.expect("absolute alias")
|
||||
.into(),
|
||||
access: if source == "grant_write" {
|
||||
FileSystemAccessMode::Write
|
||||
} else {
|
||||
FileSystemAccessMode::Read
|
||||
},
|
||||
missing_path_behavior: None,
|
||||
});
|
||||
}
|
||||
let executable = if source == "executable" {
|
||||
alias.as_path()
|
||||
} else {
|
||||
Path::new("/bin/true")
|
||||
};
|
||||
let policy_cwd = if source == "policy_cwd" {
|
||||
alias.as_path()
|
||||
} else {
|
||||
Path::new("/")
|
||||
};
|
||||
let command_cwd = if source == "command_cwd" {
|
||||
alias.as_path()
|
||||
} else {
|
||||
Path::new("/")
|
||||
};
|
||||
let error = create_bwrap_command_args(
|
||||
vec![path_to_string(executable)],
|
||||
&policy,
|
||||
policy_cwd,
|
||||
command_cwd,
|
||||
BwrapOptions {
|
||||
mask_wslg_distro: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect_err("explicit alias must be rejected before mounts are built");
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
format!(
|
||||
"Fatal error: restricted sandboxes do not support paths under {WSLG_DISTRO_ROOT}: {}; use the corresponding path under the primary filesystem root instead",
|
||||
alias.display()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glob_ancestor_mask_in_no_rg_fallback() {
|
||||
// Isolate PATH in a child test process instead of mutating the test runner's
|
||||
// environment. This exercises the supported missing-rg branch end to end.
|
||||
const CHILD: &str = "CODEX_WSLG_GLOB_TEST_CHILD";
|
||||
if std::env::var_os(CHILD).is_none() {
|
||||
let output = Command::new(std::env::current_exe().expect("test executable"))
|
||||
.args([
|
||||
"--exact",
|
||||
"bwrap::wslg_tests::glob_ancestor_mask_in_no_rg_fallback",
|
||||
"--nocapture",
|
||||
])
|
||||
.env(CHILD, "1")
|
||||
.env("PATH", "")
|
||||
.output()
|
||||
.expect("run isolated test");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
return;
|
||||
}
|
||||
let temp = tempfile::tempdir().expect("temp directory");
|
||||
// '/' is an ancestor on every test host; no WSL mount is needed. Only
|
||||
// inspect arguments: never launch this synthetic filesystem policy.
|
||||
let relative_root: PathBuf =
|
||||
std::iter::repeat_n("..", temp.path().components().count() - 1).collect();
|
||||
std::os::unix::fs::symlink(relative_root, temp.path().join("alias")).expect("relative symlink");
|
||||
let mut policy = FileSystemSandboxPolicy::read_only();
|
||||
policy.entries.push(FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: format!("{}/alias*", temp.path().display()),
|
||||
},
|
||||
access: FileSystemAccessMode::Deny,
|
||||
missing_path_behavior: None,
|
||||
});
|
||||
let args = create_bwrap_command_args(
|
||||
vec!["/bin/true".to_string()],
|
||||
&policy,
|
||||
Path::new("/"),
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
mask_wslg_distro: true,
|
||||
mount_proc: false,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("expanded ancestor mask should suffice")
|
||||
.args;
|
||||
assert!(
|
||||
args.windows(4)
|
||||
.any(|args| args == ["--tmpfs", "/", "--remount-ro", "/"])
|
||||
);
|
||||
assert!(!args.iter().any(|arg| arg == WSLG_DISTRO_ROOT));
|
||||
assert!(args.windows(2).any(|args| args == ["--tmpfs", "/proc"]));
|
||||
}
|
||||
@@ -23,6 +23,8 @@ mod linux_run_main;
|
||||
mod proxy_lifecycle;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod proxy_routing;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod wslg;
|
||||
|
||||
/// Exit status returned when bundled bubblewrap fails digest verification.
|
||||
#[cfg(target_os = "linux")]
|
||||
|
||||
@@ -23,6 +23,7 @@ use std::time::Duration;
|
||||
use crate::bwrap::BwrapNetworkMode;
|
||||
use crate::bwrap::BwrapOptions;
|
||||
use crate::bwrap::WSL_INTEROP_DIR;
|
||||
use crate::bwrap::WSLG_DISTRO_ROOT;
|
||||
use crate::bwrap::create_bwrap_command_args;
|
||||
use crate::landlock::apply_permission_profile_to_current_thread;
|
||||
use crate::launcher::exec_bwrap;
|
||||
@@ -303,6 +304,31 @@ pub fn run_main() -> ! {
|
||||
} else {
|
||||
(None, Vec::new())
|
||||
};
|
||||
let options = BwrapOptions {
|
||||
mount_proc: !no_proc,
|
||||
network_mode: bwrap_network_mode(network_sandbox_policy, allow_network_for_proxy),
|
||||
mask_wsl_interop: !file_system_sandbox_policy.has_full_disk_write_access()
|
||||
&& Path::new(WSL_INTEROP_DIR).is_dir(),
|
||||
mask_wslg_distro: (!file_system_sandbox_policy.has_full_disk_write_access()
|
||||
|| !file_system_sandbox_policy
|
||||
.get_unreadable_globs_with_cwd(&sandbox_policy_cwd)
|
||||
.is_empty())
|
||||
&& crate::wslg::is_duplicate_root(Path::new(WSLG_DISTRO_ROOT))
|
||||
.unwrap_or_else(|err| exit_with_bwrap_build_error(err.into())),
|
||||
..Default::default()
|
||||
};
|
||||
if options.mask_wslg_distro
|
||||
&& let Some(executable) = command
|
||||
.first()
|
||||
.filter(|executable| executable.contains('/'))
|
||||
{
|
||||
let executable = command_cwd
|
||||
.as_deref()
|
||||
.unwrap_or(&sandbox_policy_cwd)
|
||||
.join(executable);
|
||||
crate::wslg::ensure_supported_path(&executable)
|
||||
.unwrap_or_else(|err| exit_with_bwrap_build_error(err));
|
||||
}
|
||||
let inner = build_inner_seccomp_command(InnerSeccompCommandArgs {
|
||||
sandbox_policy_cwd: &sandbox_policy_cwd,
|
||||
command_cwd: command_cwd.as_deref(),
|
||||
@@ -315,10 +341,9 @@ pub fn run_main() -> ! {
|
||||
&sandbox_policy_cwd,
|
||||
command_cwd.as_deref(),
|
||||
&file_system_sandbox_policy,
|
||||
bwrap_network_mode(network_sandbox_policy, allow_network_for_proxy),
|
||||
options,
|
||||
inner,
|
||||
proxy_controls,
|
||||
!no_proc,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -418,30 +443,21 @@ fn run_bwrap_with_proc_fallback(
|
||||
sandbox_policy_cwd: &Path,
|
||||
command_cwd: Option<&Path>,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_mode: BwrapNetworkMode,
|
||||
mut options: BwrapOptions,
|
||||
inner: Vec<String>,
|
||||
proxy_controls: Vec<File>,
|
||||
mount_proc: bool,
|
||||
) -> ! {
|
||||
let mut mount_proc = mount_proc;
|
||||
let command_cwd = command_cwd.unwrap_or(sandbox_policy_cwd);
|
||||
|
||||
if mount_proc
|
||||
&& !preflight_proc_mount_support(network_mode)
|
||||
if options.mount_proc
|
||||
&& !preflight_proc_mount_support(options.network_mode)
|
||||
.unwrap_or_else(|err| exit_with_bwrap_build_error(err))
|
||||
{
|
||||
// Keep the retry silent so sandbox-internal diagnostics do not leak into the
|
||||
// child process stderr stream.
|
||||
mount_proc = false;
|
||||
options.mount_proc = false;
|
||||
}
|
||||
|
||||
let options = BwrapOptions {
|
||||
mount_proc,
|
||||
network_mode,
|
||||
mask_wsl_interop: !file_system_sandbox_policy.has_full_disk_write_access()
|
||||
&& Path::new(WSL_INTEROP_DIR).is_dir(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut bwrap_args = build_bwrap_argv(
|
||||
inner,
|
||||
file_system_sandbox_policy,
|
||||
|
||||
66
codex-rs/linux-sandbox/src/wslg.rs
Normal file
66
codex-rs/linux-sandbox/src/wslg.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
//! Identify WSLg's duplicate root without relying on filtered environment variables
|
||||
//! or kernel branding. An unrelated directory at the same path is left untouched.
|
||||
//! Explicit alias paths are rejected before building the restricted filesystem.
|
||||
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::Path;
|
||||
|
||||
pub(crate) fn is_duplicate_root(path: &Path) -> io::Result<bool> {
|
||||
match fs::symlink_metadata(path) {
|
||||
Ok(metadata) => {
|
||||
let root = fs::metadata("/")?;
|
||||
Ok(metadata.is_dir() && (metadata.dev(), metadata.ino()) == (root.dev(), root.ino()))
|
||||
}
|
||||
Err(err)
|
||||
if matches!(
|
||||
err.kind(),
|
||||
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
|
||||
) =>
|
||||
{
|
||||
Ok(false)
|
||||
}
|
||||
Err(_) => {
|
||||
// Mount metadata remains readable when an ancestor is not searchable.
|
||||
// If neither observation is available, fail rather than guess that
|
||||
// an inaccessible duplicate can safely remain in the sandbox.
|
||||
let mountinfo = fs::read_to_string("/proc/self/mountinfo")?;
|
||||
let root = mount_identity(&mountinfo, "/")?
|
||||
.ok_or_else(|| io::Error::other("root mount identity is unavailable"))?;
|
||||
Ok(Some(root) == mount_identity(&mountinfo, &path.to_string_lossy())?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_supported_path(path: &Path) -> codex_protocol::error::Result<()> {
|
||||
let alias = crate::bwrap::WSLG_DISTRO_ROOT;
|
||||
if path.starts_with(alias)
|
||||
|| fs::canonicalize(path).is_ok_and(|resolved| resolved.starts_with(alias))
|
||||
{
|
||||
return Err(codex_protocol::error::CodexErr::Fatal(format!(
|
||||
"restricted sandboxes do not support paths under {alias}: {}; use the corresponding path under the primary filesystem root instead",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mount_identity<'a>(mountinfo: &'a str, path: &str) -> io::Result<Option<(&'a str, &'a str)>> {
|
||||
let mut identities = mountinfo.lines().filter_map(|line| {
|
||||
let mut fields = line.split_ascii_whitespace();
|
||||
let device = fields.nth(2)?;
|
||||
let root = fields.next()?;
|
||||
let mountpoint = fields.next()?;
|
||||
(mountpoint == path).then_some((device, root))
|
||||
});
|
||||
let identity = identities.next();
|
||||
if identities.any(|other| Some(other) != identity) {
|
||||
return Err(io::Error::other("mount identity is ambiguous"));
|
||||
}
|
||||
Ok(identity)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "wslg_tests.rs"]
|
||||
mod tests;
|
||||
49
codex-rs/linux-sandbox/src/wslg_tests.rs
Normal file
49
codex-rs/linux-sandbox/src/wslg_tests.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use super::is_duplicate_root;
|
||||
use super::mount_identity;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn ordinary_paths_do_not_identify_a_duplicate_root() {
|
||||
let temp = tempfile::tempdir().expect("temporary directory");
|
||||
let file = temp.path().join("file");
|
||||
fs::write(&file, "fixture").expect("write file");
|
||||
let link = temp.path().join("link");
|
||||
std::os::unix::fs::symlink("/", &link).expect("create symlink");
|
||||
let missing = temp.path().join("missing");
|
||||
let not_a_directory = file.join("child");
|
||||
for path in [temp.path(), &file, &link, &missing, ¬_a_directory] {
|
||||
assert!(!is_duplicate_root(path).expect("inspect path"));
|
||||
}
|
||||
assert!(is_duplicate_root(Path::new("/")).expect("inspect root"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mount_identity_distinguishes_roots_on_the_same_device() {
|
||||
let mounts = "36 25 8:16 / / rw - ext4 /dev/sdb rw\n\
|
||||
39 25 8:16 / /mnt/wslg/distro rw - ext4 /dev/sdb rw\n\
|
||||
40 25 8:16 /other /unrelated rw - ext4 /dev/sdb rw\n\
|
||||
41 25 0:45 / /different-device rw - tmpfs tmpfs rw\n";
|
||||
assert_eq!(
|
||||
mount_identity(mounts, "/").unwrap(),
|
||||
mount_identity(mounts, "/mnt/wslg/distro").unwrap()
|
||||
);
|
||||
assert_ne!(
|
||||
mount_identity(mounts, "/").unwrap(),
|
||||
mount_identity(mounts, "/unrelated").unwrap()
|
||||
);
|
||||
assert_ne!(
|
||||
mount_identity(mounts, "/").unwrap(),
|
||||
mount_identity(mounts, "/different-device").unwrap()
|
||||
);
|
||||
assert_eq!(mount_identity(mounts, "/missing").unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conflicting_stacked_mounts_are_ambiguous() {
|
||||
let mounts = "36 25 8:16 / / rw - ext4 /dev/sdb rw\n\
|
||||
39 25 8:16 / /mnt/wslg/distro rw - ext4 /dev/sdb rw\n\
|
||||
40 39 0:45 / /mnt/wslg/distro rw - tmpfs tmpfs rw\n";
|
||||
assert!(mount_identity(mounts, "/mnt/wslg/distro").is_err());
|
||||
}
|
||||
@@ -25,6 +25,9 @@ use std::process::Output;
|
||||
use std::time::Duration;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[path = "wslg_tests.rs"]
|
||||
mod wslg_tests;
|
||||
|
||||
// At least on GitHub CI, the arm64 tests appear to need longer timeouts.
|
||||
|
||||
#[cfg(not(target_arch = "aarch64"))]
|
||||
|
||||
80
codex-rs/linux-sandbox/tests/suite/wslg_tests.rs
Normal file
80
codex-rs/linux-sandbox/tests/suite/wslg_tests.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
//! WSLg runtime coverage for the duplicate-view mask and procfs fallback.
|
||||
|
||||
use super::NETWORK_TIMEOUT_MS;
|
||||
use super::codex_linux_sandbox_exe;
|
||||
use super::should_skip_bwrap_tests;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::os::unix::fs::MetadataExt as _;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
async fn wslg_duplicate_view_is_masked_with_and_without_fresh_procfs() {
|
||||
let root = std::fs::metadata("/").expect("inspect root");
|
||||
let has_wslg_root = std::fs::symlink_metadata("/mnt/wslg/distro").is_ok_and(|metadata| {
|
||||
metadata.is_dir() && (metadata.dev(), metadata.ino()) == (root.dev(), root.ino())
|
||||
});
|
||||
if !has_wslg_root || should_skip_bwrap_tests().await {
|
||||
eprintln!("skipping WSLg test: WSLg or bwrap prerequisites are unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
let workspace = tempfile::tempdir().expect("create workspace");
|
||||
let allowed = workspace.path().join("allowed.txt");
|
||||
std::fs::write(&allowed, "allowed\n").expect("write allowed fixture");
|
||||
let profile = serde_json::to_string(&PermissionProfile::read_only())
|
||||
.expect("serialize read-only profile");
|
||||
|
||||
for no_proc in [false, true] {
|
||||
let mut command = tokio::process::Command::new(codex_linux_sandbox_exe());
|
||||
command
|
||||
.arg("--sandbox-policy-cwd")
|
||||
.arg(workspace.path())
|
||||
.args(["--permission-profile", &profile]);
|
||||
if no_proc {
|
||||
command.arg("--no-proc");
|
||||
}
|
||||
// Inspect the protection itself, without accessing data through the alias.
|
||||
let script = if no_proc {
|
||||
"stat -c %a /mnt/wslg/distro && stat -f -c %T /mnt/wslg/distro && cat \"$1\" && test ! -e /proc/1"
|
||||
} else {
|
||||
"stat -c %a /mnt/wslg/distro && stat -f -c %T /mnt/wslg/distro && cat \"$1\""
|
||||
};
|
||||
let output = tokio::time::timeout(
|
||||
Duration::from_millis(NETWORK_TIMEOUT_MS),
|
||||
command
|
||||
.args(["--", "/bin/sh", "-c", script, "sh"])
|
||||
.arg(&allowed)
|
||||
.kill_on_drop(true)
|
||||
.output(),
|
||||
)
|
||||
.await
|
||||
.expect("WSLg sandbox check should finish")
|
||||
.expect("sandbox helper should start");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"WSLg mask check failed (no_proc={no_proc}): {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert_eq!(output.stdout, b"0\ntmpfs\nallowed\n");
|
||||
}
|
||||
// Exercise the public helper boundary: production wraps the requested
|
||||
// executable in another helper command before constructing bwrap args.
|
||||
let output = tokio::process::Command::new(codex_linux_sandbox_exe())
|
||||
.arg("--sandbox-policy-cwd")
|
||||
.arg(workspace.path())
|
||||
.args([
|
||||
"--permission-profile",
|
||||
&profile,
|
||||
"--",
|
||||
"/mnt/wslg/distro/bin/true",
|
||||
])
|
||||
.output()
|
||||
.await
|
||||
.expect("sandbox helper should start");
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
assert_eq!(
|
||||
String::from_utf8(output.stderr).expect("UTF-8 diagnostic"),
|
||||
"error building bubblewrap command: Fatal error: restricted sandboxes do not support paths under /mnt/wslg/distro: /mnt/wslg/distro/bin/true; use the corresponding path under the primary filesystem root instead\n"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user