Files
codex/codex-rs/linux-sandbox/tests/suite/nested_metadata_tests.rs
T
jif 8bbe8f8702 Fix read-only metadata mount ordering for nested writable roots (#47623)
## Why

Nested writable roots must allow the Linux sandbox to start while preserving read-only metadata protections, including when a writable directory sits beneath a read-only `.codex` directory.

## What changed

Defer read-only subpath mounts shared with a deeper writable root until that root is bound. Keep symlink target remapping and writable-symlink validation for deferred paths so mount ordering does not bypass those checks.

## Testing

Add regression coverage for mounting `.git`, `.agents`, and `.codex` metadata exactly once after the nested writable bind. Add sandbox integration cases for ordinary and symlinked roots, using both parent and nested working directories, to verify startup, allowed writes, metadata protection, and temporary mountpoint cleanup.

GitOrigin-RevId: 45e439eaf537e10bfa8b839bad816b0401fb65aa
2026-09-23 18:51:35 +00:00

134 lines
4.9 KiB
Rust

//! Nested writable roots must start while preserving read-only metadata protections.
use super::LONG_TIMEOUT_MS;
use super::create_env_from_core_vars;
use super::run_cmd_result_with_permission_profile_for_cwd;
use super::should_skip_bwrap_tests;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
enum RootLayout {
Directory,
Symlink,
}
#[test_case::test_case(RootLayout::Directory, ""; "home_cwd")]
#[test_case::test_case(RootLayout::Directory, ".codex/visualizations/thread"; "visualization_cwd")]
#[test_case::test_case(RootLayout::Symlink, ""; "symlink_home_cwd")]
#[test_case::test_case(RootLayout::Symlink, ".codex/visualizations/thread"; "symlink_visualization_cwd")]
#[tokio::test]
async fn sandbox_starts_with_nested_writable_metadata(layout: RootLayout, relative_cwd: &str) {
if should_skip_bwrap_tests().await {
eprintln!("skipping bwrap test: bwrap sandbox prerequisites are unavailable");
return;
}
let temp = tempfile::tempdir().expect("tempdir");
let home =
AbsolutePathBuf::from_absolute_path(temp.path().join("home")).expect("absolute home");
match layout {
RootLayout::Directory => std::fs::create_dir(&home).expect("create home"),
RootLayout::Symlink => {
let target = temp.path().join("real-home");
std::fs::create_dir(&target).expect("create real home");
std::os::unix::fs::symlink(target, &home).expect("create home alias");
}
}
let codex_home = home.join(".codex");
let visualization = codex_home.join("visualizations/thread");
std::fs::create_dir_all(&visualization).expect("create visualization directory");
let protected_file = codex_home.join("config.toml");
std::fs::write(&protected_file, "unchanged").expect("write protected config");
let mut entries = vec![
FileSystemSandboxEntry::new(
FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
FileSystemAccessMode::Read,
),
FileSystemSandboxEntry::new(home.clone().into(), FileSystemAccessMode::Write),
FileSystemSandboxEntry::new(codex_home.clone().into(), FileSystemAccessMode::Read),
FileSystemSandboxEntry::new(visualization.clone().into(), FileSystemAccessMode::Write),
];
// Remote clients expand generated workspace metadata rules to concrete
// paths, including paths that do not yet exist on the executor.
for name in [".git", ".agents", ".codex"] {
entries.push(FileSystemSandboxEntry::skip_missing_path(
visualization.join(name).into(),
FileSystemAccessMode::Read,
));
}
let permission_profile = PermissionProfile::from_runtime_permissions(
&FileSystemSandboxPolicy::restricted(entries),
NetworkSandboxPolicy::Enabled,
);
let script = r#"
set -eu
workspace="$1"
visualization="$2"
printf home > "$workspace/allowed.txt"
printf visualization > "$visualization/allowed.txt"
if (rm "$workspace") 2>/dev/null; then
exit 13
fi
if (printf changed > "$workspace/.codex/config.toml") 2>/dev/null; then
exit 10
fi
if (touch "$workspace/.codex/forbidden.txt") 2>/dev/null; then
exit 11
fi
for name in .git .agents .codex; do
test -d "$visualization/$name"
if (touch "$visualization/$name/forbidden.txt") 2>/dev/null; then
exit 12
fi
done
printf nested-metadata-protected
"#;
let output = run_cmd_result_with_permission_profile_for_cwd(
&[
"/bin/sh",
"-c",
script,
"nested-metadata-test",
home.to_str().expect("UTF-8 home"),
visualization
.to_str()
.expect("UTF-8 visualization directory"),
],
home.join(relative_cwd),
permission_profile,
create_env_from_core_vars(),
LONG_TIMEOUT_MS,
/*use_legacy_landlock*/ false,
)
.await
.expect("nested writable root should start under bubblewrap");
assert_eq!(
(output.exit_code, output.stdout.text, output.stderr.text),
(0, "nested-metadata-protected".to_string(), String::new())
);
for (path, expected) in [
(home.join("allowed.txt"), "home"),
(visualization.join("allowed.txt"), "visualization"),
(protected_file, "unchanged"),
] {
assert_eq!(std::fs::read_to_string(path).expect("read file"), expected);
}
for name in [".git", ".agents", ".codex"] {
assert!(
!visualization.join(name).exists(),
"temporary {name} mountpoint should be cleaned up"
);
}
}