Bubblewrap命令构造
Bubblewrap argv 不是把 readable roots 变成 --ro-bind、writable roots 变成 --bind 就结束。真正困难的是重叠权限:一个 writable root 内可能有 read-only metadata,denied parent 内又可能有更窄的 writable child,unreadable glob 只能在启动前展开成已有路径,missing protected path 还必须阻止首次创建。所有这些约束都依赖 mount 顺序。
rust-v0.150.0 的构造器还处理两个运行时竞态。第一,writable path 中的 symlink 可以在 policy 生成后被进程替换,所以 read-only/deny path 穿过 writable symlink 时必须 fail closed。第二,Bubblewrap mount target 必须存在,构造器可能临时创建 empty file/directory;多个并发 sandbox 共享这些 target 时,需要 registry、inode identity 和清理协议,不能由某个进程随意删除。
本文承接Linux Seccomp与Namespace和Linux Landlock策略。前文解释 namespace 与 inner Seccomp,本篇只聚焦 filesystem mount argv、launcher compatibility 和清理 sidecar;不重复 proxy bridge 或 syscall filter。
1. 返回模型
1.1 BwrapArgs
构造结果不仅是 argv。preserved_files 保存需要跨 exec 的 FD;synthetic_mount_targets 记录为了满足 mount target 存在性而创建或接管的空文件/目录;protected_create_targets 记录 policy 要求“不应出现”的 metadata path,outer supervisor 会监视并清理。
源码位置:codex-rs/linux-sandbox/src/bwrap.rs :: BwrapArgs, SyntheticMountTarget
#[derive(Debug)]
pub(crate) struct BwrapArgs {
pub args: Vec<String>,
pub preserved_files: Vec<File>,
pub synthetic_mount_targets: Vec<SyntheticMountTarget>,
pub protected_create_targets: Vec<ProtectedCreateTarget>,
}
#[derive(Debug, Clone)]
pub(crate) struct SyntheticMountTarget {
path: PathBuf,
kind: SyntheticMountTargetKind,
pre_existing_path: Option<FileIdentity>,
}FileIdentity 保存 device/inode。清理时不仅检查“文件为空”,还检查是不是构造器自己创建或替换的那个 inode,避免删除原来就存在的真实空文件。
1.2 快路径
full disk write 且没有 unreadable glob 时,FullAccess network 可以直接返回原 command;网络需要 Isolated/ProxyOnly 时仍必须包装,以创建 network namespace。只要存在 unreadable glob,即使 full disk write 也要构造具体 mask。
源码位置:codex-rs/linux-sandbox/src/bwrap.rs :: create_bwrap_command_args
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() {
return if options.network_mode == BwrapNetworkMode::FullAccess {
Ok(BwrapArgs {
args: command,
preserved_files: Vec::new(),
synthetic_mount_targets: Vec::new(),
protected_create_targets: Vec::new(),
})
} else {
Ok(create_bwrap_flags_full_filesystem(command, options))
};
}
create_bwrap_flags(
command,
file_system_sandbox_policy,
sandbox_policy_cwd,
command_cwd,
options,
)2. 根视图
2.1 Full read
full disk read 从 --ro-bind / / 开始,再用 --dev /dev 建立 minimal device tree。/dev 必须在 writable /dev/* bind 之前建立,否则后来的 device mount 会覆盖显式 writable subpath。
源码位置:codex-rs/linux-sandbox/src/bwrap.rs :: create_filesystem_args
let args = if file_system_sandbox_policy.has_full_disk_read_access() {
// Read-only root, then mount a minimal device tree.
// `/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(),
]2.2 Restricted read
restricted read 默认从 --tmpfs / 空根开始,只挂载 approved readable roots 和 minimal /dev。如果 readable roots 显式包含 /,则切回 read-only root baseline,再在后面重施 deny carveout。
源码位置:codex-rs/linux-sandbox/src/bwrap.rs :: create_filesystem_args
} 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()),
);
}
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;
}
args.push("--ro-bind".to_string());
args.push(path_to_string(&root));
args.push(path_to_string(&root));
}
}missing readable root 被跳过;missing protected path 则在 writable root 处理中使用 synthetic mask,二者语义不同。
3. Glob预展开
3.1 Search root
Bubblewrap 不理解 glob,只能 mask concrete paths。构造器把每个 pattern 按首个 glob metacharacter 拆成 static search root 和相对 glob;root-level pattern(如 /**/*.env)会失败关闭,因为启动时扫描整个 / 过宽。
源码位置:codex-rs/linux-sandbox/src/bwrap.rs :: expand_unreadable_globs_with_ripgrep, split_pattern_for_ripgrep
let mut patterns_by_search_root: BTreeMap<AbsolutePathBuf, Vec<String>> = BTreeMap::new();
for pattern in patterns {
let Some((search_root, glob)) = split_pattern_for_ripgrep(pattern, cwd) else {
return Err(CodexErr::Fatal(format!(
"unreadable glob `{pattern}` cannot be safely expanded; use a pattern with a non-root directory prefix"
)));
};
if search_root.as_path().is_dir() {
patterns_by_search_root
.entry(search_root)
.or_default()
.push(glob);
}
}同一 search root 下的多个 pattern 合并为一次扫描,减少启动成本。每个 logical match 还会加入 canonical symlink target,防止通过可读 symlink 指向被 mask 文件的真实位置。
3.2 Glob扫描
优先使用 rg --files --hidden --no-ignore --null,可选 --max-depth。exit 1 且 stderr 为空表示无匹配;rg 不存在时使用内部 globset walker;其它失败保持 fatal,不能因为扫描工具异常而静默减弱 deny-read。
源码位置:codex-rs/linux-sandbox/src/bwrap.rs :: ripgrep_files, glob_files
let mut command = Command::new("rg");
command
.arg("--files")
.arg("--hidden")
.arg("--no-ignore")
.arg("--null");
if let Some(max_depth) = max_depth {
command.arg("--max-depth").arg(max_depth.to_string());
}
for glob in globs {
command.arg("--glob").arg(glob);
}
command.arg("--").arg(search_root);
let output = match command.output() {
Ok(output) => output,
Err(err) if err.kind() == io::ErrorKind::NotFound => {
return glob_files(search_root, globs, max_depth);
}
Err(err) => return Err(err.into()),
};
if !output.status.success() {
if output.status.code() == Some(1) && output.stderr.is_empty() {
return Ok(Vec::new());
}
return Err(CodexErr::Fatal(format!(
"ripgrep unreadable glob scan failed for {}: {}",
search_root.display(),
String::from_utf8_lossy(&output.stderr)
)));
}expanded path 总数上限为 8192;超过上限同样失败关闭。glob_scan_max_depth=0 则显式不展开现有 match,这一配置会改变 Bubblewrap mask 覆盖面,因此应与 direct filesystem tools 的运行时 glob enforcement 区分。
4. 重叠权限排序
4.1 六阶段顺序
源码注释给出 mount precedence 的真实算法:
- 建立 read-only root 或空根;
- 建 minimal
/dev; - 先 mask 位于所有 writable root 外部的 denied ancestor;
- 按路径深度 bind writable roots;
- 在每个 writable root 中重新施加 read-only metadata;
- 最后 mask nested 与 unrelated deny paths。
源码位置:codex-rs/linux-sandbox/src/bwrap.rs :: create_filesystem_args
/// The mount order is important:
/// 1. Full-read policies, and restricted policies that explicitly read `/`,
/// use `--ro-bind / /`; other restricted-read policies start from
/// `--tmpfs /` and layer scoped `--ro-bind` mounts.
/// 2. `--dev /dev` mounts a minimal writable `/dev` with standard device nodes.
/// 3. Unreadable ancestors of writable roots are masked before their child
/// mounts are rebound so nested writable carveouts can be reopened safely.
/// 4. `--bind <root> <root>` re-enables writes for allowed roots.
/// 5. `--ro-bind <subpath> <subpath>` re-applies read-only protections under
/// 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.4.2 Deny父与Write子
若 /blocked denied,但 /blocked/allowed writable,构造器先用 execute-only tmpfs mask /blocked,再创建 child mount target、remount parent read-only,最后 bind writable child。111 允许遍历到显式重开的 descendant,但不能列出 denied parent 内容。
源码位置:codex-rs/linux-sandbox/src/bwrap.rs :: append_existing_unreadable_path_args
if unreadable_root.is_dir() {
let mut writable_descendants: Vec<&Path> = allowed_write_paths
.iter()
.map(PathBuf::as_path)
.filter(|path| *path != unreadable_root && path.starts_with(unreadable_root))
.collect();
bwrap_args.args.push("--perms".to_string());
bwrap_args.args.push(if writable_descendants.is_empty() {
"000".to_string()
} else {
"111".to_string()
});
bwrap_args.args.push("--tmpfs".to_string());
bwrap_args.args.push(path_to_string(unreadable_root));
writable_descendants.sort_by_key(|path| path_depth(path));
for writable_descendant in writable_descendants {
append_mount_target_parent_dir_args(
&mut bwrap_args.args,
writable_descendant,
unreadable_root,
);
}
bwrap_args.args.push("--remount-ro".to_string());
bwrap_args.args.push(path_to_string(unreadable_root));
return Ok(());
}测试 split_policy_reenables_writable_subpaths_after_unreadable_parent 精确比较四个 index:deny mask < child dir创建 < parent remount-ro < child bind。
4.3 Read父与Write子
同理,/docs read-only 而 /docs/public writable 时,writable roots 按 path depth 排序;parent 的 --ro-bind 必须早于 nested child 的 --bind,否则 parent remount 会把 child 再次封闭。
测试 split_policy_reopens_writable_child_after_read_only_parent 断言 docs_ro_index < docs_public_rw_index,这是 mount sequence 而非数据结构排序的验证。
5. Metadata保护
5.1 已存在路径
writable root 的 protected metadata names 会加入 read_only_subpaths。已存在 .git、.codex 或 .agents 通过 --ro-bind 重新覆盖 writable root;如果 writable root 本身是 symlink,subpath 会映射到 canonical target。
源码位置:codex-rs/linux-sandbox/src/bwrap.rs :: append_metadata_path_masks_for_writable_root, append_protected_create_targets_for_writable_root
fn append_metadata_path_masks_for_writable_root(
read_only_subpaths: &mut Vec<PathBuf>,
root: &Path,
protected_metadata_names: &[String],
) {
for name in protected_metadata_names {
let path = root.join(name);
if !read_only_subpaths.iter().any(|subpath| subpath == &path) {
read_only_subpaths.push(path);
}
}
}
fn append_protected_create_targets_for_writable_root(
bwrap_args: &mut BwrapArgs,
protected_metadata_names: &[String],
root: &Path,
symlink_target: Option<&Path>,
read_only_subpaths: &[PathBuf],
) {
for name in protected_metadata_names {
let mut path = root.join(name);
if let Some(target) = symlink_target
&& let Ok(relative_path) = path.strip_prefix(root)
{
path = target.join(relative_path);
}
if read_only_subpaths.iter().any(|subpath| subpath == &path) || path.exists() {
continue;
}
bwrap_args
.protected_create_targets
.push(ProtectedCreateTarget::missing(&path));
}
}5.2 Missing path
Bubblewrap mount target 必须存在。missing protected metadata directory 使用 mode 555 tmpfs + remount-ro;其它 missing path 使用 /dev/null FD 的 --ro-bind-data 创建空文件 mask。构造器同时记录 synthetic target,outer supervisor 后续按 inode 与并发 owner清理。
源码位置:codex-rs/linux-sandbox/src/bwrap.rs :: append_missing_read_only_subpath_args, append_missing_empty_file_bind_data_args
fn append_missing_read_only_subpath_args(bwrap_args: &mut BwrapArgs, path: &Path) -> Result<()> {
if path.file_name().is_some_and(is_protected_metadata_name) {
append_empty_directory_args(bwrap_args, path);
bwrap_args
.synthetic_mount_targets
.push(SyntheticMountTarget::missing_empty_directory(path));
return Ok(());
}
append_missing_empty_file_bind_data_args(bwrap_args, path)
}
fn append_missing_empty_file_bind_data_args(bwrap_args: &mut BwrapArgs, path: &Path) -> Result<()> {
append_empty_file_bind_data_args(bwrap_args, path)?;
bwrap_args
.synthetic_mount_targets
.push(SyntheticMountTarget::missing(path));
Ok(())
}第一 missing component 会被 mask,而不是只 mask 最终 leaf。这样 missing/.codex/config.toml 不能通过先创建 ancestor 绕过。
6. Symlink边界
6.1 Writable symlink
read-only 或 deny path 如果穿过 sandbox 内可写的 symlink,启动时 canonicalize 只会保护旧 target。进程可以替换 symlink 后访问新 target,因此构造器扫描 logical path,发现 writable root 下的 symlink component 就返回 Fatal error。
源码位置:codex-rs/linux-sandbox/src/bwrap.rs :: first_writable_symlink_component_in_path, append_read_only_subpath_args
fn first_writable_symlink_component_in_path(
target_path: &Path,
allowed_write_paths: &[PathBuf],
) -> Option<PathBuf> {
let mut current = PathBuf::new();
for component in target_path.components() {
match component {
Component::RootDir => {
current.push(Path::new("/"));
continue;
}
Component::CurDir => continue,
Component::ParentDir => {
current.pop();
continue;
}
Component::Normal(part) => current.push(part),
Component::Prefix(_) => continue,
}
let metadata = match std::fs::symlink_metadata(¤t) {
Ok(metadata) => metadata,
Err(_) => break,
};
if metadata.file_type().is_symlink()
&& is_within_allowed_write_paths(¤t, allowed_write_paths)
{
return Some(current);
}
}
None
}append_unreadable_root_args 使用同一检查,因此 read-only 与 deny 两类约束都不会接受可变 symlink snapshot。
6.2 Symlinked root
writable root 本身若是 symlink,构造器可以 bind canonical target,并将 logical root 与 target 都加入 allowed_write_paths。这与“约束 path 穿过可写 symlink”不同:前者由 policy 显式授予整个 root,后者是 root 内部可被进程改写的中间跳转。
7. Launcher兼容
7.1 Capability probe
launcher 优先使用 PATH 中可信 system bwrap,并检查 --as-pid-1、--perms、--argv0 和 --ro-bind-fd capability。缺少必要 --perms 时拒绝该 system binary;找不到合格 system bwrap 才尝试 bundled binary。
源码位置:codex-rs/linux-sandbox/src/launcher.rs :: SystemBwrapCapabilities, preferred_bwrap_launcher
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct SystemBwrapCapabilities {
supports_argv0: bool,
supports_perms: bool,
supports_ro_bind_fd: bool,
}
fn preferred_bwrap_launcher() -> BubblewrapLauncher {
static LAUNCHER: OnceLock<BubblewrapLauncher> = OnceLock::new();
LAUNCHER
.get_or_init(|| {
if let Some(path) = find_system_bwrap_in_path()
&& let Some(launcher) = system_bwrap_launcher_for_path(&path)
{
return BubblewrapLauncher::System(launcher);
}
match bundled_bwrap::launcher() {
Some(launcher) => BubblewrapLauncher::Bundled(launcher),
None => BubblewrapLauncher::Unavailable,
}
})
.clone()
}7.2 argv0兼容
bwrap 支持 --argv0 时,在 command separator 前插入 --argv0 codex-linux-sandbox;旧版不支持时,只替换第一个 inner helper command path,不修改用户 command 中后续出现的 current executable。
源码位置:codex-rs/linux-sandbox/src/linux_run_main.rs :: apply_inner_command_argv0_for_launcher
let command_separator_index = argv
.iter()
.position(|arg| arg == "--")
.unwrap_or_else(|| panic!("bubblewrap argv is missing command separator '--'"));
if supports_argv0 {
argv.splice(
command_separator_index..command_separator_index,
["--argv0".to_string(), CODEX_LINUX_SANDBOX_ARG0.to_string()],
);
return;
}
let command_index = command_separator_index + 1;
let Some(command) = argv.get_mut(command_index) else {
panic!("bubblewrap argv is missing inner command after '--'");
};
*command = argv0_fallback_command;7.3 FD mount兼容
旧 system bwrap 不支持 --ro-bind-fd 时,launcher 把它翻译成 /proc/self/fd/N 的普通 --ro-bind,并向 trusted inner stage 注入 --verify-fd-mount N:DEST。如果 argv 没有 --apply-seccomp-then-exec,则拒绝 translation,避免未认证 descriptor mount 直接进入用户 command。
源码位置:codex-rs/linux-sandbox/src/launcher.rs :: translate_legacy_bwrap_fd_mounts
verification_args.push("--verify-fd-mount".to_string());
verification_args.push(format!("{fd}:{destination}"));
verified_fds.push(fd);
argv[argument_index] = "--ro-bind".to_string();
argv[argument_index + 1] = format!("/proc/self/fd/{fd}");
if !argv[inner_command + 1..]
.iter()
.take_while(|argument| argument.as_str() != "--")
.any(|argument| argument == "--apply-seccomp-then-exec")
{
return Err("descriptor-backed mounts require the trusted inner sandbox stage".to_string());
}
argv.splice(inner_command + 1..inner_command + 1, verification_args);8. 测试边界
bwrap.rs 当前包含 31 项 unit test,覆盖 default glob depth、root view、platform defaults、writable/read-only/deny overlap、missing metadata、symlink fail-closed、canonical cwd、glob depth/canonical target 和 root-prefix glob rejection。Linux integration suite 另有 27 项 async test,实际运行 Bubblewrap helper验证 read/write、metadata、symlink和glob行为。
这些测试由 Linux target 条件编译。当前非 Linux 主机只能执行共享 helper argv 测试,不能把 0 runnable Linux tests计为 mount namespace通过。Linux target建议执行:
cargo test -p codex-linux-sandbox --lib -- --nocapture --test-threads=1
cargo test -p codex-linux-sandbox --test all -- --nocapture --test-threads=1
cargo test -p codex-sandboxing --lib landlock::tests:: -- --nocapture --test-threads=1关键断言包括:
- denied parent先 mask,writable child target随后创建并 bind;
- read-only parent remount index 小于 nested writable child bind index;
- missing protected metadata生成 synthetic target和空目录/文件 mask;
- writable symlink穿越使 sandbox construction失败;
- unreadable glob按 max depth展开,并同时 mask symlink canonical target;
- root-prefix glob拒绝启动,而不是扫描整个 filesystem;
- old bwrap FD translation必须注入 inner verification。
这些断言证明 argv算法与Linux fixture行为;边界在于 mount能否执行仍依赖 user namespace、Bubblewrap capability和container policy。启动前 glob expansion只覆盖当时已有路径,后续新建文件仍需要其它 runtime read enforcement配合,不能把 concrete mask当成动态 glob watcher。
9. 继续阅读
阅读源码时可以从 create_bwrap_command_args 进入 create_filesystem_args,先标出 root baseline,再追踪 unreadable_ancestors_of_writable_roots、sorted_writable_roots、nested/rootless unreadable三个集合的执行顺序。之后阅读 missing path、symlink和launcher translation,才能解释一条最终 argv为什么以特定顺序出现。
下一篇Linux沙箱降级与诊断将分析 bwrap缺失、user namespace不可用、WSL1、proc mount失败、helper digest和sandbox denial的诊断边界。
