in codex-rs/linux-sandbox/src/bwrap.rs [367:630]
fn create_filesystem_args(
file_system_sandbox_policy: &FileSystemSandboxPolicy,
cwd: &Path,
glob_scan_max_depth: Option<usize>,
) -> Result<BwrapArgs> {
let unreadable_globs = file_system_sandbox_policy.get_unreadable_globs_with_cwd(cwd);
// Bubblewrap requires bind mount targets to exist. Skip missing writable
// 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()
.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()
&& !unreadable_globs.is_empty()
{
writable_roots.push(WritableRoot {
root: AbsolutePathBuf::from_absolute_path("/")?,
read_only_subpaths: Vec::new(),
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()
.map(AbsolutePathBuf::into_path_buf)
.collect::<Vec<_>>();
// Bubblewrap can only mask concrete paths. Expand unreadable glob patterns
// to the existing matches we can see before constructing the mount overlay;
// core tool helpers still evaluate the original patterns directly at read time.
unreadable_roots.extend(
expand_unreadable_globs_with_ripgrep(&unreadable_globs, cwd, glob_scan_max_depth)?
.into_iter()
.map(AbsolutePathBuf::into_path_buf),
);
unreadable_roots.sort();
unreadable_roots.dedup();
let args = if file_system_sandbox_policy.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,
// urandom, and tty. `/dev` must be mounted before writable roots so
// explicit `/dev/*` writable binds remain visible.
vec![
"--ro-bind".to_string(),
"/".to_string(),
"/".to_string(),
"--dev".to_string(),
"/dev".to_string(),
]
} else {
// Start from an empty filesystem and add only the approved readable
// roots plus a minimal `/dev`.
let mut args = vec![
"--tmpfs".to_string(),
"/".to_string(),
"--dev".to_string(),
"/dev".to_string(),
];
let mut readable_roots: BTreeSet<PathBuf> = file_system_sandbox_policy
.get_readable_roots_with_cwd(cwd)
.into_iter()
.map(PathBuf::from)
.collect();
if file_system_sandbox_policy.include_platform_defaults() {
readable_roots.extend(
LINUX_PLATFORM_DEFAULT_READ_ROOTS
.iter()
.map(|path| PathBuf::from(*path))
.filter(|path| path.exists()),
);
}
// A restricted policy can still explicitly request `/`, which is
// the broad read baseline. Explicit unreadable carveouts are
// re-applied later.
if readable_roots.iter().any(|root| root == Path::new("/")) {
args = vec![
"--ro-bind".to_string(),
"/".to_string(),
"/".to_string(),
"--dev".to_string(),
"/dev".to_string(),
];
} else {
for root in readable_roots {
if !root.exists() {
continue;
}
// Writable roots are rebound by real target below; mirror that
// for their restricted-read bootstrap mount. Plain read-only
// roots must stay logical because callers may execute those
// paths inside bwrap, such as Bazel runfiles helper binaries.
let mount_root = if writable_roots
.iter()
.any(|writable_root| root.starts_with(writable_root.root.as_path()))
{
canonical_target_if_symlinked_path(&root).unwrap_or(root)
} else {
root
};
args.push("--ro-bind".to_string());
args.push(path_to_string(&mount_root));
args.push(path_to_string(&mount_root));
}
}
args
};
let mut bwrap_args = BwrapArgs {
args,
preserved_files: Vec::new(),
synthetic_mount_targets: Vec::new(),
protected_create_targets: Vec::new(),
};
let mut allowed_write_paths = Vec::with_capacity(writable_roots.len());
for writable_root in &writable_roots {
let root = writable_root.root.as_path();
allowed_write_paths.push(root.to_path_buf());
if let Some(target) = canonical_target_if_symlinked_path(root) {
allowed_write_paths.push(target);
}
}
let unreadable_paths: HashSet<PathBuf> = unreadable_roots.iter().cloned().collect();
let mut sorted_writable_roots = writable_roots;
sorted_writable_roots.sort_by_key(|writable_root| path_depth(writable_root.root.as_path()));
// Mask only the unreadable ancestors that sit outside every writable root.
// Unreadable paths nested under a broader writable root are applied after
// that broader root is bound, then reopened by any deeper writable child.
let mut unreadable_ancestors_of_writable_roots: Vec<PathBuf> = unreadable_roots
.iter()
.filter(|path| {
let unreadable_root = path.as_path();
!allowed_write_paths
.iter()
.any(|root| unreadable_root.starts_with(root))
&& allowed_write_paths
.iter()
.any(|root| root.starts_with(unreadable_root))
})
.cloned()
.collect();
unreadable_ancestors_of_writable_roots.sort_by_key(|path| path_depth(path));
for unreadable_root in &unreadable_ancestors_of_writable_roots {
append_unreadable_root_args(&mut bwrap_args, unreadable_root, &allowed_write_paths)?;
}
for writable_root in &sorted_writable_roots {
let root = writable_root.root.as_path();
let symlink_target = canonical_target_if_symlinked_path(root);
// If a denied ancestor was already masked, recreate any missing mount
// target parents before binding the narrower writable descendant.
if let Some(masking_root) = unreadable_roots
.iter()
.map(PathBuf::as_path)
.filter(|unreadable_root| root.starts_with(unreadable_root))
.max_by_key(|unreadable_root| path_depth(unreadable_root))
{
append_mount_target_parent_dir_args(&mut bwrap_args.args, root, masking_root);
}
let mount_root = symlink_target.as_deref().unwrap_or(root);
bwrap_args.args.push("--bind".to_string());
bwrap_args.args.push(path_to_string(mount_root));
bwrap_args.args.push(path_to_string(mount_root));
let mut read_only_subpaths: Vec<PathBuf> = writable_root
.read_only_subpaths
.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(
&mut read_only_subpaths,
root,
mount_root,
&protected_metadata_names,
);
if let Some(target) = &symlink_target {
read_only_subpaths = remap_paths_for_symlink_target(read_only_subpaths, root, target);
}
append_protected_create_targets_for_writable_root(
&mut bwrap_args,
&protected_metadata_names,
root,
symlink_target.as_deref(),
&read_only_subpaths,
);
read_only_subpaths.sort_by_key(|path| path_depth(path));
for subpath in read_only_subpaths {
append_read_only_subpath_args(&mut bwrap_args, &subpath, &allowed_write_paths)?;
}
let mut nested_unreadable_roots: Vec<PathBuf> = unreadable_roots
.iter()
.filter(|path| path.starts_with(root))
.cloned()
.collect();
if let Some(target) = &symlink_target {
nested_unreadable_roots =
remap_paths_for_symlink_target(nested_unreadable_roots, root, target);
}
nested_unreadable_roots.sort_by_key(|path| path_depth(path));
for unreadable_root in nested_unreadable_roots {
append_unreadable_root_args(&mut bwrap_args, &unreadable_root, &allowed_write_paths)?;
}
}
let mut rootless_unreadable_roots: Vec<PathBuf> = unreadable_roots
.iter()
.filter(|path| {
let unreadable_root = path.as_path();
!allowed_write_paths
.iter()
.any(|root| unreadable_root.starts_with(root) || root.starts_with(unreadable_root))
})
.cloned()
.collect();
rootless_unreadable_roots.sort_by_key(|path| path_depth(path));
for unreadable_root in rootless_unreadable_roots {
append_unreadable_root_args(&mut bwrap_args, &unreadable_root, &allowed_write_paths)?;
}
Ok(bwrap_args)
}