mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
Protect macOS Seatbelt writable root anchors (#39599)
## Why A sandboxed process could replace a writable directory root, changing the authority boundary used to construct a later sandbox policy. ## What changed - Deny unlinking or renaming directory roots granted write access by Seatbelt, while preserving replacement and deletion behavior for writable files. - Reject writable roots with nested symlink components and report these as Seatbelt preparation errors instead of network proxy failures. - Normalize writable roots before adding them to the generated Seatbelt policy. ## Testing Add coverage for symlink rejection, error classification, directory-root replacement and rename protection, newly created roots, and writable file replacement. GitOrigin-RevId: ab1ed4e55f20034bc43e028e6529d3d1f0d8181c
This commit is contained in:
@@ -8,4 +8,5 @@ codex_rust_crate(
|
||||
"src/seatbelt_network_policy.sbpl",
|
||||
],
|
||||
crate_name = "codex_sandboxing",
|
||||
test_tags = ["no-sandbox"],
|
||||
)
|
||||
|
||||
@@ -68,6 +68,10 @@ impl From<SandboxTransformError> for CodexErr {
|
||||
SandboxTransformError::EnvironmentNetworkProxy(message) => {
|
||||
CodexErr::UnsupportedOperation(message)
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
SandboxTransformError::SeatbeltPreparation(message) => {
|
||||
CodexErr::UnsupportedOperation(message)
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
SandboxTransformError::Wsl1UnsupportedForBubblewrap => {
|
||||
CodexErr::UnsupportedOperation(crate::bwrap::WSL1_BWRAP_WARNING.to_string())
|
||||
|
||||
@@ -207,6 +207,8 @@ pub enum SandboxTransformError {
|
||||
},
|
||||
MissingLinuxSandboxExecutable,
|
||||
EnvironmentNetworkProxy(String),
|
||||
#[cfg(target_os = "macos")]
|
||||
SeatbeltPreparation(String),
|
||||
#[cfg(target_os = "linux")]
|
||||
Wsl1UnsupportedForBubblewrap,
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
@@ -234,6 +236,10 @@ impl std::fmt::Display for SandboxTransformError {
|
||||
Self::EnvironmentNetworkProxy(err) => {
|
||||
write!(f, "failed to prepare environment network proxy: {err}")
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::SeatbeltPreparation(err) => {
|
||||
write!(f, "failed to prepare Seatbelt sandbox: {err}")
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
Self::Wsl1UnsupportedForBubblewrap => write!(f, "{WSL1_BWRAP_WARNING}"),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
@@ -253,6 +259,8 @@ impl std::error::Error for SandboxTransformError {
|
||||
| Self::InvalidSandboxPolicyCwd { source, .. } => Some(source),
|
||||
Self::MissingLinuxSandboxExecutable => None,
|
||||
Self::EnvironmentNetworkProxy(_) => None,
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::SeatbeltPreparation(_) => None,
|
||||
#[cfg(target_os = "linux")]
|
||||
Self::Wsl1UnsupportedForBubblewrap => None,
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
@@ -360,6 +368,7 @@ impl SandboxManager {
|
||||
SandboxType::MacosSeatbelt => {
|
||||
use crate::seatbelt::CreateSeatbeltCommandArgsParams;
|
||||
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
|
||||
use crate::seatbelt::SeatbeltPreparationError;
|
||||
use crate::seatbelt::create_seatbelt_command_args_with_profile;
|
||||
|
||||
let pending = pending_sandboxed_request?;
|
||||
@@ -380,7 +389,14 @@ impl SandboxManager {
|
||||
},
|
||||
self.seatbelt_profile,
|
||||
)
|
||||
.map_err(SandboxTransformError::EnvironmentNetworkProxy)?;
|
||||
.map_err(|err| match err {
|
||||
SeatbeltPreparationError::FileSystem(message) => {
|
||||
SandboxTransformError::SeatbeltPreparation(message)
|
||||
}
|
||||
SeatbeltPreparationError::EnvironmentNetworkProxy(message) => {
|
||||
SandboxTransformError::EnvironmentNetworkProxy(message)
|
||||
}
|
||||
})?;
|
||||
let mut full_command = Vec::with_capacity(1 + args.len());
|
||||
full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string());
|
||||
full_command.append(&mut args);
|
||||
|
||||
@@ -122,6 +122,62 @@ fn unsandboxed_transform_preserves_foreign_cwd_and_unrestricted_file_system_poli
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn symlinked_workspace_reports_seatbelt_preparation_error() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let manager = SandboxManager::new();
|
||||
let temp_dir = TempDir::new().expect("create temp dir");
|
||||
let target = temp_dir.path().join("target");
|
||||
let workspace = temp_dir.path().join("workspace");
|
||||
std::fs::create_dir(&target).expect("create target");
|
||||
symlink(&target, &workspace).expect("create symlinked workspace");
|
||||
let workspace = AbsolutePathBuf::from_absolute_path(workspace).expect("absolute workspace");
|
||||
let workspace_uri = PathUri::from_abs_path(&workspace);
|
||||
let permissions = PermissionProfile::from_runtime_permissions(
|
||||
&FileSystemSandboxPolicy::workspace_write(
|
||||
&[],
|
||||
/*exclude_tmpdir_env_var*/ true,
|
||||
/*exclude_slash_tmp*/ true,
|
||||
),
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
|
||||
let error = manager
|
||||
.transform(SandboxTransformRequest {
|
||||
command: SandboxCommand {
|
||||
program: "true".into(),
|
||||
args: Vec::new(),
|
||||
cwd: workspace_uri.clone(),
|
||||
env: HashMap::new(),
|
||||
managed_network: None,
|
||||
additional_permissions: None,
|
||||
},
|
||||
permissions: &permissions,
|
||||
sandbox: SandboxType::MacosSeatbelt,
|
||||
enforce_managed_network: false,
|
||||
environment_id: None,
|
||||
network: None,
|
||||
sandbox_policy_cwd: &workspace_uri,
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
})
|
||||
.expect_err("symlinked workspace should be rejected");
|
||||
|
||||
assert!(matches!(
|
||||
&error,
|
||||
super::SandboxTransformError::SeatbeltPreparation(message)
|
||||
if message.contains("symlinked writable roots are not supported")
|
||||
));
|
||||
assert!(
|
||||
!error.to_string().contains("network proxy"),
|
||||
"filesystem error should not be attributed to network proxy: {error}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_additional_permissions_enable_network_for_external_sandbox() {
|
||||
let manager = SandboxManager::new();
|
||||
|
||||
@@ -9,6 +9,7 @@ use codex_protocol::permissions::PROTECTED_METADATA_PATH_NAMES;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::WritableRoot;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::canonicalize_preserving_symlinks;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashMap;
|
||||
@@ -32,6 +33,22 @@ pub(crate) enum MacosSeatbeltProfile {
|
||||
FileSystemHelper,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum SeatbeltPreparationError {
|
||||
FileSystem(String),
|
||||
EnvironmentNetworkProxy(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SeatbeltPreparationError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::FileSystem(message) | Self::EnvironmentNetworkProxy(message) => {
|
||||
f.write_str(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// When working with `sandbox-exec`, only consider `sandbox-exec` in `/usr/bin`
|
||||
/// to defend against an attacker trying to inject a malicious version on the
|
||||
/// PATH. If /usr/bin/sandbox-exec has been tampered with, then the attacker
|
||||
@@ -358,19 +375,76 @@ struct SeatbeltAccessRoot {
|
||||
protected_metadata_names: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum SeatbeltAccessKind {
|
||||
Read,
|
||||
Write,
|
||||
}
|
||||
|
||||
fn nested_symlink_component(path: &Path) -> Option<&Path> {
|
||||
// Keep top-level macOS aliases such as `/tmp -> /private/tmp` compatible,
|
||||
// but reject symlinks in user-controlled path components.
|
||||
path.ancestors().find(|ancestor| {
|
||||
let Ok(metadata) = std::fs::symlink_metadata(ancestor) else {
|
||||
return false;
|
||||
};
|
||||
metadata.file_type().is_symlink() && ancestor.parent().and_then(Path::parent).is_some()
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_writable_root_for_sandbox(
|
||||
root: AbsolutePathBuf,
|
||||
) -> Result<AbsolutePathBuf, SeatbeltPreparationError> {
|
||||
if let Some(symlink) = nested_symlink_component(root.as_path()) {
|
||||
return Err(SeatbeltPreparationError::FileSystem(format!(
|
||||
"writable root {} contains symlink component {}; symlinked writable roots are not supported",
|
||||
root.display(),
|
||||
symlink.display()
|
||||
)));
|
||||
}
|
||||
|
||||
let normalized = canonicalize_preserving_symlinks(root.as_path()).map_err(|err| {
|
||||
SeatbeltPreparationError::FileSystem(format!(
|
||||
"failed to normalize writable root {} for Seatbelt: {err}",
|
||||
root.display()
|
||||
))
|
||||
})?;
|
||||
AbsolutePathBuf::from_absolute_path(normalized).map_err(|err| {
|
||||
SeatbeltPreparationError::FileSystem(format!(
|
||||
"failed to normalize writable root {} for Seatbelt: {err}",
|
||||
root.display()
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn build_seatbelt_access_policy(
|
||||
action: &str,
|
||||
param_prefix: &str,
|
||||
access_kind: SeatbeltAccessKind,
|
||||
roots: Vec<SeatbeltAccessRoot>,
|
||||
) -> (String, Vec<(String, PathBuf)>) {
|
||||
) -> Result<(String, Vec<(String, PathBuf)>), SeatbeltPreparationError> {
|
||||
let mut policy_components = Vec::new();
|
||||
let mut root_anchor_denies = Vec::new();
|
||||
let mut params = Vec::new();
|
||||
let (action, param_prefix) = match access_kind {
|
||||
SeatbeltAccessKind::Read => ("file-read*", "READABLE_ROOT"),
|
||||
SeatbeltAccessKind::Write => ("file-write*", "WRITABLE_ROOT"),
|
||||
};
|
||||
|
||||
for (index, access_root) in roots.into_iter().enumerate() {
|
||||
let root =
|
||||
normalize_path_for_sandbox(access_root.root.as_path()).unwrap_or(access_root.root);
|
||||
let root = match access_kind {
|
||||
SeatbeltAccessKind::Read => {
|
||||
normalize_path_for_sandbox(access_root.root.as_path()).unwrap_or(access_root.root)
|
||||
}
|
||||
SeatbeltAccessKind::Write => normalize_writable_root_for_sandbox(access_root.root)?,
|
||||
};
|
||||
let root_param = format!("{param_prefix}_{index}");
|
||||
params.push((root_param.clone(), root.clone().into_path_buf()));
|
||||
if access_kind == SeatbeltAccessKind::Write {
|
||||
// A sandboxed process must not be able to replace an authority
|
||||
// boundary that will be reused to build the next sandbox policy.
|
||||
root_anchor_denies.push(format!(
|
||||
"(deny file-write-unlink (require-all (literal (param \"{root_param}\")) (vnode-type DIRECTORY)))"
|
||||
));
|
||||
}
|
||||
|
||||
if access_root.excluded_subpaths.is_empty()
|
||||
&& access_root.protected_metadata_names.is_empty()
|
||||
@@ -406,12 +480,14 @@ fn build_seatbelt_access_policy(
|
||||
}
|
||||
|
||||
if policy_components.is_empty() {
|
||||
(String::new(), Vec::new())
|
||||
Ok((String::new(), Vec::new()))
|
||||
} else {
|
||||
(
|
||||
format!("(allow {action}\n{}\n)", policy_components.join(" ")),
|
||||
params,
|
||||
)
|
||||
let mut policies = vec![format!(
|
||||
"(allow {action}\n{}\n)",
|
||||
policy_components.join(" ")
|
||||
)];
|
||||
policies.extend(root_anchor_denies);
|
||||
Ok((policies.join("\n"), params))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -633,12 +709,13 @@ pub fn create_seatbelt_command_args(
|
||||
args: CreateSeatbeltCommandArgsParams<'_>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
create_seatbelt_command_args_with_profile(args, MacosSeatbeltProfile::Process)
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn create_seatbelt_command_args_with_profile(
|
||||
args: CreateSeatbeltCommandArgsParams<'_>,
|
||||
profile: MacosSeatbeltProfile,
|
||||
) -> Result<Vec<String>, String> {
|
||||
) -> Result<Vec<String>, SeatbeltPreparationError> {
|
||||
let CreateSeatbeltCommandArgsParams {
|
||||
command,
|
||||
file_system_sandbox_policy,
|
||||
@@ -663,19 +740,17 @@ pub(crate) fn create_seatbelt_command_args_with_profile(
|
||||
)
|
||||
} else {
|
||||
build_seatbelt_access_policy(
|
||||
"file-write*",
|
||||
"WRITABLE_ROOT",
|
||||
SeatbeltAccessKind::Write,
|
||||
vec![SeatbeltAccessRoot {
|
||||
root: root_absolute_path(),
|
||||
excluded_subpaths: unreadable_roots.clone(),
|
||||
protected_metadata_names: Vec::new(),
|
||||
}],
|
||||
)
|
||||
)?
|
||||
}
|
||||
} else {
|
||||
build_seatbelt_access_policy(
|
||||
"file-write*",
|
||||
"WRITABLE_ROOT",
|
||||
SeatbeltAccessKind::Write,
|
||||
file_system_sandbox_policy
|
||||
.get_writable_roots_with_cwd(sandbox_policy_cwd)
|
||||
.into_iter()
|
||||
@@ -689,7 +764,7 @@ pub(crate) fn create_seatbelt_command_args_with_profile(
|
||||
excluded_subpaths: root.read_only_subpaths,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
)?
|
||||
};
|
||||
|
||||
let (file_read_policy, file_read_dir_params) =
|
||||
@@ -701,14 +776,13 @@ pub(crate) fn create_seatbelt_command_args_with_profile(
|
||||
)
|
||||
} else {
|
||||
let (policy, params) = build_seatbelt_access_policy(
|
||||
"file-read*",
|
||||
"READABLE_ROOT",
|
||||
SeatbeltAccessKind::Read,
|
||||
vec![SeatbeltAccessRoot {
|
||||
root: root_absolute_path(),
|
||||
excluded_subpaths: unreadable_roots,
|
||||
protected_metadata_names: Vec::new(),
|
||||
}],
|
||||
);
|
||||
)?;
|
||||
(
|
||||
format!("; allow read-only file operations\n{policy}"),
|
||||
params,
|
||||
@@ -716,8 +790,7 @@ pub(crate) fn create_seatbelt_command_args_with_profile(
|
||||
}
|
||||
} else {
|
||||
let (policy, params) = build_seatbelt_access_policy(
|
||||
"file-read*",
|
||||
"READABLE_ROOT",
|
||||
SeatbeltAccessKind::Read,
|
||||
file_system_sandbox_policy
|
||||
.get_readable_roots_with_cwd(sandbox_policy_cwd)
|
||||
.into_iter()
|
||||
@@ -731,7 +804,7 @@ pub(crate) fn create_seatbelt_command_args_with_profile(
|
||||
root,
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
)?;
|
||||
if policy.is_empty() {
|
||||
(String::new(), params)
|
||||
} else {
|
||||
@@ -747,7 +820,8 @@ pub(crate) fn create_seatbelt_command_args_with_profile(
|
||||
network,
|
||||
environment_id,
|
||||
extra_allow_unix_sockets,
|
||||
)?;
|
||||
)
|
||||
.map_err(SeatbeltPreparationError::EnvironmentNetworkProxy)?;
|
||||
let network_policy =
|
||||
dynamic_network_policy_for_network(network_sandbox_policy, enforce_managed_network, &proxy);
|
||||
|
||||
|
||||
@@ -63,6 +63,25 @@ fn seatbelt_policy_arg(args: &[String]) -> &str {
|
||||
.expect("seatbelt args should include policy text")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn restricted_write_policy(paths: &[&Path]) -> FileSystemSandboxPolicy {
|
||||
let mut entries = vec![FileSystemSandboxEntry::new(
|
||||
FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
FileSystemAccessMode::Read,
|
||||
)];
|
||||
entries.extend(paths.iter().map(|path| {
|
||||
FileSystemSandboxEntry::new(
|
||||
AbsolutePathBuf::from_absolute_path(path)
|
||||
.expect("absolute writable path")
|
||||
.into(),
|
||||
FileSystemAccessMode::Write,
|
||||
)
|
||||
}));
|
||||
FileSystemSandboxPolicy::restricted(entries)
|
||||
}
|
||||
|
||||
fn seatbelt_protected_metadata_name_requirements(root: &Path) -> String {
|
||||
let mut root = root.to_string_lossy().to_string();
|
||||
while root.len() > 1 && root.ends_with('/') {
|
||||
@@ -1201,6 +1220,226 @@ fn create_seatbelt_args_with_read_only_git_and_codex_subpaths() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn create_seatbelt_args_rejects_symlinked_writable_root() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let target = tmp.path().join("target");
|
||||
let workspace = tmp.path().join("workspace");
|
||||
fs::create_dir(&target).expect("create target");
|
||||
symlink(&target, &workspace).expect("create symlinked workspace");
|
||||
let policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
|
||||
let error = create_seatbelt_command_args_for_legacy_policy(
|
||||
vec!["/usr/bin/true".to_string()],
|
||||
&policy,
|
||||
&workspace,
|
||||
/*enforce_managed_network*/ false,
|
||||
/*network*/ None,
|
||||
)
|
||||
.expect_err("symlinked workspace should be rejected");
|
||||
|
||||
assert!(
|
||||
error.contains("symlinked writable roots are not supported"),
|
||||
"unexpected error: {error}"
|
||||
);
|
||||
assert!(
|
||||
error.contains(&workspace.display().to_string()),
|
||||
"error should identify the rejected workspace: {error}"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn seatbelt_prevents_writable_root_replacement() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let workspace = tmp.path().join("workspace");
|
||||
let target = tmp.path().join("target");
|
||||
fs::create_dir(&workspace).expect("create workspace");
|
||||
fs::create_dir(&target).expect("create target");
|
||||
let policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
let shell_command = vec![
|
||||
"/bin/sh".to_string(),
|
||||
"-c".to_string(),
|
||||
"rm -rf \"$PWD\" && ln -s \"$1\" \"$PWD\"".to_string(),
|
||||
"sh".to_string(),
|
||||
target.display().to_string(),
|
||||
];
|
||||
let args = create_seatbelt_command_args_for_legacy_policy(
|
||||
shell_command,
|
||||
&policy,
|
||||
&workspace,
|
||||
/*enforce_managed_network*/ false,
|
||||
/*network*/ None,
|
||||
)
|
||||
.expect("build seatbelt command");
|
||||
|
||||
let policy_text = seatbelt_policy_arg(&args);
|
||||
assert!(
|
||||
policy_text.contains(
|
||||
"(deny file-write-unlink (require-all (literal (param \"WRITABLE_ROOT_0\")) (vnode-type DIRECTORY)))"
|
||||
),
|
||||
"expected writable-root anchor protection in policy:\n{policy_text}"
|
||||
);
|
||||
|
||||
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
|
||||
.args(&args)
|
||||
.current_dir(&workspace)
|
||||
.output()
|
||||
.expect("execute seatbelt command");
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
let workspace_metadata = fs::symlink_metadata(&workspace).expect("workspace should remain");
|
||||
assert!(
|
||||
workspace_metadata.is_dir() && !workspace_metadata.file_type().is_symlink(),
|
||||
"sandboxed command replaced {}: {stderr}",
|
||||
workspace.display()
|
||||
);
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"workspace replacement should fail under Seatbelt"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn seatbelt_allows_file_root_replacement_and_deletion() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let target = tmp.path().join("target.txt");
|
||||
let replacement = tmp.path().join("replacement.txt");
|
||||
fs::write(&target, "before").expect("write target");
|
||||
fs::write(&replacement, "after").expect("write replacement");
|
||||
let policy = restricted_write_policy(&[target.as_path(), replacement.as_path()]);
|
||||
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
|
||||
command: vec![
|
||||
"/bin/sh".to_string(),
|
||||
"-c".to_string(),
|
||||
"mv \"$1\" \"$2\" && test \"$(cat \"$2\")\" = after && rm \"$2\"".to_string(),
|
||||
"sh".to_string(),
|
||||
replacement.display().to_string(),
|
||||
target.display().to_string(),
|
||||
],
|
||||
file_system_sandbox_policy: &policy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
sandbox_policy_cwd: tmp.path(),
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
environment_id: None,
|
||||
network: None,
|
||||
extra_allow_unix_sockets: &[],
|
||||
})
|
||||
.expect("build seatbelt command");
|
||||
|
||||
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
|
||||
.args(&args)
|
||||
.current_dir(tmp.path())
|
||||
.output()
|
||||
.expect("execute seatbelt command");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"file replacement and deletion should succeed under Seatbelt: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert!(!target.exists(), "target should have been deleted");
|
||||
assert!(!replacement.exists(), "replacement should have been moved");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn seatbelt_prevents_writable_directory_root_rename() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let source = tmp.path().join("source");
|
||||
let destination = tmp.path().join("destination");
|
||||
fs::create_dir(&source).expect("create source");
|
||||
let policy = restricted_write_policy(&[source.as_path(), destination.as_path()]);
|
||||
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
|
||||
command: vec![
|
||||
"/bin/mv".to_string(),
|
||||
source.display().to_string(),
|
||||
destination.display().to_string(),
|
||||
],
|
||||
file_system_sandbox_policy: &policy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
sandbox_policy_cwd: tmp.path(),
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
environment_id: None,
|
||||
network: None,
|
||||
extra_allow_unix_sockets: &[],
|
||||
})
|
||||
.expect("build seatbelt command");
|
||||
|
||||
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
|
||||
.args(&args)
|
||||
.current_dir(tmp.path())
|
||||
.output()
|
||||
.expect("execute seatbelt command");
|
||||
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"directory-root rename should fail under Seatbelt"
|
||||
);
|
||||
assert!(source.is_dir(), "source directory should remain in place");
|
||||
assert!(
|
||||
!destination.exists(),
|
||||
"destination should not have been created"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn seatbelt_protects_writable_root_created_as_directory() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let writable_root = tmp.path().join("writable-root");
|
||||
let policy = restricted_write_policy(&[writable_root.as_path()]);
|
||||
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
|
||||
command: vec![
|
||||
"/bin/sh".to_string(),
|
||||
"-c".to_string(),
|
||||
"mkdir \"$1\" && rmdir \"$1\"".to_string(),
|
||||
"sh".to_string(),
|
||||
writable_root.display().to_string(),
|
||||
],
|
||||
file_system_sandbox_policy: &policy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
sandbox_policy_cwd: tmp.path(),
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
environment_id: None,
|
||||
network: None,
|
||||
extra_allow_unix_sockets: &[],
|
||||
})
|
||||
.expect("build seatbelt command");
|
||||
|
||||
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
|
||||
.args(&args)
|
||||
.current_dir(tmp.path())
|
||||
.output()
|
||||
.expect("execute seatbelt command");
|
||||
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"removing a newly created directory root should fail under Seatbelt"
|
||||
);
|
||||
assert!(
|
||||
writable_root.is_dir(),
|
||||
"newly created directory root should remain protected"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_seatbelt_args_block_first_time_dot_codex_creation_with_metadata_name_regex() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
|
||||
Reference in New Issue
Block a user