mirror of
https://github.com/openai/codex.git
synced 2026-06-02 11:22:01 +00:00
Compare commits
4 Commits
dev/winsto
...
codex/viya
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ab25cb2c5 | ||
|
|
0dc8cf86c9 | ||
|
|
4418bce894 | ||
|
|
e7864acdbe |
@@ -17,6 +17,10 @@ use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
|
||||
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_sandboxing::EffectiveFilesystemPermissions;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_sandboxing::FilesystemPermissionsContext;
|
||||
use codex_sandboxing::landlock::allow_network_for_proxy;
|
||||
use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_permission_profile;
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -260,13 +264,21 @@ async fn run_command_under_sandbox(
|
||||
let mut child = match sandbox_type {
|
||||
#[cfg(target_os = "macos")]
|
||||
SandboxType::Seatbelt => {
|
||||
let file_system_sandbox_policy = config.permissions.file_system_sandbox_policy();
|
||||
let network_sandbox_policy = config.permissions.network_sandbox_policy();
|
||||
let permission_profile = config.permissions.effective_permission_profile();
|
||||
let network_sandbox_policy = permission_profile.network_sandbox_policy();
|
||||
let effective_filesystem_permissions = EffectiveFilesystemPermissions::from_profile(
|
||||
&permission_profile,
|
||||
FilesystemPermissionsContext {
|
||||
policy_evaluation_cwd: &permission_profile_cwd,
|
||||
},
|
||||
)
|
||||
.map_err(|err| {
|
||||
anyhow::anyhow!("failed to derive effective filesystem permissions: {err}")
|
||||
})?;
|
||||
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
|
||||
command,
|
||||
file_system_sandbox_policy: &file_system_sandbox_policy,
|
||||
effective_filesystem_permissions: &effective_filesystem_permissions,
|
||||
network_sandbox_policy,
|
||||
sandbox_policy_cwd: permission_profile_cwd.as_path(),
|
||||
enforce_managed_network: false,
|
||||
network: network.as_ref(),
|
||||
extra_allow_unix_sockets: allow_unix_sockets,
|
||||
|
||||
@@ -122,6 +122,7 @@ impl ExecRequest {
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
permission_profile,
|
||||
effective_filesystem_permissions: _,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
arg0,
|
||||
|
||||
@@ -46,7 +46,9 @@ use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::FileChange;
|
||||
use codex_protocol::protocol::PatchApplyUpdatedEvent;
|
||||
use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy;
|
||||
use codex_sandboxing::EffectiveFilesystemPermissions;
|
||||
use codex_sandboxing::FilesystemPermissionsContext;
|
||||
use codex_sandboxing::policy_transforms::effective_permission_profile;
|
||||
use codex_sandboxing::policy_transforms::merge_permission_profiles;
|
||||
use codex_sandboxing::policy_transforms::normalize_additional_permissions;
|
||||
use codex_tools::ToolName;
|
||||
@@ -224,8 +226,7 @@ fn to_abs_path(cwd: &AbsolutePathBuf, path: &Path) -> Option<AbsolutePathBuf> {
|
||||
|
||||
fn write_permissions_for_paths(
|
||||
file_paths: &[AbsolutePathBuf],
|
||||
file_system_sandbox_policy: &codex_protocol::permissions::FileSystemSandboxPolicy,
|
||||
cwd: &AbsolutePathBuf,
|
||||
effective_filesystem_permissions: &EffectiveFilesystemPermissions,
|
||||
) -> Option<AdditionalPermissionProfile> {
|
||||
let write_paths = file_paths
|
||||
.iter()
|
||||
@@ -234,9 +235,7 @@ fn write_permissions_for_paths(
|
||||
.unwrap_or_else(|| path.clone())
|
||||
.into_path_buf()
|
||||
})
|
||||
.filter(|path| {
|
||||
!file_system_sandbox_policy.can_write_path_with_cwd(path.as_path(), cwd.as_path())
|
||||
})
|
||||
.filter(|path| !effective_filesystem_permissions.can_write(path.as_path()))
|
||||
.collect::<BTreeSet<_>>()
|
||||
.into_iter()
|
||||
.map(AbsolutePathBuf::from_absolute_path)
|
||||
@@ -267,34 +266,46 @@ async fn effective_patch_permissions(
|
||||
turn: &TurnContext,
|
||||
action: &ApplyPatchAction,
|
||||
cwd: &AbsolutePathBuf,
|
||||
) -> (
|
||||
Vec<AbsolutePathBuf>,
|
||||
crate::tools::handlers::EffectiveAdditionalPermissions,
|
||||
codex_protocol::permissions::FileSystemSandboxPolicy,
|
||||
) {
|
||||
) -> Result<
|
||||
(
|
||||
Vec<AbsolutePathBuf>,
|
||||
crate::tools::handlers::EffectiveAdditionalPermissions,
|
||||
codex_protocol::permissions::FileSystemSandboxPolicy,
|
||||
),
|
||||
FunctionCallError,
|
||||
> {
|
||||
let file_paths = file_paths_for_action(action);
|
||||
let granted_permissions = merge_permission_profiles(
|
||||
session.granted_session_permissions().await.as_ref(),
|
||||
session.granted_turn_permissions().await.as_ref(),
|
||||
);
|
||||
let base_file_system_sandbox_policy = turn.file_system_sandbox_policy();
|
||||
let file_system_sandbox_policy = effective_file_system_sandbox_policy(
|
||||
&base_file_system_sandbox_policy,
|
||||
granted_permissions.as_ref(),
|
||||
);
|
||||
let effective_permission_profile =
|
||||
effective_permission_profile(&turn.permission_profile(), granted_permissions.as_ref());
|
||||
let file_system_sandbox_policy = effective_permission_profile.file_system_sandbox_policy();
|
||||
let effective_filesystem_permissions = EffectiveFilesystemPermissions::from_profile(
|
||||
&effective_permission_profile,
|
||||
FilesystemPermissionsContext {
|
||||
policy_evaluation_cwd: cwd,
|
||||
},
|
||||
)
|
||||
.map_err(|err| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"failed to derive effective filesystem permissions for apply_patch: {err}"
|
||||
))
|
||||
})?;
|
||||
let effective_additional_permissions = apply_granted_turn_permissions(
|
||||
session,
|
||||
cwd.as_path(),
|
||||
crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
write_permissions_for_paths(&file_paths, &file_system_sandbox_policy, cwd),
|
||||
write_permissions_for_paths(&file_paths, &effective_filesystem_permissions),
|
||||
)
|
||||
.await;
|
||||
|
||||
(
|
||||
Ok((
|
||||
file_paths,
|
||||
effective_additional_permissions,
|
||||
file_system_sandbox_policy,
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -354,7 +365,7 @@ impl ToolExecutor<ToolInvocation> for ApplyPatchHandler {
|
||||
codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => {
|
||||
let (file_paths, effective_additional_permissions, file_system_sandbox_policy) =
|
||||
effective_patch_permissions(session.as_ref(), turn.as_ref(), &changes, &cwd)
|
||||
.await;
|
||||
.await?;
|
||||
match apply_patch::apply_patch(turn.as_ref(), &file_system_sandbox_policy, changes)
|
||||
.await
|
||||
{
|
||||
@@ -506,7 +517,7 @@ pub(crate) async fn intercept_apply_patch(
|
||||
{
|
||||
codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => {
|
||||
let (approval_keys, effective_additional_permissions, file_system_sandbox_policy) =
|
||||
effective_patch_permissions(session.as_ref(), turn.as_ref(), &changes, cwd).await;
|
||||
effective_patch_permissions(session.as_ref(), turn.as_ref(), &changes, cwd).await?;
|
||||
match apply_patch::apply_patch(turn.as_ref(), &file_system_sandbox_policy, changes)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use super::*;
|
||||
use codex_apply_patch::MaybeApplyPatchVerified;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::FileChange;
|
||||
use core_test_support::PathBufExt;
|
||||
use core_test_support::PathExt;
|
||||
@@ -27,6 +29,24 @@ fn sample_patch() -> &'static str {
|
||||
*** End Patch"#
|
||||
}
|
||||
|
||||
fn effective_permissions(
|
||||
sandbox_policy: &FileSystemSandboxPolicy,
|
||||
cwd: &AbsolutePathBuf,
|
||||
) -> codex_sandboxing::EffectiveFilesystemPermissions {
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
sandbox_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
)
|
||||
.materialize_project_roots_with_workspace_roots(std::slice::from_ref(cwd));
|
||||
codex_sandboxing::EffectiveFilesystemPermissions::from_profile(
|
||||
&permission_profile,
|
||||
codex_sandboxing::FilesystemPermissionsContext {
|
||||
policy_evaluation_cwd: cwd,
|
||||
},
|
||||
)
|
||||
.expect("derive effective filesystem permissions")
|
||||
}
|
||||
|
||||
async fn invocation_for_payload(payload: ToolPayload) -> ToolInvocation {
|
||||
let (session, turn) = make_session_and_context().await;
|
||||
ToolInvocation {
|
||||
@@ -255,8 +275,9 @@ fn write_permissions_for_paths_skip_dirs_already_writable_under_workspace_root()
|
||||
/*exclude_tmpdir_env_var*/ true,
|
||||
/*exclude_slash_tmp*/ false,
|
||||
);
|
||||
let effective_permissions = effective_permissions(&sandbox_policy, &cwd);
|
||||
|
||||
let permissions = write_permissions_for_paths(&[file_path], &sandbox_policy, &cwd);
|
||||
let permissions = write_permissions_for_paths(&[file_path], &effective_permissions);
|
||||
|
||||
assert_eq!(permissions, None);
|
||||
}
|
||||
@@ -276,8 +297,9 @@ fn write_permissions_for_paths_keep_dirs_outside_workspace_root() {
|
||||
/*exclude_tmpdir_env_var*/ true,
|
||||
/*exclude_slash_tmp*/ true,
|
||||
);
|
||||
let effective_permissions = effective_permissions(&sandbox_policy, &cwd_abs);
|
||||
|
||||
let permissions = write_permissions_for_paths(&[file_path], &sandbox_policy, &cwd_abs);
|
||||
let permissions = write_permissions_for_paths(&[file_path], &effective_permissions);
|
||||
let expected_outside =
|
||||
dunce::simplified(&outside.canonicalize().expect("canonicalize outside dir")).abs();
|
||||
|
||||
|
||||
@@ -27,11 +27,8 @@ use std::process::Command;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result;
|
||||
use codex_protocol::permissions::is_protected_metadata_name;
|
||||
use codex_protocol::protocol::FileSystemAccessMode;
|
||||
use codex_protocol::protocol::FileSystemPath;
|
||||
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
||||
use codex_protocol::protocol::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::WritableRoot;
|
||||
use codex_sandboxing::EffectiveFilesystemPermissions;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use globset::GlobBuilder;
|
||||
use globset::GlobSet;
|
||||
@@ -233,16 +230,16 @@ impl SyntheticMountTarget {
|
||||
/// namespace restrictions apply while preserving full filesystem access.
|
||||
pub(crate) fn create_bwrap_command_args(
|
||||
command: Vec<String>,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
effective_filesystem_permissions: &EffectiveFilesystemPermissions,
|
||||
sandbox_policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
options: BwrapOptions,
|
||||
) -> Result<BwrapArgs> {
|
||||
let unreadable_globs =
|
||||
file_system_sandbox_policy.get_unreadable_globs_with_cwd(sandbox_policy_cwd);
|
||||
// Full disk write normally skips bwrap, but unreadable glob patterns still
|
||||
// need concrete bwrap masks for the matches expanded below.
|
||||
if file_system_sandbox_policy.has_full_disk_write_access() && unreadable_globs.is_empty() {
|
||||
if effective_filesystem_permissions.has_full_disk_write_access()
|
||||
&& effective_filesystem_permissions.unreadable_globs.is_empty()
|
||||
{
|
||||
return if options.network_mode == BwrapNetworkMode::FullAccess {
|
||||
Ok(BwrapArgs {
|
||||
args: command,
|
||||
@@ -257,7 +254,7 @@ pub(crate) fn create_bwrap_command_args(
|
||||
|
||||
create_bwrap_flags(
|
||||
command,
|
||||
file_system_sandbox_policy,
|
||||
effective_filesystem_permissions,
|
||||
sandbox_policy_cwd,
|
||||
command_cwd,
|
||||
options,
|
||||
@@ -296,7 +293,7 @@ fn create_bwrap_flags_full_filesystem(command: Vec<String>, options: BwrapOption
|
||||
/// Build the bubblewrap flags (everything after `argv[0]`).
|
||||
fn create_bwrap_flags(
|
||||
command: Vec<String>,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
effective_filesystem_permissions: &EffectiveFilesystemPermissions,
|
||||
sandbox_policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
options: BwrapOptions,
|
||||
@@ -306,12 +303,12 @@ fn create_bwrap_flags(
|
||||
preserved_files,
|
||||
synthetic_mount_targets,
|
||||
protected_create_targets,
|
||||
} = create_filesystem_args(
|
||||
file_system_sandbox_policy,
|
||||
} = create_effective_filesystem_args(
|
||||
effective_filesystem_permissions,
|
||||
sandbox_policy_cwd,
|
||||
options
|
||||
.glob_scan_max_depth
|
||||
.or(file_system_sandbox_policy.glob_scan_max_depth),
|
||||
.or(effective_filesystem_permissions.glob_scan_max_depth),
|
||||
)?;
|
||||
let normalized_command_cwd = normalize_command_cwd_for_bwrap(command_cwd);
|
||||
let mut args = Vec::new();
|
||||
@@ -364,22 +361,27 @@ fn create_bwrap_flags(
|
||||
/// those writable roots so protected subpaths win.
|
||||
/// 6. Nested unreadable carveouts under a writable root are masked after that
|
||||
/// root is bound, and unrelated unreadable roots are masked afterward.
|
||||
fn create_filesystem_args(
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
fn create_effective_filesystem_args(
|
||||
effective_filesystem_permissions: &EffectiveFilesystemPermissions,
|
||||
cwd: &Path,
|
||||
glob_scan_max_depth: Option<usize>,
|
||||
) -> Result<BwrapArgs> {
|
||||
let unreadable_globs = file_system_sandbox_policy.get_unreadable_globs_with_cwd(cwd);
|
||||
let unreadable_globs = effective_filesystem_permissions
|
||||
.unreadable_globs
|
||||
.iter()
|
||||
.map(|glob| glob.pattern().to_string())
|
||||
.collect::<Vec<_>>();
|
||||
// Bubblewrap requires bind mount targets to exist. Skip missing writable
|
||||
// roots so mixed-platform configs can keep harmless paths for other
|
||||
// environments without breaking Linux command startup.
|
||||
let mut writable_roots = file_system_sandbox_policy
|
||||
.get_writable_roots_with_cwd(cwd)
|
||||
.into_iter()
|
||||
let mut writable_roots = effective_filesystem_permissions
|
||||
.writable_roots
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter(|writable_root| writable_root.root.as_path().exists())
|
||||
.collect::<Vec<_>>();
|
||||
if writable_roots.is_empty()
|
||||
&& file_system_sandbox_policy.has_full_disk_write_access()
|
||||
&& effective_filesystem_permissions.has_full_disk_write_access()
|
||||
&& !unreadable_globs.is_empty()
|
||||
{
|
||||
writable_roots.push(WritableRoot {
|
||||
@@ -388,42 +390,10 @@ fn create_filesystem_args(
|
||||
protected_metadata_names: Vec::new(),
|
||||
});
|
||||
}
|
||||
let missing_auto_metadata_read_only_project_root_subpaths: HashSet<PathBuf> =
|
||||
file_system_sandbox_policy
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|entry| entry.access == FileSystemAccessMode::Read)
|
||||
.filter_map(|entry| {
|
||||
let FileSystemPath::Special {
|
||||
value:
|
||||
FileSystemSpecialPath::ProjectRoots {
|
||||
subpath: Some(subpath),
|
||||
},
|
||||
} = &entry.path
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
// Automatic repo-metadata read masks are skipped here so the
|
||||
// metadata handling below can apply the root-scoped
|
||||
// protection consistently for `.git`, `.agents`, and `.codex`.
|
||||
// User-authored `read` rules for other subpaths and `none`
|
||||
// rules should keep their normal bwrap behavior, which can mask
|
||||
// the first missing component to prevent creation under writable
|
||||
// roots.
|
||||
let project_subpath = subpath.as_path();
|
||||
if project_subpath != Path::new(".git")
|
||||
&& project_subpath != Path::new(".agents")
|
||||
&& project_subpath != Path::new(".codex")
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let resolved = AbsolutePathBuf::resolve_path_against_base(subpath, cwd);
|
||||
(!resolved.as_path().exists()).then(|| resolved.into_path_buf())
|
||||
})
|
||||
.collect();
|
||||
let mut unreadable_roots = file_system_sandbox_policy
|
||||
.get_unreadable_roots_with_cwd(cwd)
|
||||
.into_iter()
|
||||
let mut unreadable_roots = effective_filesystem_permissions
|
||||
.unreadable_roots
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(AbsolutePathBuf::into_path_buf)
|
||||
.collect::<Vec<_>>();
|
||||
// Bubblewrap can only mask concrete paths. Expand unreadable glob patterns
|
||||
@@ -437,7 +407,7 @@ fn create_filesystem_args(
|
||||
unreadable_roots.sort();
|
||||
unreadable_roots.dedup();
|
||||
|
||||
let args = if file_system_sandbox_policy.has_full_disk_read_access() {
|
||||
let args = if effective_filesystem_permissions.has_full_disk_read_access() {
|
||||
// Read-only root, then mount a minimal device tree.
|
||||
// In bubblewrap (`bubblewrap.c`, `SETUP_MOUNT_DEV`), `--dev /dev`
|
||||
// creates the standard minimal nodes: null, zero, full, random,
|
||||
@@ -460,12 +430,13 @@ fn create_filesystem_args(
|
||||
"/dev".to_string(),
|
||||
];
|
||||
|
||||
let mut readable_roots: BTreeSet<PathBuf> = file_system_sandbox_policy
|
||||
.get_readable_roots_with_cwd(cwd)
|
||||
.into_iter()
|
||||
let mut readable_roots: BTreeSet<PathBuf> = effective_filesystem_permissions
|
||||
.readable_roots
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(PathBuf::from)
|
||||
.collect();
|
||||
if file_system_sandbox_policy.include_platform_defaults() {
|
||||
if effective_filesystem_permissions.include_platform_defaults {
|
||||
readable_roots.extend(
|
||||
LINUX_PLATFORM_DEFAULT_READ_ROOTS
|
||||
.iter()
|
||||
@@ -573,7 +544,6 @@ fn create_filesystem_args(
|
||||
.iter()
|
||||
.map(|path| path.as_path().to_path_buf())
|
||||
.filter(|path| !unreadable_paths.contains(path))
|
||||
.filter(|path| !missing_auto_metadata_read_only_project_root_subpaths.contains(path))
|
||||
.collect();
|
||||
let protected_metadata_names = writable_root.protected_metadata_names.clone();
|
||||
append_metadata_path_masks_for_writable_root(
|
||||
@@ -1324,6 +1294,61 @@ fn find_first_non_existent_component(target_path: &Path) -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn effective_test_filesystem_permissions(
|
||||
file_system_sandbox_policy: &codex_protocol::protocol::FileSystemSandboxPolicy,
|
||||
cwd: &Path,
|
||||
) -> EffectiveFilesystemPermissions {
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(cwd)
|
||||
.unwrap_or_else(|err| panic!("test policy cwd should be absolute: {err}"));
|
||||
let file_system_sandbox_policy = file_system_sandbox_policy
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(std::slice::from_ref(&cwd));
|
||||
let permission_profile = codex_protocol::models::PermissionProfile::from_runtime_permissions(
|
||||
&file_system_sandbox_policy,
|
||||
codex_protocol::protocol::NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
codex_sandboxing::EffectiveFilesystemPermissions::from_profile(
|
||||
&permission_profile,
|
||||
codex_sandboxing::FilesystemPermissionsContext {
|
||||
policy_evaluation_cwd: &cwd,
|
||||
},
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
panic!("test filesystem policy should yield effective permissions: {err}")
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn create_filesystem_args(
|
||||
file_system_sandbox_policy: &codex_protocol::protocol::FileSystemSandboxPolicy,
|
||||
cwd: &Path,
|
||||
glob_scan_max_depth: Option<usize>,
|
||||
) -> Result<BwrapArgs> {
|
||||
let effective_filesystem_permissions =
|
||||
effective_test_filesystem_permissions(file_system_sandbox_policy, cwd);
|
||||
create_effective_filesystem_args(&effective_filesystem_permissions, cwd, glob_scan_max_depth)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn create_bwrap_command_args_for_policy(
|
||||
command: Vec<String>,
|
||||
file_system_sandbox_policy: &codex_protocol::protocol::FileSystemSandboxPolicy,
|
||||
sandbox_policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
options: BwrapOptions,
|
||||
) -> Result<BwrapArgs> {
|
||||
let effective_filesystem_permissions =
|
||||
effective_test_filesystem_permissions(file_system_sandbox_policy, sandbox_policy_cwd);
|
||||
create_bwrap_command_args(
|
||||
command,
|
||||
&effective_filesystem_permissions,
|
||||
sandbox_policy_cwd,
|
||||
command_cwd,
|
||||
options,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1360,7 +1385,7 @@ mod tests {
|
||||
#[test]
|
||||
fn full_disk_write_full_network_returns_unwrapped_command() {
|
||||
let command = vec!["/bin/true".to_string()];
|
||||
let args = create_bwrap_command_args(
|
||||
let args = create_bwrap_command_args_for_policy(
|
||||
command.clone(),
|
||||
&FileSystemSandboxPolicy::unrestricted(),
|
||||
Path::new("/"),
|
||||
@@ -1379,7 +1404,7 @@ mod tests {
|
||||
#[test]
|
||||
fn full_disk_write_proxy_only_keeps_full_filesystem_but_unshares_network() {
|
||||
let command = vec!["/bin/true".to_string()];
|
||||
let args = create_bwrap_command_args(
|
||||
let args = create_bwrap_command_args_for_policy(
|
||||
command,
|
||||
&FileSystemSandboxPolicy::unrestricted(),
|
||||
Path::new("/"),
|
||||
@@ -1431,7 +1456,7 @@ mod tests {
|
||||
]);
|
||||
let command = vec!["/bin/true".to_string()];
|
||||
|
||||
let args = create_bwrap_command_args(
|
||||
let args = create_bwrap_command_args_for_policy(
|
||||
command.clone(),
|
||||
&policy,
|
||||
temp_dir.path(),
|
||||
@@ -1479,7 +1504,7 @@ mod tests {
|
||||
},
|
||||
]);
|
||||
|
||||
let args = create_bwrap_command_args(
|
||||
let args = create_bwrap_command_args_for_policy(
|
||||
vec!["/bin/true".to_string()],
|
||||
&policy,
|
||||
sandbox_policy_cwd.as_path(),
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
//! Filesystem restrictions are enforced by bubblewrap in `linux_run_main`.
|
||||
//! Landlock helpers remain available here as legacy/backup utilities.
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result;
|
||||
use codex_protocol::error::SandboxErr;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::NetworkSandboxPolicy;
|
||||
use codex_sandboxing::EffectiveFilesystemPermissions;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
use landlock::ABI;
|
||||
@@ -39,15 +38,13 @@ use seccompiler::apply_filter;
|
||||
/// - installing the network seccomp filter when network access is disabled.
|
||||
///
|
||||
/// Filesystem restrictions are intentionally handled by bubblewrap.
|
||||
pub(crate) fn apply_permission_profile_to_current_thread(
|
||||
permission_profile: &PermissionProfile,
|
||||
cwd: &Path,
|
||||
pub(crate) fn apply_effective_permissions_to_current_thread(
|
||||
effective_filesystem_permissions: &EffectiveFilesystemPermissions,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
apply_landlock_fs: bool,
|
||||
allow_network_for_proxy: bool,
|
||||
proxy_routed_network: bool,
|
||||
) -> Result<()> {
|
||||
let (file_system_sandbox_policy, network_sandbox_policy) =
|
||||
permission_profile.to_runtime_permissions();
|
||||
let network_seccomp_mode = network_seccomp_mode(
|
||||
network_sandbox_policy,
|
||||
allow_network_for_proxy,
|
||||
@@ -59,7 +56,7 @@ pub(crate) fn apply_permission_profile_to_current_thread(
|
||||
// we avoid this unless we need seccomp or we are explicitly using the
|
||||
// legacy Landlock filesystem pipeline.
|
||||
if network_seccomp_mode.is_some()
|
||||
|| (apply_landlock_fs && !file_system_sandbox_policy.has_full_disk_write_access())
|
||||
|| (apply_landlock_fs && !effective_filesystem_permissions.has_full_disk_write_access())
|
||||
{
|
||||
set_no_new_privs()?;
|
||||
}
|
||||
@@ -68,18 +65,18 @@ pub(crate) fn apply_permission_profile_to_current_thread(
|
||||
install_network_seccomp_filter_on_current_thread(mode)?;
|
||||
}
|
||||
|
||||
if apply_landlock_fs && !file_system_sandbox_policy.has_full_disk_write_access() {
|
||||
if !file_system_sandbox_policy.has_full_disk_read_access() {
|
||||
if apply_landlock_fs && !effective_filesystem_permissions.has_full_disk_write_access() {
|
||||
if !effective_filesystem_permissions.has_full_disk_read_access() {
|
||||
return Err(CodexErr::UnsupportedOperation(
|
||||
"Restricted read-only access is not supported by the legacy Linux Landlock filesystem backend."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let writable_roots = file_system_sandbox_policy
|
||||
.get_writable_roots_with_cwd(cwd)
|
||||
.into_iter()
|
||||
.map(|writable_root| writable_root.root)
|
||||
let writable_roots = effective_filesystem_permissions
|
||||
.writable_roots
|
||||
.iter()
|
||||
.map(|writable_root| writable_root.root.clone())
|
||||
.collect();
|
||||
install_filesystem_landlock_rules_on_current_thread(writable_roots)?;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ use std::time::Duration;
|
||||
use crate::bwrap::BwrapNetworkMode;
|
||||
use crate::bwrap::BwrapOptions;
|
||||
use crate::bwrap::create_bwrap_command_args;
|
||||
use crate::landlock::apply_permission_profile_to_current_thread;
|
||||
use crate::landlock::apply_effective_permissions_to_current_thread;
|
||||
use crate::launcher::exec_bwrap;
|
||||
use crate::launcher::preferred_bwrap_supports_argv0;
|
||||
use crate::proxy_routing::activate_proxy_routes_in_netns;
|
||||
@@ -29,7 +29,10 @@ use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
||||
use codex_protocol::protocol::NetworkSandboxPolicy;
|
||||
use codex_sandboxing::EffectiveFilesystemPermissions;
|
||||
use codex_sandboxing::FilesystemPermissionsContext;
|
||||
use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
static BWRAP_CHILD_PID: AtomicI32 = AtomicI32::new(0);
|
||||
static PENDING_FORWARDED_SIGNAL: AtomicI32 = AtomicI32::new(0);
|
||||
@@ -166,6 +169,15 @@ pub fn run_main() -> ! {
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
} = resolve_permission_profile(permission_profile).unwrap_or_else(|err| panic!("{err}"));
|
||||
let policy_evaluation_cwd = AbsolutePathBuf::from_absolute_path(&sandbox_policy_cwd)
|
||||
.unwrap_or_else(|err| panic!("sandbox policy cwd should be absolute: {err}"));
|
||||
let effective_filesystem_permissions = EffectiveFilesystemPermissions::from_profile(
|
||||
&permission_profile,
|
||||
FilesystemPermissionsContext {
|
||||
policy_evaluation_cwd: &policy_evaluation_cwd,
|
||||
},
|
||||
)
|
||||
.unwrap_or_else(|err| panic!("failed to derive effective filesystem permissions: {err}"));
|
||||
ensure_legacy_landlock_mode_supports_policy(
|
||||
use_legacy_landlock,
|
||||
&file_system_sandbox_policy,
|
||||
@@ -185,9 +197,9 @@ pub fn run_main() -> ! {
|
||||
}
|
||||
}
|
||||
let proxy_routing_active = allow_network_for_proxy;
|
||||
if let Err(e) = apply_permission_profile_to_current_thread(
|
||||
&permission_profile,
|
||||
&sandbox_policy_cwd,
|
||||
if let Err(e) = apply_effective_permissions_to_current_thread(
|
||||
&effective_filesystem_permissions,
|
||||
network_sandbox_policy,
|
||||
/*apply_landlock_fs*/ false,
|
||||
allow_network_for_proxy,
|
||||
proxy_routing_active,
|
||||
@@ -197,10 +209,10 @@ pub fn run_main() -> ! {
|
||||
exec_or_panic(command);
|
||||
}
|
||||
|
||||
if file_system_sandbox_policy.has_full_disk_write_access() && !allow_network_for_proxy {
|
||||
if let Err(e) = apply_permission_profile_to_current_thread(
|
||||
&permission_profile,
|
||||
&sandbox_policy_cwd,
|
||||
if effective_filesystem_permissions.has_full_disk_write_access() && !allow_network_for_proxy {
|
||||
if let Err(e) = apply_effective_permissions_to_current_thread(
|
||||
&effective_filesystem_permissions,
|
||||
network_sandbox_policy,
|
||||
/*apply_landlock_fs*/ false,
|
||||
allow_network_for_proxy,
|
||||
/*proxy_routed_network*/ false,
|
||||
@@ -233,7 +245,7 @@ pub fn run_main() -> ! {
|
||||
run_bwrap_with_proc_fallback(
|
||||
&sandbox_policy_cwd,
|
||||
command_cwd.as_deref(),
|
||||
&file_system_sandbox_policy,
|
||||
&effective_filesystem_permissions,
|
||||
network_sandbox_policy,
|
||||
inner,
|
||||
!no_proc,
|
||||
@@ -242,9 +254,9 @@ pub fn run_main() -> ! {
|
||||
}
|
||||
|
||||
// Legacy path: Landlock enforcement only, when bwrap sandboxing is not enabled.
|
||||
if let Err(e) = apply_permission_profile_to_current_thread(
|
||||
&permission_profile,
|
||||
&sandbox_policy_cwd,
|
||||
if let Err(e) = apply_effective_permissions_to_current_thread(
|
||||
&effective_filesystem_permissions,
|
||||
network_sandbox_policy,
|
||||
/*apply_landlock_fs*/ true,
|
||||
allow_network_for_proxy,
|
||||
/*proxy_routed_network*/ false,
|
||||
@@ -317,7 +329,7 @@ fn ensure_legacy_landlock_mode_supports_policy(
|
||||
fn run_bwrap_with_proc_fallback(
|
||||
sandbox_policy_cwd: &Path,
|
||||
command_cwd: Option<&Path>,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
effective_filesystem_permissions: &EffectiveFilesystemPermissions,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
inner: Vec<String>,
|
||||
mount_proc: bool,
|
||||
@@ -331,7 +343,7 @@ fn run_bwrap_with_proc_fallback(
|
||||
&& !preflight_proc_mount_support(
|
||||
sandbox_policy_cwd,
|
||||
command_cwd,
|
||||
file_system_sandbox_policy,
|
||||
effective_filesystem_permissions,
|
||||
network_mode,
|
||||
)
|
||||
.unwrap_or_else(|err| exit_with_bwrap_build_error(err))
|
||||
@@ -348,7 +360,7 @@ fn run_bwrap_with_proc_fallback(
|
||||
};
|
||||
let mut bwrap_args = build_bwrap_argv(
|
||||
inner,
|
||||
file_system_sandbox_policy,
|
||||
effective_filesystem_permissions,
|
||||
sandbox_policy_cwd,
|
||||
command_cwd,
|
||||
options,
|
||||
@@ -373,14 +385,14 @@ fn bwrap_network_mode(
|
||||
|
||||
fn build_bwrap_argv(
|
||||
inner: Vec<String>,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
effective_filesystem_permissions: &EffectiveFilesystemPermissions,
|
||||
sandbox_policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
options: BwrapOptions,
|
||||
) -> CodexResult<crate::bwrap::BwrapArgs> {
|
||||
let bwrap_args = create_bwrap_command_args(
|
||||
inner,
|
||||
file_system_sandbox_policy,
|
||||
effective_filesystem_permissions,
|
||||
sandbox_policy_cwd,
|
||||
command_cwd,
|
||||
options,
|
||||
@@ -444,13 +456,13 @@ fn current_process_argv0() -> String {
|
||||
fn preflight_proc_mount_support(
|
||||
sandbox_policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
effective_filesystem_permissions: &EffectiveFilesystemPermissions,
|
||||
network_mode: BwrapNetworkMode,
|
||||
) -> CodexResult<bool> {
|
||||
let preflight_argv = build_preflight_bwrap_argv(
|
||||
sandbox_policy_cwd,
|
||||
command_cwd,
|
||||
file_system_sandbox_policy,
|
||||
effective_filesystem_permissions,
|
||||
network_mode,
|
||||
)?;
|
||||
let stderr = run_bwrap_in_child_capture_stderr(preflight_argv);
|
||||
@@ -460,13 +472,13 @@ fn preflight_proc_mount_support(
|
||||
fn build_preflight_bwrap_argv(
|
||||
sandbox_policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
effective_filesystem_permissions: &EffectiveFilesystemPermissions,
|
||||
network_mode: BwrapNetworkMode,
|
||||
) -> CodexResult<crate::bwrap::BwrapArgs> {
|
||||
let preflight_command = vec![resolve_true_command()];
|
||||
build_bwrap_argv(
|
||||
preflight_command,
|
||||
file_system_sandbox_policy,
|
||||
effective_filesystem_permissions,
|
||||
sandbox_policy_cwd,
|
||||
command_cwd,
|
||||
BwrapOptions {
|
||||
|
||||
@@ -23,6 +23,64 @@ fn read_only_file_system_policy() -> FileSystemSandboxPolicy {
|
||||
read_only_permission_profile().file_system_sandbox_policy()
|
||||
}
|
||||
|
||||
fn effective_permissions_for_policy(
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
cwd: &Path,
|
||||
) -> codex_sandboxing::EffectiveFilesystemPermissions {
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(cwd)
|
||||
.unwrap_or_else(|err| panic!("test policy cwd should be absolute: {err}"));
|
||||
let file_system_sandbox_policy = file_system_sandbox_policy
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(std::slice::from_ref(&cwd));
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
&file_system_sandbox_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
codex_sandboxing::EffectiveFilesystemPermissions::from_profile(
|
||||
&permission_profile,
|
||||
codex_sandboxing::FilesystemPermissionsContext {
|
||||
policy_evaluation_cwd: &cwd,
|
||||
},
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
panic!("test filesystem policy should yield effective permissions: {err}")
|
||||
})
|
||||
}
|
||||
|
||||
fn build_bwrap_argv_for_policy(
|
||||
inner: Vec<String>,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
sandbox_policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
options: BwrapOptions,
|
||||
) -> codex_protocol::error::Result<crate::bwrap::BwrapArgs> {
|
||||
let effective_filesystem_permissions =
|
||||
effective_permissions_for_policy(file_system_sandbox_policy, sandbox_policy_cwd);
|
||||
build_bwrap_argv(
|
||||
inner,
|
||||
&effective_filesystem_permissions,
|
||||
sandbox_policy_cwd,
|
||||
command_cwd,
|
||||
options,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_preflight_bwrap_argv_for_policy(
|
||||
sandbox_policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_mode: BwrapNetworkMode,
|
||||
) -> codex_protocol::error::Result<crate::bwrap::BwrapArgs> {
|
||||
let effective_filesystem_permissions =
|
||||
effective_permissions_for_policy(file_system_sandbox_policy, sandbox_policy_cwd);
|
||||
build_preflight_bwrap_argv(
|
||||
sandbox_policy_cwd,
|
||||
command_cwd,
|
||||
&effective_filesystem_permissions,
|
||||
network_mode,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_proc_mount_invalid_argument_failure() {
|
||||
let stderr = "bwrap: Can't mount proc on /newroot/proc: Invalid argument";
|
||||
@@ -50,7 +108,7 @@ fn ignores_non_proc_mount_errors() {
|
||||
#[test]
|
||||
fn inserts_bwrap_argv0_before_command_separator() {
|
||||
let file_system_sandbox_policy = read_only_file_system_policy();
|
||||
let mut argv = build_bwrap_argv(
|
||||
let mut argv = build_bwrap_argv_for_policy(
|
||||
vec!["/bin/true".to_string()],
|
||||
&file_system_sandbox_policy,
|
||||
Path::new("/"),
|
||||
@@ -94,7 +152,7 @@ fn inserts_bwrap_argv0_before_command_separator() {
|
||||
#[test]
|
||||
fn rewrites_inner_command_path_when_bwrap_lacks_argv0() {
|
||||
let file_system_sandbox_policy = read_only_file_system_policy();
|
||||
let mut argv = build_bwrap_argv(
|
||||
let mut argv = build_bwrap_argv_for_policy(
|
||||
vec!["/bin/true".to_string()],
|
||||
&file_system_sandbox_policy,
|
||||
Path::new("/"),
|
||||
@@ -163,7 +221,7 @@ fn rewrites_bwrap_helper_command_not_nested_user_command_when_current_exe_appear
|
||||
#[test]
|
||||
fn inserts_unshare_net_when_network_isolation_requested() {
|
||||
let file_system_sandbox_policy = read_only_file_system_policy();
|
||||
let argv = build_bwrap_argv(
|
||||
let argv = build_bwrap_argv_for_policy(
|
||||
vec!["/bin/true".to_string()],
|
||||
&file_system_sandbox_policy,
|
||||
Path::new("/"),
|
||||
@@ -182,7 +240,7 @@ fn inserts_unshare_net_when_network_isolation_requested() {
|
||||
#[test]
|
||||
fn inserts_unshare_net_when_proxy_only_network_mode_requested() {
|
||||
let file_system_sandbox_policy = read_only_file_system_policy();
|
||||
let argv = build_bwrap_argv(
|
||||
let argv = build_bwrap_argv_for_policy(
|
||||
vec!["/bin/true".to_string()],
|
||||
&file_system_sandbox_policy,
|
||||
Path::new("/"),
|
||||
@@ -263,7 +321,7 @@ fn managed_proxy_preflight_argv_is_wrapped_for_full_access_policy() {
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
/*allow_network_for_proxy*/ true,
|
||||
);
|
||||
let argv = build_preflight_bwrap_argv(
|
||||
let argv = build_preflight_bwrap_argv_for_policy(
|
||||
Path::new("/"),
|
||||
Path::new("/"),
|
||||
&FileSystemSandboxPolicy::unrestricted(),
|
||||
|
||||
189
codex-rs/sandboxing/src/effective_filesystem_permissions.rs
Normal file
189
codex-rs/sandboxing/src/effective_filesystem_permissions.rs
Normal file
@@ -0,0 +1,189 @@
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxKind;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::permissions::ReadDenyMatcher;
|
||||
use codex_protocol::permissions::project_roots_glob_pattern;
|
||||
use codex_protocol::protocol::WritableRoot;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::fmt;
|
||||
use std::path::Path;
|
||||
|
||||
/// Context needed to evaluate an already-materialized filesystem policy.
|
||||
pub struct FilesystemPermissionsContext<'a> {
|
||||
/// Resolves cwd-sensitive policy mechanics such as filesystem root and
|
||||
/// relative candidate paths. It is not workspace-root authority.
|
||||
pub policy_evaluation_cwd: &'a AbsolutePathBuf,
|
||||
}
|
||||
|
||||
/// The outer filesystem access mode represented by effective permissions.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FilesystemPermissionsMode {
|
||||
Restricted,
|
||||
Unrestricted,
|
||||
External,
|
||||
}
|
||||
|
||||
/// A deny-read glob retained in effective filesystem enforcement inputs.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ValidatedDenyGlob {
|
||||
pattern: String,
|
||||
}
|
||||
|
||||
impl ValidatedDenyGlob {
|
||||
pub fn pattern(&self) -> &str {
|
||||
&self.pattern
|
||||
}
|
||||
}
|
||||
|
||||
/// Effective filesystem enforcement facts derived from a permission profile.
|
||||
///
|
||||
/// This internal representation centralizes effective roots, writable carveouts,
|
||||
/// protected metadata, and read-deny matching before platform-specific lowering.
|
||||
pub struct EffectiveFilesystemPermissions {
|
||||
pub mode: FilesystemPermissionsMode,
|
||||
pub readable_roots: Vec<AbsolutePathBuf>,
|
||||
pub writable_roots: Vec<WritableRoot>,
|
||||
pub unreadable_roots: Vec<AbsolutePathBuf>,
|
||||
pub unreadable_globs: Vec<ValidatedDenyGlob>,
|
||||
pub include_platform_defaults: bool,
|
||||
pub glob_scan_max_depth: Option<usize>,
|
||||
file_system_policy: FileSystemSandboxPolicy,
|
||||
policy_evaluation_cwd: AbsolutePathBuf,
|
||||
read_deny_matcher: Option<ReadDenyMatcher>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for EffectiveFilesystemPermissions {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_struct("EffectiveFilesystemPermissions")
|
||||
.field("mode", &self.mode)
|
||||
.field("readable_roots", &self.readable_roots)
|
||||
.field("writable_roots", &self.writable_roots)
|
||||
.field("unreadable_roots", &self.unreadable_roots)
|
||||
.field("unreadable_globs", &self.unreadable_globs)
|
||||
.field("include_platform_defaults", &self.include_platform_defaults)
|
||||
.field("glob_scan_max_depth", &self.glob_scan_max_depth)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl EffectiveFilesystemPermissions {
|
||||
/// Derives effective filesystem enforcement facts for platform consumers.
|
||||
///
|
||||
/// Callers must pass an effective `PermissionProfile` after runtime grants and
|
||||
/// runtime workspace roots have been applied. Symbolic workspace-root entries
|
||||
/// are rejected at this boundary rather than resolved from a working directory.
|
||||
pub fn from_profile(
|
||||
permission_profile: &PermissionProfile,
|
||||
context: FilesystemPermissionsContext<'_>,
|
||||
) -> Result<Self, FilesystemPermissionsError> {
|
||||
let file_system_policy = permission_profile.file_system_sandbox_policy();
|
||||
if contains_unmaterialized_workspace_roots(&file_system_policy) {
|
||||
return Err(FilesystemPermissionsError::UnmaterializedWorkspaceRoots);
|
||||
}
|
||||
// Direct enforcement queries have historically failed closed for malformed
|
||||
// deny patterns. Platform lowering that expands concrete targets can still
|
||||
// validate the patterns before acting on the filesystem.
|
||||
let read_deny_matcher =
|
||||
ReadDenyMatcher::new(&file_system_policy, context.policy_evaluation_cwd.as_path());
|
||||
let mode = match file_system_policy.kind {
|
||||
FileSystemSandboxKind::Restricted => FilesystemPermissionsMode::Restricted,
|
||||
FileSystemSandboxKind::Unrestricted => FilesystemPermissionsMode::Unrestricted,
|
||||
FileSystemSandboxKind::ExternalSandbox => FilesystemPermissionsMode::External,
|
||||
};
|
||||
let readable_roots =
|
||||
file_system_policy.get_readable_roots_with_cwd(context.policy_evaluation_cwd.as_path());
|
||||
let writable_roots =
|
||||
file_system_policy.get_writable_roots_with_cwd(context.policy_evaluation_cwd.as_path());
|
||||
let unreadable_roots = file_system_policy
|
||||
.get_unreadable_roots_with_cwd(context.policy_evaluation_cwd.as_path());
|
||||
let unreadable_globs = file_system_policy
|
||||
.get_unreadable_globs_with_cwd(context.policy_evaluation_cwd.as_path())
|
||||
.into_iter()
|
||||
.map(|pattern| ValidatedDenyGlob { pattern })
|
||||
.collect();
|
||||
let include_platform_defaults = file_system_policy.include_platform_defaults();
|
||||
let glob_scan_max_depth = file_system_policy.glob_scan_max_depth;
|
||||
|
||||
Ok(Self {
|
||||
mode,
|
||||
readable_roots,
|
||||
writable_roots,
|
||||
unreadable_roots,
|
||||
unreadable_globs,
|
||||
include_platform_defaults,
|
||||
glob_scan_max_depth,
|
||||
file_system_policy,
|
||||
policy_evaluation_cwd: context.policy_evaluation_cwd.clone(),
|
||||
read_deny_matcher,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns whether a read is permitted after applying explicit read denies.
|
||||
pub fn can_read(&self, path: &Path) -> bool {
|
||||
self.file_system_policy
|
||||
.can_read_path_with_cwd(path, self.policy_evaluation_cwd.as_path())
|
||||
&& !self.is_read_denied(path)
|
||||
}
|
||||
|
||||
/// Returns whether a write is permitted, including protected metadata rules.
|
||||
pub fn can_write(&self, path: &Path) -> bool {
|
||||
self.file_system_policy
|
||||
.can_write_path_with_cwd(path, self.policy_evaluation_cwd.as_path())
|
||||
}
|
||||
|
||||
/// Returns whether `path` is matched by an explicit deny-read entry.
|
||||
pub fn is_read_denied(&self, path: &Path) -> bool {
|
||||
self.read_deny_matcher
|
||||
.as_ref()
|
||||
.is_some_and(|matcher| matcher.is_read_denied(path))
|
||||
}
|
||||
|
||||
pub fn has_full_disk_read_access(&self) -> bool {
|
||||
self.file_system_policy.has_full_disk_read_access()
|
||||
}
|
||||
|
||||
pub fn has_full_disk_write_access(&self) -> bool {
|
||||
self.file_system_policy.has_full_disk_write_access()
|
||||
}
|
||||
}
|
||||
|
||||
/// An error deriving filesystem enforcement facts from a permission profile.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum FilesystemPermissionsError {
|
||||
UnmaterializedWorkspaceRoots,
|
||||
InvalidDenyGlob(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for FilesystemPermissionsError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::UnmaterializedWorkspaceRoots => formatter.write_str(
|
||||
"effective filesystem permissions require workspace roots to be materialized from runtime workspace roots",
|
||||
),
|
||||
Self::InvalidDenyGlob(message) => formatter.write_str(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for FilesystemPermissionsError {}
|
||||
|
||||
fn contains_unmaterialized_workspace_roots(file_system_policy: &FileSystemSandboxPolicy) -> bool {
|
||||
let workspace_glob_prefix = project_roots_glob_pattern(Path::new(""));
|
||||
file_system_policy
|
||||
.entries
|
||||
.iter()
|
||||
.any(|entry| match &entry.path {
|
||||
FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::ProjectRoots { .. },
|
||||
} => true,
|
||||
FileSystemPath::GlobPattern { pattern } => pattern.starts_with(&workspace_glob_prefix),
|
||||
FileSystemPath::Path { .. } | FileSystemPath::Special { .. } => false,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "effective_filesystem_permissions_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,279 @@
|
||||
use super::EffectiveFilesystemPermissions;
|
||||
use super::FilesystemPermissionsContext;
|
||||
use super::FilesystemPermissionsError;
|
||||
use super::FilesystemPermissionsMode;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSandboxKind;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::permissions::ReadDenyMatcher;
|
||||
use codex_protocol::permissions::project_roots_glob_pattern;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn absolute_path(path: &Path) -> AbsolutePathBuf {
|
||||
AbsolutePathBuf::from_absolute_path(path).expect("absolute path")
|
||||
}
|
||||
|
||||
fn derive_effective(
|
||||
permission_profile: &PermissionProfile,
|
||||
cwd: &AbsolutePathBuf,
|
||||
) -> super::EffectiveFilesystemPermissions {
|
||||
EffectiveFilesystemPermissions::from_profile(
|
||||
permission_profile,
|
||||
FilesystemPermissionsContext {
|
||||
policy_evaluation_cwd: cwd,
|
||||
},
|
||||
)
|
||||
.expect("valid effective filesystem permissions")
|
||||
}
|
||||
|
||||
fn assert_projected_fields_match_policy(
|
||||
permission_profile: &PermissionProfile,
|
||||
cwd: &AbsolutePathBuf,
|
||||
) -> super::EffectiveFilesystemPermissions {
|
||||
let effective = derive_effective(permission_profile, cwd);
|
||||
let policy = permission_profile.file_system_sandbox_policy();
|
||||
let expected_mode = match policy.kind {
|
||||
FileSystemSandboxKind::Restricted => FilesystemPermissionsMode::Restricted,
|
||||
FileSystemSandboxKind::Unrestricted => FilesystemPermissionsMode::Unrestricted,
|
||||
FileSystemSandboxKind::ExternalSandbox => FilesystemPermissionsMode::External,
|
||||
};
|
||||
|
||||
assert_eq!(effective.mode, expected_mode);
|
||||
assert_eq!(
|
||||
effective.readable_roots,
|
||||
policy.get_readable_roots_with_cwd(cwd.as_path())
|
||||
);
|
||||
assert_eq!(
|
||||
effective.writable_roots,
|
||||
policy.get_writable_roots_with_cwd(cwd.as_path())
|
||||
);
|
||||
assert_eq!(
|
||||
effective.unreadable_roots,
|
||||
policy.get_unreadable_roots_with_cwd(cwd.as_path())
|
||||
);
|
||||
assert_eq!(
|
||||
effective
|
||||
.unreadable_globs
|
||||
.iter()
|
||||
.map(|glob| glob.pattern().to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
policy.get_unreadable_globs_with_cwd(cwd.as_path())
|
||||
);
|
||||
assert_eq!(
|
||||
effective.include_platform_defaults,
|
||||
policy.include_platform_defaults()
|
||||
);
|
||||
assert_eq!(effective.glob_scan_max_depth, policy.glob_scan_max_depth);
|
||||
assert_eq!(
|
||||
effective.has_full_disk_read_access(),
|
||||
policy.has_full_disk_read_access()
|
||||
);
|
||||
assert_eq!(
|
||||
effective.has_full_disk_write_access(),
|
||||
policy.has_full_disk_write_access()
|
||||
);
|
||||
|
||||
effective
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_access_modes_preserve_builtin_profile_semantics() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = absolute_path(temp_dir.path());
|
||||
|
||||
let read_only = assert_projected_fields_match_policy(&PermissionProfile::read_only(), &cwd);
|
||||
assert_eq!(read_only.mode, FilesystemPermissionsMode::Restricted);
|
||||
assert_eq!(read_only.can_read(cwd.as_path()), true);
|
||||
assert_eq!(read_only.can_write(cwd.as_path()), false);
|
||||
|
||||
let unrestricted = assert_projected_fields_match_policy(&PermissionProfile::Disabled, &cwd);
|
||||
assert_eq!(unrestricted.mode, FilesystemPermissionsMode::Unrestricted);
|
||||
assert_eq!(unrestricted.can_write(cwd.as_path()), true);
|
||||
|
||||
let external = assert_projected_fields_match_policy(
|
||||
&PermissionProfile::External {
|
||||
network: NetworkSandboxPolicy::Restricted,
|
||||
},
|
||||
&cwd,
|
||||
);
|
||||
assert_eq!(external.mode, FilesystemPermissionsMode::External);
|
||||
assert_eq!(external.has_full_disk_write_access(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_workspace_permissions_reject_unmaterialized_runtime_workspace_roots() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = absolute_path(temp_dir.path());
|
||||
let unresolved_exact = PermissionProfile::workspace_write();
|
||||
let unresolved_glob_policy =
|
||||
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: project_roots_glob_pattern(Path::new("**/*.env")),
|
||||
},
|
||||
access: FileSystemAccessMode::Deny,
|
||||
}]);
|
||||
let unresolved_glob = PermissionProfile::from_runtime_permissions(
|
||||
&unresolved_glob_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
|
||||
for profile in [&unresolved_exact, &unresolved_glob] {
|
||||
let error = EffectiveFilesystemPermissions::from_profile(
|
||||
profile,
|
||||
FilesystemPermissionsContext {
|
||||
policy_evaluation_cwd: &cwd,
|
||||
},
|
||||
)
|
||||
.expect_err("unresolved runtime workspace roots should fail");
|
||||
assert_eq!(
|
||||
error,
|
||||
FilesystemPermissionsError::UnmaterializedWorkspaceRoots
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_workspace_permissions_preserve_materialized_workspace_roots() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = absolute_path(temp_dir.path());
|
||||
let first_root = cwd.join("first");
|
||||
let second_root = cwd.join("second");
|
||||
let permission_profile = PermissionProfile::workspace_write()
|
||||
.materialize_project_roots_with_workspace_roots(&[first_root.clone(), second_root.clone()]);
|
||||
let effective = assert_projected_fields_match_policy(&permission_profile, &cwd);
|
||||
|
||||
assert_eq!(effective.can_write(first_root.join("src").as_path()), true);
|
||||
assert_eq!(effective.can_write(second_root.join("src").as_path()), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_permissions_preserve_nested_carveouts_and_read_denies() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = absolute_path(temp_dir.path());
|
||||
let workspace = cwd.join("workspace");
|
||||
let read_only_child = workspace.join("generated");
|
||||
let denied_child = workspace.join("private");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: workspace.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: read_only_child.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: denied_child.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::Deny,
|
||||
},
|
||||
]);
|
||||
let permission_profile =
|
||||
PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted);
|
||||
let effective = assert_projected_fields_match_policy(&permission_profile, &cwd);
|
||||
|
||||
assert_eq!(
|
||||
effective.can_write(workspace.join("file.txt").as_path()),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
effective.can_write(read_only_child.join("file.txt").as_path()),
|
||||
false
|
||||
);
|
||||
assert_eq!(effective.can_write(workspace.join(".git").as_path()), false);
|
||||
assert_eq!(
|
||||
effective.can_read(denied_child.join("secret.txt").as_path()),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn effective_permissions_preserve_symlinked_writable_roots() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = absolute_path(temp_dir.path());
|
||||
let target = cwd.join("target");
|
||||
let link = cwd.join("linked-workspace");
|
||||
std::fs::create_dir_all(target.as_path()).expect("create target");
|
||||
std::os::unix::fs::symlink(target.as_path(), link.as_path()).expect("create symlink");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: link.clone() },
|
||||
access: FileSystemAccessMode::Write,
|
||||
}]);
|
||||
let permission_profile =
|
||||
PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted);
|
||||
let effective = assert_projected_fields_match_policy(&permission_profile, &cwd);
|
||||
|
||||
assert_eq!(
|
||||
effective.can_write(link.join("file.txt").as_path()),
|
||||
policy.can_write_path_with_cwd(link.join("file.txt").as_path(), cwd.as_path())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_permissions_preserve_accepted_deny_glob_matching() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = absolute_path(temp_dir.path());
|
||||
let pattern = cwd.join("secret[.txt").to_string_lossy().into_owned();
|
||||
let denied_path = cwd.join("secret[.txt");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: cwd.clone() },
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern { pattern },
|
||||
access: FileSystemAccessMode::Deny,
|
||||
},
|
||||
]);
|
||||
let permission_profile =
|
||||
PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted);
|
||||
let effective = assert_projected_fields_match_policy(&permission_profile, &cwd);
|
||||
let current_matcher = ReadDenyMatcher::try_new(&policy, cwd.as_path())
|
||||
.expect("accepted pattern")
|
||||
.expect("deny matcher");
|
||||
|
||||
assert_eq!(effective.is_read_denied(denied_path.as_path()), true);
|
||||
assert_eq!(
|
||||
effective.is_read_denied(denied_path.as_path()),
|
||||
current_matcher.is_read_denied(denied_path.as_path())
|
||||
);
|
||||
assert_eq!(effective.can_read(denied_path.as_path()), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_permissions_fail_closed_for_malformed_deny_globs() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = absolute_path(temp_dir.path());
|
||||
let readable_path = cwd.join("readable.txt");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: cwd.clone() },
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: format!("{}/**/[z-a]", cwd.as_path().display()),
|
||||
},
|
||||
access: FileSystemAccessMode::Deny,
|
||||
},
|
||||
]);
|
||||
let permission_profile =
|
||||
PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted);
|
||||
let effective = derive_effective(&permission_profile, &cwd);
|
||||
|
||||
assert_eq!(effective.is_read_denied(readable_path.as_path()), true);
|
||||
assert_eq!(effective.can_read(readable_path.as_path()), false);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
mod bwrap;
|
||||
mod effective_filesystem_permissions;
|
||||
pub mod landlock;
|
||||
mod manager;
|
||||
pub mod policy_transforms;
|
||||
@@ -10,6 +11,11 @@ pub mod seatbelt;
|
||||
pub use bwrap::find_system_bwrap_in_path;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use bwrap::system_bwrap_warning;
|
||||
pub use effective_filesystem_permissions::EffectiveFilesystemPermissions;
|
||||
pub use effective_filesystem_permissions::FilesystemPermissionsContext;
|
||||
pub use effective_filesystem_permissions::FilesystemPermissionsError;
|
||||
pub use effective_filesystem_permissions::FilesystemPermissionsMode;
|
||||
pub use effective_filesystem_permissions::ValidatedDenyGlob;
|
||||
pub use manager::SandboxCommand;
|
||||
pub use manager::SandboxExecRequest;
|
||||
pub use manager::SandboxManager;
|
||||
@@ -35,6 +41,12 @@ impl From<SandboxTransformError> for CodexErr {
|
||||
SandboxTransformError::MissingLinuxSandboxExecutable => {
|
||||
CodexErr::LandlockSandboxExecutableNotProvided
|
||||
}
|
||||
SandboxTransformError::InvalidPermissionProfileCwd(message) => {
|
||||
CodexErr::UnsupportedOperation(message)
|
||||
}
|
||||
SandboxTransformError::EffectiveFilesystemPermissions(err) => {
|
||||
CodexErr::UnsupportedOperation(err.to_string())
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
SandboxTransformError::Wsl1UnsupportedForBubblewrap => {
|
||||
CodexErr::UnsupportedOperation(crate::bwrap::WSL1_BWRAP_WARNING.to_string())
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use crate::EffectiveFilesystemPermissions;
|
||||
use crate::FilesystemPermissionsContext;
|
||||
use crate::FilesystemPermissionsError;
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::bwrap::WSL1_BWRAP_WARNING;
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -80,6 +83,7 @@ pub struct SandboxExecRequest {
|
||||
pub windows_sandbox_level: WindowsSandboxLevel,
|
||||
pub windows_sandbox_private_desktop: bool,
|
||||
pub permission_profile: PermissionProfile,
|
||||
pub effective_filesystem_permissions: EffectiveFilesystemPermissions,
|
||||
pub file_system_sandbox_policy: FileSystemSandboxPolicy,
|
||||
pub network_sandbox_policy: NetworkSandboxPolicy,
|
||||
pub arg0: Option<String>,
|
||||
@@ -106,6 +110,8 @@ pub struct SandboxTransformRequest<'a> {
|
||||
#[derive(Debug)]
|
||||
pub enum SandboxTransformError {
|
||||
MissingLinuxSandboxExecutable,
|
||||
InvalidPermissionProfileCwd(String),
|
||||
EffectiveFilesystemPermissions(FilesystemPermissionsError),
|
||||
#[cfg(target_os = "linux")]
|
||||
Wsl1UnsupportedForBubblewrap,
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
@@ -118,6 +124,8 @@ impl std::fmt::Display for SandboxTransformError {
|
||||
Self::MissingLinuxSandboxExecutable => {
|
||||
write!(f, "missing codex-linux-sandbox executable path")
|
||||
}
|
||||
Self::InvalidPermissionProfileCwd(message) => f.write_str(message),
|
||||
Self::EffectiveFilesystemPermissions(err) => write!(f, "{err}"),
|
||||
#[cfg(target_os = "linux")]
|
||||
Self::Wsl1UnsupportedForBubblewrap => write!(f, "{WSL1_BWRAP_WARNING}"),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
@@ -186,6 +194,15 @@ impl SandboxManager {
|
||||
effective_permission_profile(permissions, additional_permissions.as_ref());
|
||||
let (effective_file_system_policy, effective_network_policy) =
|
||||
effective_permission_profile.to_runtime_permissions();
|
||||
let policy_evaluation_cwd = AbsolutePathBuf::from_absolute_path(sandbox_policy_cwd)
|
||||
.map_err(|err| SandboxTransformError::InvalidPermissionProfileCwd(err.to_string()))?;
|
||||
let effective_filesystem_permissions = EffectiveFilesystemPermissions::from_profile(
|
||||
&effective_permission_profile,
|
||||
FilesystemPermissionsContext {
|
||||
policy_evaluation_cwd: &policy_evaluation_cwd,
|
||||
},
|
||||
)
|
||||
.map_err(SandboxTransformError::EffectiveFilesystemPermissions)?;
|
||||
let mut argv = Vec::with_capacity(1 + command.args.len());
|
||||
argv.push(command.program);
|
||||
argv.extend(command.args.into_iter().map(OsString::from));
|
||||
@@ -200,9 +217,8 @@ impl SandboxManager {
|
||||
|
||||
let mut args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
|
||||
command: os_argv_to_strings(argv),
|
||||
file_system_sandbox_policy: &effective_file_system_policy,
|
||||
effective_filesystem_permissions: &effective_filesystem_permissions,
|
||||
network_sandbox_policy: effective_network_policy,
|
||||
sandbox_policy_cwd,
|
||||
enforce_managed_network,
|
||||
network,
|
||||
extra_allow_unix_sockets: &[],
|
||||
@@ -253,6 +269,7 @@ impl SandboxManager {
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
permission_profile: effective_permission_profile,
|
||||
effective_filesystem_permissions,
|
||||
file_system_sandbox_policy: effective_file_system_policy,
|
||||
network_sandbox_policy: effective_network_policy,
|
||||
arg0: arg0_override,
|
||||
|
||||
@@ -217,6 +217,18 @@ fn transform_additional_permissions_preserves_denied_entries() {
|
||||
})
|
||||
.expect("transform");
|
||||
|
||||
assert_eq!(
|
||||
exec_request
|
||||
.effective_filesystem_permissions
|
||||
.can_write(allowed_path.as_path()),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
exec_request
|
||||
.effective_filesystem_permissions
|
||||
.can_read(denied_path.as_path()),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
exec_request.file_system_sandbox_policy,
|
||||
FileSystemSandboxPolicy::restricted(vec![
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use crate::EffectiveFilesystemPermissions;
|
||||
use crate::FilesystemPermissionsContext;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_network_proxy::PROXY_URL_ENV_KEYS;
|
||||
use codex_network_proxy::has_proxy_url_env_vars;
|
||||
use codex_network_proxy::proxy_url_env_value;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::permissions::PROTECTED_METADATA_PATH_NAMES;
|
||||
@@ -404,9 +407,8 @@ fn seatbelt_protected_metadata_name_regex(root: &AbsolutePathBuf, name: &str) ->
|
||||
}
|
||||
|
||||
fn protected_metadata_names_for_writable_root(
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
effective_filesystem_permissions: &EffectiveFilesystemPermissions,
|
||||
writable_root: &WritableRoot,
|
||||
cwd: &Path,
|
||||
) -> Vec<String> {
|
||||
let mut names = writable_root.protected_metadata_names.clone();
|
||||
for name in PROTECTED_METADATA_PATH_NAMES {
|
||||
@@ -414,7 +416,7 @@ fn protected_metadata_names_for_writable_root(
|
||||
continue;
|
||||
}
|
||||
let path = writable_root.root.join(*name);
|
||||
if !file_system_sandbox_policy.can_write_path_with_cwd(path.as_path(), cwd) {
|
||||
if !effective_filesystem_permissions.can_write(path.as_path()) {
|
||||
names.push((*name).to_string());
|
||||
}
|
||||
}
|
||||
@@ -422,25 +424,24 @@ fn protected_metadata_names_for_writable_root(
|
||||
}
|
||||
|
||||
fn build_seatbelt_unreadable_glob_policy(
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
cwd: &Path,
|
||||
effective_filesystem_permissions: &EffectiveFilesystemPermissions,
|
||||
) -> String {
|
||||
// Seatbelt does not understand the filesystem policy's glob syntax directly.
|
||||
// Convert each unreadable pattern into an anchored regex deny rule and apply
|
||||
// it to both reads and unlink-style writes so a denied path cannot be probed
|
||||
// through destructive filesystem operations.
|
||||
let unreadable_globs = file_system_sandbox_policy.get_unreadable_globs_with_cwd(cwd);
|
||||
if unreadable_globs.is_empty() {
|
||||
if effective_filesystem_permissions.unreadable_globs.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let mut policy_components = Vec::new();
|
||||
for pattern in unreadable_globs {
|
||||
for glob in &effective_filesystem_permissions.unreadable_globs {
|
||||
let pattern = glob.pattern();
|
||||
let mut regexes = BTreeSet::new();
|
||||
if let Some(regex) = seatbelt_regex_for_unreadable_glob(&pattern) {
|
||||
if let Some(regex) = seatbelt_regex_for_unreadable_glob(pattern) {
|
||||
regexes.insert(regex);
|
||||
}
|
||||
if let Some(pattern) = canonicalize_glob_static_prefix_for_sandbox(&pattern)
|
||||
if let Some(pattern) = canonicalize_glob_static_prefix_for_sandbox(pattern)
|
||||
&& let Some(regex) = seatbelt_regex_for_unreadable_glob(&pattern)
|
||||
{
|
||||
regexes.insert(regex);
|
||||
@@ -573,15 +574,31 @@ fn create_seatbelt_command_args_for_legacy_policy(
|
||||
enforce_managed_network: bool,
|
||||
network: Option<&NetworkProxy>,
|
||||
) -> Vec<String> {
|
||||
let legacy_workspace_root = AbsolutePathBuf::from_absolute_path(sandbox_policy_cwd)
|
||||
.unwrap_or_else(|err| panic!("sandbox policy cwd should be absolute: {err}"));
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
|
||||
sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
)
|
||||
.materialize_project_roots_with_workspace_roots(std::slice::from_ref(&legacy_workspace_root));
|
||||
let network_sandbox_policy = NetworkSandboxPolicy::from(sandbox_policy);
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
let effective_filesystem_permissions = EffectiveFilesystemPermissions::from_profile(
|
||||
&permission_profile,
|
||||
FilesystemPermissionsContext {
|
||||
policy_evaluation_cwd: &legacy_workspace_root,
|
||||
},
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
panic!("legacy filesystem policy should yield effective permissions: {err}")
|
||||
});
|
||||
create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
|
||||
command,
|
||||
file_system_sandbox_policy: &file_system_sandbox_policy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy::from(sandbox_policy),
|
||||
sandbox_policy_cwd,
|
||||
effective_filesystem_permissions: &effective_filesystem_permissions,
|
||||
network_sandbox_policy,
|
||||
enforce_managed_network,
|
||||
network,
|
||||
extra_allow_unix_sockets: &[],
|
||||
@@ -591,9 +608,8 @@ fn create_seatbelt_command_args_for_legacy_policy(
|
||||
#[derive(Debug)]
|
||||
pub struct CreateSeatbeltCommandArgsParams<'a> {
|
||||
pub command: Vec<String>,
|
||||
pub file_system_sandbox_policy: &'a FileSystemSandboxPolicy,
|
||||
pub effective_filesystem_permissions: &'a EffectiveFilesystemPermissions,
|
||||
pub network_sandbox_policy: NetworkSandboxPolicy,
|
||||
pub sandbox_policy_cwd: &'a Path,
|
||||
pub enforce_managed_network: bool,
|
||||
pub network: Option<&'a NetworkProxy>,
|
||||
pub extra_allow_unix_sockets: &'a [AbsolutePathBuf],
|
||||
@@ -602,18 +618,16 @@ pub struct CreateSeatbeltCommandArgsParams<'a> {
|
||||
pub fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -> Vec<String> {
|
||||
let CreateSeatbeltCommandArgsParams {
|
||||
command,
|
||||
file_system_sandbox_policy,
|
||||
effective_filesystem_permissions,
|
||||
network_sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
enforce_managed_network,
|
||||
network,
|
||||
extra_allow_unix_sockets,
|
||||
} = args;
|
||||
|
||||
let unreadable_roots =
|
||||
file_system_sandbox_policy.get_unreadable_roots_with_cwd(sandbox_policy_cwd);
|
||||
let unreadable_roots = effective_filesystem_permissions.unreadable_roots.clone();
|
||||
let (file_write_policy, file_write_dir_params) =
|
||||
if file_system_sandbox_policy.has_full_disk_write_access() {
|
||||
if effective_filesystem_permissions.has_full_disk_write_access() {
|
||||
if unreadable_roots.is_empty() {
|
||||
// Allegedly, this is more permissive than `(allow file-write*)`.
|
||||
(
|
||||
@@ -635,14 +649,14 @@ pub fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -
|
||||
build_seatbelt_access_policy(
|
||||
"file-write*",
|
||||
"WRITABLE_ROOT",
|
||||
file_system_sandbox_policy
|
||||
.get_writable_roots_with_cwd(sandbox_policy_cwd)
|
||||
.into_iter()
|
||||
effective_filesystem_permissions
|
||||
.writable_roots
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|root| SeatbeltAccessRoot {
|
||||
protected_metadata_names: protected_metadata_names_for_writable_root(
|
||||
file_system_sandbox_policy,
|
||||
effective_filesystem_permissions,
|
||||
&root,
|
||||
sandbox_policy_cwd,
|
||||
),
|
||||
root: root.root,
|
||||
excluded_subpaths: root.read_only_subpaths,
|
||||
@@ -652,7 +666,7 @@ pub fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -
|
||||
};
|
||||
|
||||
let (file_read_policy, file_read_dir_params) =
|
||||
if file_system_sandbox_policy.has_full_disk_read_access() {
|
||||
if effective_filesystem_permissions.has_full_disk_read_access() {
|
||||
if unreadable_roots.is_empty() {
|
||||
(
|
||||
"; allow read-only file operations\n(allow file-read*)".to_string(),
|
||||
@@ -677,9 +691,10 @@ pub fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -
|
||||
let (policy, params) = build_seatbelt_access_policy(
|
||||
"file-read*",
|
||||
"READABLE_ROOT",
|
||||
file_system_sandbox_policy
|
||||
.get_readable_roots_with_cwd(sandbox_policy_cwd)
|
||||
.into_iter()
|
||||
effective_filesystem_permissions
|
||||
.readable_roots
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|root| SeatbeltAccessRoot {
|
||||
excluded_subpaths: unreadable_roots
|
||||
.iter()
|
||||
@@ -705,9 +720,8 @@ pub fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -
|
||||
let network_policy =
|
||||
dynamic_network_policy_for_network(network_sandbox_policy, enforce_managed_network, &proxy);
|
||||
|
||||
let include_platform_defaults = file_system_sandbox_policy.include_platform_defaults();
|
||||
let deny_read_policy =
|
||||
build_seatbelt_unreadable_glob_policy(file_system_sandbox_policy, sandbox_policy_cwd);
|
||||
let include_platform_defaults = effective_filesystem_permissions.include_platform_defaults;
|
||||
let deny_read_policy = build_seatbelt_unreadable_glob_policy(effective_filesystem_permissions);
|
||||
let mut policy_sections = vec![
|
||||
MACOS_SEATBELT_BASE_POLICY.to_string(),
|
||||
file_read_policy,
|
||||
|
||||
@@ -19,6 +19,7 @@ use codex_network_proxy::NetworkProxyConfig;
|
||||
use codex_network_proxy::NetworkProxyConstraints;
|
||||
use codex_network_proxy::NetworkProxyState;
|
||||
use codex_network_proxy::build_config_state;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
@@ -50,6 +51,30 @@ fn absolute_path(path: &str) -> AbsolutePathBuf {
|
||||
AbsolutePathBuf::from_absolute_path(Path::new(path)).expect("absolute path")
|
||||
}
|
||||
|
||||
fn effective_permissions(
|
||||
file_system_policy: &FileSystemSandboxPolicy,
|
||||
cwd: &Path,
|
||||
) -> crate::EffectiveFilesystemPermissions {
|
||||
let policy_evaluation_cwd =
|
||||
AbsolutePathBuf::from_absolute_path(cwd).expect("absolute sandbox policy cwd");
|
||||
let file_system_policy = file_system_policy
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(std::slice::from_ref(
|
||||
&policy_evaluation_cwd,
|
||||
));
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
&file_system_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
crate::EffectiveFilesystemPermissions::from_profile(
|
||||
&permission_profile,
|
||||
crate::FilesystemPermissionsContext {
|
||||
policy_evaluation_cwd: &policy_evaluation_cwd,
|
||||
},
|
||||
)
|
||||
.expect("derive effective filesystem permissions")
|
||||
}
|
||||
|
||||
fn seatbelt_policy_arg(args: &[String]) -> &str {
|
||||
let policy_index = args
|
||||
.iter()
|
||||
@@ -198,12 +223,12 @@ fn explicit_unreadable_paths_are_excluded_from_full_disk_read_and_write_access()
|
||||
access: FileSystemAccessMode::Deny,
|
||||
},
|
||||
]);
|
||||
let effective_permissions = effective_permissions(&file_system_policy, Path::new("/"));
|
||||
|
||||
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
|
||||
command: vec!["/bin/true".to_string()],
|
||||
file_system_sandbox_policy: &file_system_policy,
|
||||
effective_filesystem_permissions: &effective_permissions,
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
sandbox_policy_cwd: Path::new("/"),
|
||||
enforce_managed_network: false,
|
||||
network: None,
|
||||
extra_allow_unix_sockets: &[],
|
||||
@@ -270,12 +295,12 @@ fn explicit_unreadable_paths_are_excluded_from_readable_roots() {
|
||||
access: FileSystemAccessMode::Deny,
|
||||
},
|
||||
]);
|
||||
let effective_permissions = effective_permissions(&file_system_policy, Path::new("/"));
|
||||
|
||||
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
|
||||
command: vec!["/bin/true".to_string()],
|
||||
file_system_sandbox_policy: &file_system_policy,
|
||||
effective_filesystem_permissions: &effective_permissions,
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
sandbox_policy_cwd: Path::new("/"),
|
||||
enforce_managed_network: false,
|
||||
network: None,
|
||||
extra_allow_unix_sockets: &[],
|
||||
@@ -374,8 +399,9 @@ fn unreadable_glob_policy_includes_canonicalized_static_prefix() {
|
||||
path: FileSystemPath::GlobPattern { pattern },
|
||||
access: FileSystemAccessMode::Deny,
|
||||
});
|
||||
let effective_permissions = effective_permissions(&policy, temp_dir.path());
|
||||
|
||||
let seatbelt_policy = build_seatbelt_unreadable_glob_policy(&policy, temp_dir.path());
|
||||
let seatbelt_policy = build_seatbelt_unreadable_glob_policy(&effective_permissions);
|
||||
|
||||
assert!(
|
||||
seatbelt_policy.contains(&format!(r#"(deny file-read* (regex #"{expected_regex}"))"#)),
|
||||
@@ -573,12 +599,12 @@ fn create_seatbelt_args_allowlists_explicit_unix_socket_paths_without_proxy() {
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
cwd.path(),
|
||||
);
|
||||
let effective_permissions = effective_permissions(&file_system_policy, cwd.path());
|
||||
let extra_allow_unix_sockets = vec![absolute_path("/tmp/codex-browser-use")];
|
||||
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
|
||||
command: vec!["/usr/bin/true".to_string()],
|
||||
file_system_sandbox_policy: &file_system_policy,
|
||||
effective_filesystem_permissions: &effective_permissions,
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
sandbox_policy_cwd: cwd.path(),
|
||||
enforce_managed_network: false,
|
||||
network: None,
|
||||
extra_allow_unix_sockets: &extra_allow_unix_sockets,
|
||||
@@ -613,6 +639,7 @@ async fn create_seatbelt_args_merges_proxy_and_explicit_unix_socket_paths() -> a
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
cwd.path(),
|
||||
);
|
||||
let effective_permissions = effective_permissions(&file_system_policy, cwd.path());
|
||||
let network_socket = "/tmp/codex-proxy-use";
|
||||
let explicit_socket = "/tmp/codex-browser-use";
|
||||
let mut network_config = NetworkProxyConfig::default();
|
||||
@@ -634,9 +661,8 @@ async fn create_seatbelt_args_merges_proxy_and_explicit_unix_socket_paths() -> a
|
||||
|
||||
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
|
||||
command: vec!["/usr/bin/true".to_string()],
|
||||
file_system_sandbox_policy: &file_system_policy,
|
||||
effective_filesystem_permissions: &effective_permissions,
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
sandbox_policy_cwd: cwd.path(),
|
||||
enforce_managed_network: false,
|
||||
network: Some(&network_proxy),
|
||||
extra_allow_unix_sockets: &extra_allow_unix_sockets,
|
||||
@@ -672,12 +698,12 @@ fn create_seatbelt_args_preserves_full_network_with_explicit_unix_socket_paths()
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
cwd.path(),
|
||||
);
|
||||
let effective_permissions = effective_permissions(&file_system_policy, cwd.path());
|
||||
let extra_allow_unix_sockets = vec![absolute_path("/tmp/codex-browser-use")];
|
||||
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
|
||||
command: vec!["/usr/bin/true".to_string()],
|
||||
file_system_sandbox_policy: &file_system_policy,
|
||||
effective_filesystem_permissions: &effective_permissions,
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Enabled,
|
||||
sandbox_policy_cwd: cwd.path(),
|
||||
enforce_managed_network: false,
|
||||
network: None,
|
||||
extra_allow_unix_sockets: &extra_allow_unix_sockets,
|
||||
|
||||
Reference in New Issue
Block a user