From 948aaa703d888213a02aa544becd55745e37dcc3 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 25 Sep 2025 00:25:37 -0700 Subject: [PATCH] feat: add support for CODEX_SECURE_MODE=1 to restrict process observability --- codex-rs/Cargo.lock | 36 +++++- codex-rs/Cargo.toml | 2 + codex-rs/cli/Cargo.toml | 15 +++ codex-rs/cli/src/main.rs | 29 +++++ codex-rs/cli/src/pre_main_hardening.rs | 162 +++++++++++++++++++++++++ 5 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 codex-rs/cli/src/pre_main_hardening.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 5b74ec3263..bf3b190e29 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -56,7 +56,7 @@ checksum = "8fac2ce611db8b8cee9b2aa886ca03c924e9da5e5295d0dbd0526e5d0b0710f7" dependencies = [ "allocative_derive", "bumpalo", - "ctor", + "ctor 0.1.26", "hashbrown 0.14.5", "num-bigint", ] @@ -641,6 +641,8 @@ dependencies = [ "codex-protocol", "codex-protocol-ts", "codex-tui", + "ctor 0.5.0", + "libc", "owo-colors", "predicates", "pretty_assertions", @@ -650,6 +652,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "windows-sys 0.60.2", ] [[package]] @@ -1213,6 +1216,22 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ctor" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" + [[package]] name = "darling" version = "0.20.11" @@ -1463,6 +1482,21 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "dtor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e58a0764cddb55ab28955347b45be00ade43d4d6f3ba4bf3dc354e4ec9432934" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[package]] name = "dupe" version = "0.9.1" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 57e606cbb9..cb84eb9f90 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -72,6 +72,7 @@ clap = "4" clap_complete = "4" color-eyre = "0.6.3" crossterm = "0.28.1" +ctor = "0.5.0" derive_more = "2" diffy = "0.4.2" dirs = "6" @@ -149,6 +150,7 @@ walkdir = "2.5.0" webbrowser = "1.0" which = "6" wildmatch = "2.5.0" +windows-sys = "0.60.2" wiremock = "0.6" [workspace.lints] diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index c410e09a82..5aeae8d2fb 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -28,6 +28,7 @@ codex-mcp-server = { workspace = true } codex-protocol = { workspace = true } codex-protocol-ts = { workspace = true } codex-tui = { workspace = true } +ctor = { workspace = true } owo-colors = { workspace = true } serde_json = { workspace = true } supports-color = { workspace = true } @@ -41,6 +42,20 @@ tokio = { workspace = true, features = [ tracing = { workspace = true } tracing-subscriber = { workspace = true } +[target.'cfg(target_os = "linux")'.dependencies] +libc = { workspace = true } + +[target.'cfg(target_os = "macos")'.dependencies] +libc = { workspace = true } + +[target.'cfg(windows)'.dependencies] +windows-sys = { workspace = true, features = [ + "Win32_Foundation", + "Win32_System_Diagnostics_Debug", + "Win32_System_ErrorReporting", + "Win32_System_SystemServices", +] } + [dev-dependencies] assert_cmd = { workspace = true } predicates = { workspace = true } diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index df757b0cc5..7e5cb6173b 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -21,6 +21,7 @@ use std::path::PathBuf; use supports_color::Stream; mod mcp_cmd; +mod pre_main_hardening; use crate::mcp_cmd::McpCli; use crate::proto::ProtoCli; @@ -194,6 +195,34 @@ fn print_exit_messages(exit_info: AppExitInfo) { } } +pub(crate) const CODEX_SECURE_MODE_ENV_VAR: &str = "CODEX_SECURE_MODE"; + +/// As early as possible in the process lifecycle, apply hardening measures +/// if the CODEX_SECURE_MODE environment variable is set to "1". +#[ctor::ctor] +fn pre_main_hardening() { + let secure_mode = match std::env::var(CODEX_SECURE_MODE_ENV_VAR) { + Ok(value) => value, + Err(_) => return, + }; + + if secure_mode == "1" { + #[cfg(target_os = "linux")] + crate::pre_main_hardening::pre_main_hardening_linux(); + + #[cfg(target_os = "macos")] + crate::pre_main_hardening::pre_main_hardening_macos(); + + #[cfg(windows)] + crate::pre_main_hardening::pre_main_hardening_windows(); + } + + // Always clear this env var so child processes don't inherit it. + unsafe { + std::env::remove_var(CODEX_SECURE_MODE_ENV_VAR); + } +} + fn main() -> anyhow::Result<()> { arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { cli_main(codex_linux_sandbox_exe).await?; diff --git a/codex-rs/cli/src/pre_main_hardening.rs b/codex-rs/cli/src/pre_main_hardening.rs new file mode 100644 index 0000000000..c04aaf5a10 --- /dev/null +++ b/codex-rs/cli/src/pre_main_hardening.rs @@ -0,0 +1,162 @@ +#[cfg(target_os = "linux")] +const PRCTL_FAILED_EXIT_CODE: i32 = 5; + +#[cfg(target_os = "macos")] +const PTRACE_DENY_ATTACH_FAILED_EXIT_CODE: i32 = 6; + +#[cfg(any(target_os = "linux", target_os = "macos"))] +const SET_RLIMIT_CORE_FAILED_EXIT_CODE: i32 = 7; + +#[cfg(windows)] +const DEBUG_SET_PROCESS_KILL_ON_EXIT_FAILED_EXIT_CODE: i32 = 8; + +#[cfg(windows)] +const WER_SET_FLAGS_FAILED_EXIT_CODE: i32 = 9; + +#[cfg(windows)] +const WINDOWS_PROFILER_ENV_VARS: [&str; 11] = [ + "COR_ENABLE_PROFILING", + "COR_PROFILER", + "COR_PROFILER_PATH", + "COR_PROFILER_PATH_32", + "COR_PROFILER_PATH_64", + "CORECLR_ENABLE_PROFILING", + "CORECLR_PROFILER", + "CORECLR_PROFILER_PATH", + "CORECLR_PROFILER_PATH_32", + "CORECLR_PROFILER_PATH_64", + "DOTNET_STARTUP_HOOKS", +]; + +#[cfg(target_os = "linux")] +pub(crate) fn pre_main_hardening_linux() { + // Disable ptrace attach / mark process non-dumpable. + unsafe { + if libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0) != 0 { + eprintln!( + "ERROR: prctl(PR_SET_DUMPABLE, 0) failed: {}", + std::io::Error::last_os_error() + ); + std::process::exit(PRCTL_FAILED_EXIT_CODE); + } + } + + // For "defense in depth," set the core file size limit to 0. + unsafe { + let rlim = libc::rlimit { + rlim_cur: 0, + rlim_max: 0, + }; + if libc::setrlimit(libc::RLIMIT_CORE, &rlim) != 0 { + eprintln!( + "ERROR: setrlimit(RLIMIT_CORE) failed: {}", + std::io::Error::last_os_error() + ); + std::process::exit(SET_RLIMIT_CORE_FAILED_EXIT_CODE); + } + } + + // Official Codex releases are MUSL-linked, which means that variables such + // as LD_PRELOAD are ignored anyway, but just to be sure, clear them here. + let ld_keys: Vec = std::env::vars() + .filter_map(|(key, _)| { + if key.starts_with("LD_") { + Some(key) + } else { + None + } + }) + .collect(); + + for key in ld_keys { + unsafe { + std::env::remove_var(key); + } + } +} + +#[cfg(target_os = "macos")] +pub(crate) fn pre_main_hardening_macos() { + unsafe { + if libc::ptrace(libc::PT_DENY_ATTACH, 0, std::ptr::null_mut(), 0) == -1 { + eprintln!( + "ERROR: ptrace(PT_DENY_ATTACH) failed: {}", + std::io::Error::last_os_error() + ); + std::process::exit(PTRACE_DENY_ATTACH_FAILED_EXIT_CODE); + } + } + + unsafe { + let rlim = libc::rlimit { + rlim_cur: 0, + rlim_max: 0, + }; + if libc::setrlimit(libc::RLIMIT_CORE, &rlim) != 0 { + eprintln!( + "ERROR: setrlimit(RLIMIT_CORE) failed: {}", + std::io::Error::last_os_error() + ); + std::process::exit(SET_RLIMIT_CORE_FAILED_EXIT_CODE); + } + } + + // Remove all DYLD_ environment variables, which can be used to + // subvert library loading. + let dyld_keys: Vec = std::env::vars() + .filter_map(|(key, _)| { + if key.starts_with("DYLD_") { + Some(key) + } else { + None + } + }) + .collect(); + + for key in dyld_keys { + unsafe { + std::env::remove_var(key); + } + } +} + +#[cfg(windows)] +pub(crate) fn pre_main_hardening_windows() { + unsafe { + use windows_sys::Win32::Foundation::TRUE; + use windows_sys::Win32::System::Diagnostics::Debug::DebugSetProcessKillOnExit; + use windows_sys::Win32::System::Diagnostics::Debug::SEM_FAILCRITICALERRORS; + use windows_sys::Win32::System::Diagnostics::Debug::SEM_NOGPFAULTERRORBOX; + use windows_sys::Win32::System::Diagnostics::Debug::SEM_NOOPENFILEERRORBOX; + use windows_sys::Win32::System::Diagnostics::Debug::SetErrorMode; + use windows_sys::Win32::System::ErrorReporting::WER_FAULT_REPORTING_FLAG_NOHEAP; + use windows_sys::Win32::System::ErrorReporting::WER_FAULT_REPORTING_FLAG_QUEUE; + use windows_sys::Win32::System::ErrorReporting::WER_FAULT_REPORTING_NO_UI; + use windows_sys::Win32::System::ErrorReporting::WerSetFlags; + + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX); + + if DebugSetProcessKillOnExit(TRUE) == 0 { + eprintln!( + "ERROR: DebugSetProcessKillOnExit(TRUE) failed: {}", + std::io::Error::last_os_error() + ); + std::process::exit(DEBUG_SET_PROCESS_KILL_ON_EXIT_FAILED_EXIT_CODE); + } + + let flags = WER_FAULT_REPORTING_NO_UI + | WER_FAULT_REPORTING_FLAG_NOHEAP + | WER_FAULT_REPORTING_FLAG_QUEUE; + let hr = WerSetFlags(flags); + if hr < 0 { + eprintln!("ERROR: WerSetFlags() failed with HRESULT {hr:#010x}"); + std::process::exit(WER_SET_FLAGS_FAILED_EXIT_CODE); + } + } + + for var in WINDOWS_PROFILER_ENV_VARS { + unsafe { + std::env::remove_var(var); + } + } +}