Skip to content

Commit c2bf30f

Browse files
committed
Enforce preserved path names in Seatbelt
1 parent 11fa852 commit c2bf30f

2 files changed

Lines changed: 175 additions & 84 deletions

File tree

codex-rs/sandboxing/src/seatbelt.rs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ use codex_network_proxy::has_proxy_url_env_vars;
44
use codex_network_proxy::proxy_url_env_value;
55
use codex_protocol::permissions::FileSystemSandboxPolicy;
66
use codex_protocol::permissions::NetworkSandboxPolicy;
7+
use codex_protocol::permissions::PRESERVED_PATH_NAMES;
78
use codex_protocol::protocol::SandboxPolicy;
9+
use codex_protocol::protocol::WritableRoot;
810
use codex_utils_absolute_path::AbsolutePathBuf;
911
use std::collections::BTreeMap;
1012
use std::collections::BTreeSet;
@@ -328,6 +330,7 @@ fn root_absolute_path() -> AbsolutePathBuf {
328330
struct SeatbeltAccessRoot {
329331
root: AbsolutePathBuf,
330332
excluded_subpaths: Vec<AbsolutePathBuf>,
333+
preserved_path_names: Vec<String>,
331334
}
332335

333336
fn build_seatbelt_access_policy(
@@ -342,9 +345,9 @@ fn build_seatbelt_access_policy(
342345
let root =
343346
normalize_path_for_sandbox(access_root.root.as_path()).unwrap_or(access_root.root);
344347
let root_param = format!("{param_prefix}_{index}");
345-
params.push((root_param.clone(), root.into_path_buf()));
348+
params.push((root_param.clone(), root.clone().into_path_buf()));
346349

347-
if access_root.excluded_subpaths.is_empty() {
350+
if access_root.excluded_subpaths.is_empty() && access_root.preserved_path_names.is_empty() {
348351
policy_components.push(format!("(subpath (param \"{root_param}\"))"));
349352
continue;
350353
}
@@ -367,6 +370,11 @@ fn build_seatbelt_access_policy(
367370
"(require-not (subpath (param \"{excluded_param}\")))"
368371
));
369372
}
373+
for preserved_name in access_root.preserved_path_names {
374+
let regex =
375+
seatbelt_preserved_path_name_regex(&root, &preserved_name).replace('"', "\\\"");
376+
require_parts.push(format!(r#"(require-not (regex #"{regex}"))"#));
377+
}
370378
policy_components.push(format!("(require-all {} )", require_parts.join(" ")));
371379
}
372380

@@ -380,6 +388,38 @@ fn build_seatbelt_access_policy(
380388
}
381389
}
382390

391+
fn seatbelt_preserved_path_name_regex(root: &AbsolutePathBuf, name: &str) -> String {
392+
let mut root = root.to_string_lossy().to_string();
393+
while root.len() > 1 && root.ends_with('/') {
394+
root.pop();
395+
}
396+
let root = regex_lite::escape(&root);
397+
let name = regex_lite::escape(name);
398+
if root == "/" {
399+
format!(r#"^/(.*/)?{name}(/.*)?$"#)
400+
} else {
401+
format!(r#"^{root}/(.*/)?{name}(/.*)?$"#)
402+
}
403+
}
404+
405+
fn preserved_path_names_for_writable_root(
406+
file_system_sandbox_policy: &FileSystemSandboxPolicy,
407+
writable_root: &WritableRoot,
408+
cwd: &Path,
409+
) -> Vec<String> {
410+
let mut names = writable_root.preserved_path_names.clone();
411+
for name in PRESERVED_PATH_NAMES {
412+
if names.iter().any(|existing| existing == name) {
413+
continue;
414+
}
415+
let path = writable_root.root.join(*name);
416+
if !file_system_sandbox_policy.can_write_path_with_cwd(path.as_path(), cwd) {
417+
names.push((*name).to_string());
418+
}
419+
}
420+
names
421+
}
422+
383423
fn build_seatbelt_unreadable_glob_policy(
384424
file_system_sandbox_policy: &FileSystemSandboxPolicy,
385425
cwd: &Path,
@@ -586,6 +626,7 @@ pub fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -
586626
vec![SeatbeltAccessRoot {
587627
root: root_absolute_path(),
588628
excluded_subpaths: unreadable_roots.clone(),
629+
preserved_path_names: Vec::new(),
589630
}],
590631
)
591632
}
@@ -597,6 +638,11 @@ pub fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -
597638
.get_writable_roots_with_cwd(sandbox_policy_cwd)
598639
.into_iter()
599640
.map(|root| SeatbeltAccessRoot {
641+
preserved_path_names: preserved_path_names_for_writable_root(
642+
file_system_sandbox_policy,
643+
&root,
644+
sandbox_policy_cwd,
645+
),
600646
root: root.root,
601647
excluded_subpaths: root.read_only_subpaths,
602648
})
@@ -618,6 +664,7 @@ pub fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -
618664
vec![SeatbeltAccessRoot {
619665
root: root_absolute_path(),
620666
excluded_subpaths: unreadable_roots,
667+
preserved_path_names: Vec::new(),
621668
}],
622669
);
623670
(
@@ -638,6 +685,7 @@ pub fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -
638685
.filter(|path| path.as_path().starts_with(root.as_path()))
639686
.cloned()
640687
.collect(),
688+
preserved_path_names: Vec::new(),
641689
root,
642690
})
643691
.collect(),

0 commit comments

Comments
 (0)