mirror of
https://github.com/openai/codex.git
synced 2026-10-19 08:16:48 +00:00
Add regression tests for case-variant metadata protection (#54250)
## What changed - Verify that freeform `apply_patch` calls and shell-intercepted patches cannot modify `.git/config` through `.GIT/config` when approvals are disabled, while an ordinary workspace file remains writable. - Add macOS Seatbelt coverage for uppercase protected metadata paths, including absent directories, existing directories with either casing, and case variants of the workspace root. Assert that metadata creation and modification are denied while sibling workspace writes succeed. The tests skip filesystems that do not support the case-insensitive aliases they exercise. GitOrigin-RevId: 5cf6b0c36eee348e1ba0db0bc2d3bff072458364
This commit is contained in:
@@ -17,11 +17,86 @@ use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::skip_if_remote;
|
||||
use core_test_support::skip_if_sandbox;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use pretty_assertions::assert_eq;
|
||||
use test_case::test_case;
|
||||
|
||||
#[test_case(false; "freeform")]
|
||||
#[test_case(true; "shell interception")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn case_variant_metadata_patch_cannot_write_without_approval(shell: bool) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_sandbox!(Ok(()));
|
||||
skip_if_remote!(Ok(()), "exercises native macOS case aliases");
|
||||
let server = start_mock_server().await;
|
||||
let test = test_codex()
|
||||
.with_config(|config| {
|
||||
config.workspace_roots = vec![config.cwd.clone()];
|
||||
config
|
||||
.permissions
|
||||
.set_workspace_roots(config.workspace_roots.clone());
|
||||
config.approvals_reviewer = ApprovalsReviewer::User;
|
||||
config
|
||||
.permissions
|
||||
.approval_policy
|
||||
.set(AskForApproval::Never)
|
||||
.expect("disable approvals in the regression");
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile(PermissionProfile::workspace_write())
|
||||
.expect("set workspace permissions");
|
||||
})
|
||||
.build_with_auto_env(&server)
|
||||
.await?;
|
||||
std::fs::create_dir_all(test.cwd_path().join(".git"))?;
|
||||
std::fs::write(test.cwd_path().join(".git/config"), "# original\n")?;
|
||||
std::fs::write(test.cwd_path().join("notes.txt"), "# original\n")?;
|
||||
if !test.cwd_path().join(".GIT/config").exists() {
|
||||
// The reported alias requires a case-insensitive filesystem.
|
||||
return Ok(());
|
||||
}
|
||||
let mut events = Vec::new();
|
||||
for (call_id, patch) in [
|
||||
(
|
||||
"control",
|
||||
"*** Begin Patch\n*** Update File: notes.txt\n@@\n-# original\n+# changed\n*** End Patch",
|
||||
),
|
||||
(
|
||||
"protected",
|
||||
"*** Begin Patch\n*** Update File: .GIT/config\n@@\n-# original\n+# changed\n*** End Patch",
|
||||
),
|
||||
] {
|
||||
let call = if shell {
|
||||
ev_exec_command_call(call_id, &format!("apply_patch <<'PATCH'\n{patch}\nPATCH"))
|
||||
} else {
|
||||
ev_apply_patch_custom_tool_call(call_id, patch)
|
||||
};
|
||||
events.push(sse(vec![
|
||||
ev_response_created(call_id),
|
||||
call,
|
||||
ev_completed(call_id),
|
||||
]));
|
||||
}
|
||||
events.push(sse(vec![
|
||||
ev_assistant_message("done", "done"),
|
||||
ev_completed("done"),
|
||||
]));
|
||||
let responses = mount_sse_sequence(&server, events).await;
|
||||
test.submit_text_turn("Apply the patches within workspace permissions.")
|
||||
.await?;
|
||||
assert_eq!(
|
||||
(
|
||||
std::fs::read_to_string(test.cwd_path().join("notes.txt"))?,
|
||||
std::fs::read_to_string(test.cwd_path().join(".git/config"))?,
|
||||
),
|
||||
("# changed\n".to_string(), "# original\n".to_string()),
|
||||
);
|
||||
assert_eq!(responses.requests().len(), 3);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(false; "freeform")]
|
||||
#[test_case(true; "shell interception")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -2628,6 +2628,91 @@ fn seatbelt_protects_aws_in_extra_writable_home() {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn seatbelt_protects_case_variants_of_metadata() {
|
||||
let mut results = Vec::new();
|
||||
let mut expected = Vec::new();
|
||||
for name in PROTECTED_METADATA_PATH_NAMES {
|
||||
let uppercase = name.to_ascii_uppercase();
|
||||
for existing_name in [None, Some(*name), Some(uppercase.as_str())] {
|
||||
for root_name in ["workspace", "WORKSPACE"] {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let workspace = tmp.path().join("workspace");
|
||||
let variant = workspace.join(&uppercase);
|
||||
let probe = workspace.join("probe");
|
||||
fs::create_dir(&workspace).expect("create workspace");
|
||||
if !tmp.path().join("WORKSPACE").is_dir() {
|
||||
// The reported aliases require a case-insensitive filesystem.
|
||||
return;
|
||||
}
|
||||
if let Some(existing_name) = existing_name {
|
||||
let existing = workspace.join(existing_name);
|
||||
fs::create_dir(&existing).expect("create metadata");
|
||||
fs::write(existing.join("config"), "original").expect("write config");
|
||||
}
|
||||
let policy_root = tmp.path().join(root_name);
|
||||
let policy = restricted_write_policy(&[policy_root.as_path()]);
|
||||
let command = if existing_name.is_some() {
|
||||
"printf ran > \"$2\" && printf changed > \"$1/config\""
|
||||
} else {
|
||||
"printf ran > \"$2\" && mkdir -p \"$1\" && printf changed > \"$1/config\""
|
||||
};
|
||||
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
|
||||
command: vec![
|
||||
"/bin/sh".to_string(),
|
||||
"-c".to_string(),
|
||||
command.to_string(),
|
||||
"sh".to_string(),
|
||||
variant.display().to_string(),
|
||||
probe.display().to_string(),
|
||||
],
|
||||
file_system_sandbox_policy: &policy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
sandbox_policy_cwd: &workspace,
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
environment_id: None,
|
||||
network: None,
|
||||
extra_allow_unix_sockets: &[],
|
||||
})
|
||||
.expect("build seatbelt command");
|
||||
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
|
||||
.args(args)
|
||||
.current_dir(&workspace)
|
||||
.output()
|
||||
.expect("execute seatbelt command");
|
||||
assert_eq!(
|
||||
fs::read_to_string(&probe).expect("read sibling probe"),
|
||||
"ran"
|
||||
);
|
||||
results.push((
|
||||
*name,
|
||||
existing_name.map(str::to_owned),
|
||||
root_name,
|
||||
output.status.success(),
|
||||
variant.exists(),
|
||||
fs::read_to_string(
|
||||
workspace
|
||||
.join(existing_name.unwrap_or(&uppercase))
|
||||
.join("config"),
|
||||
)
|
||||
.ok(),
|
||||
));
|
||||
expected.push((
|
||||
*name,
|
||||
existing_name.map(str::to_owned),
|
||||
root_name,
|
||||
false,
|
||||
existing_name.is_some(),
|
||||
existing_name.map(|_| "original".to_string()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(results, expected);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn seatbelt_protects_resolved_target_of_symlinked_metadata_directory() {
|
||||
|
||||
Reference in New Issue
Block a user