mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
Protect shell snapshots when credential brokerage is enabled (#43909)
## Why Shell startup files can introduce credentials or overwrite brokered dummy values. Snapshot capture and replay need to respect the command's sandbox and environment policy without persisting real credentials. ## What changed - Capture and validate brokered snapshots per command using its requested shell, working directory, login mode, sandbox, and environment policy. - Discover shell-initialized credentials and prepare snapshot exports for brokered replay. Preserve shell functions and aliases while preventing startup files from overwriting dummy credentials during replay. - Rebuild snapshots as credential broker state changes and retain each command's snapshot for the lifetime of its process. - Reject brokered POSIX shell execution when a protected snapshot cannot be created, and reject brokerage with `shell_zsh_fork`. Restore real credentials and remove brokerage markers for unsupported PowerShell and Cmd execution; escalated commands bypass protected snapshots. ## Testing Add regression coverage for credential discovery and redaction, environment filtering, readonly exports, shell function and heredoc replay, command working directories and login modes, sandbox enforcement, and missing or disabled snapshots. GitOrigin-RevId: 685356cb9e80f11cf69d053e4d239ee6ccf43a9e
This commit is contained in:
@@ -34,6 +34,7 @@ use super::UNIFIED_EXEC_OUTPUT_MAX_TOKENS;
|
||||
use super::UnifiedExecError;
|
||||
use super::head_tail_buffer::HeadTailBuffer;
|
||||
use super::process_state::ProcessState;
|
||||
use crate::shell_snapshot::ShellSnapshotFile;
|
||||
|
||||
const EARLY_EXIT_GRACE_PERIOD: Duration = Duration::from_millis(150);
|
||||
pub(crate) trait SpawnLifecycle: std::fmt::Debug + Send + Sync {
|
||||
@@ -99,6 +100,8 @@ pub(crate) struct UnifiedExecProcess {
|
||||
sandbox_type: SandboxType,
|
||||
timed_out: AtomicBool,
|
||||
_spawn_lifecycle: Option<SpawnLifecycleHandle>,
|
||||
// The shell may still need to replay this file after process startup returns.
|
||||
pub(crate) _shell_snapshot: Option<Arc<ShellSnapshotFile>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for UnifiedExecProcess {
|
||||
@@ -140,6 +143,7 @@ impl UnifiedExecProcess {
|
||||
sandbox_type,
|
||||
timed_out: AtomicBool::new(false),
|
||||
_spawn_lifecycle: spawn_lifecycle,
|
||||
_shell_snapshot: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
source: core/src/unified_exec/stdin_approval_tests.rs
|
||||
expression: reason
|
||||
---
|
||||
Send input to an existing terminal. This terminal retains additional permissions. It also has an internal plugin metrics write grant. The cwd is its launch directory; the terminal's current directory and state may have changed.
|
||||
Send input to an existing terminal. This terminal retains additional permissions. It also has an internal filesystem grant. The cwd is its launch directory; the terminal's current directory and state may have changed.
|
||||
|
||||
@@ -160,7 +160,7 @@ impl TerminalPermissions {
|
||||
};
|
||||
let mut reason = format!("Send input to an existing terminal. {authority}");
|
||||
if self.internal_permissions.is_some() {
|
||||
reason.push_str(" It also has an internal plugin metrics write grant.");
|
||||
reason.push_str(" It also has an internal filesystem grant.");
|
||||
}
|
||||
reason.push_str(" The cwd is its launch directory; the terminal's current directory and state may have changed.");
|
||||
if let Some(grants) = &self.additional_permissions {
|
||||
|
||||
@@ -163,7 +163,10 @@ async fn internal_grants_require_review_without_exposing_paths() -> anyhow::Resu
|
||||
environment.config_mut().permission_profile =
|
||||
PermissionProfileSnapshot::legacy(PermissionProfile::read_only());
|
||||
let grants = serde_json::from_value(json!({
|
||||
"file_system": {"write": [turn.config.cwd.join("private-metrics")]}
|
||||
"file_system": {
|
||||
"read": [turn.config.cwd.join("private-snapshot")],
|
||||
"write": [turn.config.cwd.join("private-metrics")]
|
||||
}
|
||||
}))?;
|
||||
let permissions = TerminalPermissions::for_launch(
|
||||
&environment,
|
||||
@@ -189,6 +192,34 @@ async fn internal_grants_require_review_without_exposing_paths() -> anyhow::Resu
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn readable_snapshot_does_not_require_stdin_approval() -> anyhow::Result<()> {
|
||||
let (_session, turn) = make_session_and_context().await;
|
||||
let mut environment = turn.environments.primary().expect("environment").clone();
|
||||
environment.config_mut().permission_profile =
|
||||
PermissionProfileSnapshot::legacy(PermissionProfile::read_only());
|
||||
let snapshot = turn.config.cwd.join("shell-snapshot.sh");
|
||||
let grant = crate::shell_snapshot::snapshot_read_permissions(
|
||||
&snapshot,
|
||||
&environment.permission_profile_with_workspace_roots(),
|
||||
environment.cwd(),
|
||||
);
|
||||
assert_eq!(grant, None);
|
||||
let permissions = TerminalPermissions::for_launch(
|
||||
&environment,
|
||||
&turn,
|
||||
TerminalSandboxSource::Native,
|
||||
SandboxPermissions::UseDefault,
|
||||
/*additional_permissions*/ None,
|
||||
grant.as_ref(),
|
||||
);
|
||||
assert_eq!(
|
||||
permissions.review_requirement(&permissions.policy, environment.permission_profile()),
|
||||
Ok(SandboxPermissions::UseDefault)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case::test_case(TerminalSandboxSource::Native, SandboxPermissions::RequireEscalated; "native_disabled_sandbox_needs_review_when_enabled")]
|
||||
#[test_case::test_case(TerminalSandboxSource::Executor, SandboxPermissions::UseDefault; "executor_keeps_its_restricted_token_default")]
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user