mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
fix(macos): scrub malloc diagnostic env
This commit is contained in:
@@ -6,6 +6,7 @@ import { existsSync, realpathSync } from "fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { sanitizeMacosMallocDiagnosticEnv } from "./sanitize-macos-malloc-env.js";
|
||||
|
||||
// __dirname equivalent in ESM
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -172,6 +173,7 @@ const packageManagerEnvVar =
|
||||
: "CODEX_MANAGED_BY_NPM";
|
||||
env[packageManagerEnvVar] = "1";
|
||||
env.CODEX_MANAGED_PACKAGE_ROOT = realpathSync(path.join(__dirname, ".."));
|
||||
sanitizeMacosMallocDiagnosticEnv(env);
|
||||
|
||||
const child = spawn(binaryPath, process.argv.slice(2), {
|
||||
stdio: "inherit",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
const MACOS_MALLOC_DIAGNOSTIC_ENV_PREFIXES = [
|
||||
"MallocStackLogging",
|
||||
"MallocLogFile",
|
||||
];
|
||||
|
||||
export function sanitizeMacosMallocDiagnosticEnv(env, platform = process.platform) {
|
||||
if (platform !== "darwin") {
|
||||
return env;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(env)) {
|
||||
if (
|
||||
MACOS_MALLOC_DIAGNOSTIC_ENV_PREFIXES.some((prefix) =>
|
||||
key.startsWith(prefix),
|
||||
)
|
||||
) {
|
||||
delete env[key];
|
||||
}
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { sanitizeMacosMallocDiagnosticEnv } from "../bin/sanitize-macos-malloc-env.js";
|
||||
|
||||
test("removes macOS malloc diagnostic env vars on darwin", () => {
|
||||
const env = {
|
||||
MallocStackLogging: "0",
|
||||
MallocStackLoggingDirectory: "/tmp/stack-logs",
|
||||
MallocLogFile: "/tmp/malloc.log",
|
||||
MallocNanoZone: "0",
|
||||
PATH: "/usr/bin",
|
||||
};
|
||||
|
||||
sanitizeMacosMallocDiagnosticEnv(env, "darwin");
|
||||
|
||||
assert.deepEqual(env, {
|
||||
MallocNanoZone: "0",
|
||||
PATH: "/usr/bin",
|
||||
});
|
||||
});
|
||||
|
||||
test("leaves env unchanged off darwin", () => {
|
||||
const env = {
|
||||
MallocStackLogging: "0",
|
||||
PATH: "/usr/bin",
|
||||
};
|
||||
|
||||
sanitizeMacosMallocDiagnosticEnv(env, "linux");
|
||||
|
||||
assert.deepEqual(env, {
|
||||
MallocStackLogging: "0",
|
||||
PATH: "/usr/bin",
|
||||
});
|
||||
});
|
||||
Generated
+2
@@ -2243,6 +2243,7 @@ dependencies = [
|
||||
"codex-model-provider",
|
||||
"codex-models-manager",
|
||||
"codex-plugin",
|
||||
"codex-process-hardening",
|
||||
"codex-protocol",
|
||||
"codex-responses-api-proxy",
|
||||
"codex-rmcp-client",
|
||||
@@ -3783,6 +3784,7 @@ dependencies = [
|
||||
"codex-models-manager",
|
||||
"codex-otel",
|
||||
"codex-plugin",
|
||||
"codex-process-hardening",
|
||||
"codex-protocol",
|
||||
"codex-realtime-webrtc",
|
||||
"codex-rollout",
|
||||
|
||||
@@ -46,6 +46,7 @@ codex-model-provider = { workspace = true }
|
||||
codex-models-manager = { workspace = true }
|
||||
codex-plugin = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-process-hardening = { workspace = true }
|
||||
codex-responses-api-proxy = { workspace = true }
|
||||
codex-rmcp-client = { workspace = true }
|
||||
codex-rollout-trace = { workspace = true }
|
||||
|
||||
@@ -821,6 +821,9 @@ fn stage_str(stage: Stage) -> &'static str {
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
codex_process_hardening::remove_macos_malloc_diagnostic_env_vars();
|
||||
|
||||
arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move {
|
||||
cli_main(arg0_paths).await?;
|
||||
Ok(())
|
||||
|
||||
@@ -44,10 +44,11 @@ impl Shell {
|
||||
match self.shell_type {
|
||||
ShellType::Zsh | ShellType::Bash | ShellType::Sh => {
|
||||
let arg = if use_login_shell { "-lc" } else { "-c" };
|
||||
let command = sanitize_shell_command_for_platform(command);
|
||||
vec![
|
||||
self.shell_path.to_string_lossy().to_string(),
|
||||
arg.to_string(),
|
||||
command.to_string(),
|
||||
command,
|
||||
]
|
||||
}
|
||||
ShellType::PowerShell => {
|
||||
@@ -75,6 +76,21 @@ impl Shell {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
const MACOS_MALLOC_DIAGNOSTIC_UNSET_PREFIX: &str = "for env_entry in $(env); do env_key=${env_entry%%=*}; case \"$env_key\" in MallocStackLogging*|MallocLogFile*) unset \"$env_key\" ;; esac; done; ";
|
||||
|
||||
fn sanitize_shell_command_for_platform(command: &str) -> String {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
format!("{MACOS_MALLOC_DIAGNOSTIC_UNSET_PREFIX}{command}")
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
command.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn empty_shell_snapshot_receiver() -> watch::Receiver<Option<Arc<ShellSnapshot>>> {
|
||||
let (_tx, rx) = watch::channel(None);
|
||||
rx
|
||||
|
||||
@@ -103,6 +103,11 @@ fn shell_works(shell: Option<Shell>, command: &str, required: bool) -> bool {
|
||||
|
||||
#[test]
|
||||
fn derive_exec_args() {
|
||||
#[cfg(target_os = "macos")]
|
||||
let echo_hello = "for env_entry in $(env); do env_key=${env_entry%%=*}; case \"$env_key\" in MallocStackLogging*|MallocLogFile*) unset \"$env_key\" ;; esac; done; echo hello";
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let echo_hello = "echo hello";
|
||||
|
||||
let test_bash_shell = Shell {
|
||||
shell_type: ShellType::Bash,
|
||||
shell_path: PathBuf::from("/bin/bash"),
|
||||
@@ -110,11 +115,19 @@ fn derive_exec_args() {
|
||||
};
|
||||
assert_eq!(
|
||||
test_bash_shell.derive_exec_args("echo hello", /*use_login_shell*/ false),
|
||||
vec!["/bin/bash", "-c", "echo hello"]
|
||||
vec![
|
||||
"/bin/bash".to_string(),
|
||||
"-c".to_string(),
|
||||
echo_hello.to_string()
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
test_bash_shell.derive_exec_args("echo hello", /*use_login_shell*/ true),
|
||||
vec!["/bin/bash", "-lc", "echo hello"]
|
||||
vec![
|
||||
"/bin/bash".to_string(),
|
||||
"-lc".to_string(),
|
||||
echo_hello.to_string()
|
||||
]
|
||||
);
|
||||
|
||||
let test_zsh_shell = Shell {
|
||||
@@ -124,11 +137,19 @@ fn derive_exec_args() {
|
||||
};
|
||||
assert_eq!(
|
||||
test_zsh_shell.derive_exec_args("echo hello", /*use_login_shell*/ false),
|
||||
vec!["/bin/zsh", "-c", "echo hello"]
|
||||
vec![
|
||||
"/bin/zsh".to_string(),
|
||||
"-c".to_string(),
|
||||
echo_hello.to_string()
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
test_zsh_shell.derive_exec_args("echo hello", /*use_login_shell*/ true),
|
||||
vec!["/bin/zsh", "-lc", "echo hello"]
|
||||
vec![
|
||||
"/bin/zsh".to_string(),
|
||||
"-lc".to_string(),
|
||||
echo_hello.to_string()
|
||||
]
|
||||
);
|
||||
|
||||
let test_powershell_shell = Shell {
|
||||
|
||||
@@ -4,6 +4,10 @@ use std::ffi::OsString;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
#[cfg(any(target_os = "macos", all(unix, test)))]
|
||||
const MACOS_MALLOC_DIAGNOSTIC_ENV_VAR_PREFIXES: &[&[u8]] =
|
||||
&[b"MallocStackLogging", b"MallocLogFile"];
|
||||
|
||||
/// This is designed to be called pre-main() (using `#[ctor::ctor]`) to perform
|
||||
/// various process hardening steps, such as
|
||||
/// - disabling core dumps
|
||||
@@ -102,8 +106,17 @@ pub(crate) fn pre_main_hardening_macos() {
|
||||
// Remove macOS malloc stack-logging controls so allocator diagnostics from
|
||||
// Codex or inherited child processes do not get sprayed into the TUI:
|
||||
// https://github.com/openai/codex/issues/11555
|
||||
remove_env_vars_with_prefix(b"MallocStackLogging");
|
||||
remove_env_vars_with_prefix(b"MallocLogFile");
|
||||
remove_macos_malloc_diagnostic_env_vars();
|
||||
}
|
||||
|
||||
/// Remove macOS malloc stack-logging controls from the current process.
|
||||
///
|
||||
/// This must run before spawning descendant Codex processes that inherit the
|
||||
/// current environment. Direct native binary launches can still emit a libmalloc
|
||||
/// warning before Rust reaches `main()`.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn remove_macos_malloc_diagnostic_env_vars() {
|
||||
remove_env_vars_with_prefixes(MACOS_MALLOC_DIAGNOSTIC_ENV_VAR_PREFIXES);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -130,23 +143,36 @@ pub(crate) fn pre_main_hardening_windows() {
|
||||
|
||||
#[cfg(unix)]
|
||||
fn remove_env_vars_with_prefix(prefix: &[u8]) {
|
||||
for key in env_keys_with_prefix(std::env::vars_os(), prefix) {
|
||||
remove_env_vars_with_prefixes(&[prefix]);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn remove_env_vars_with_prefixes(prefixes: &[&[u8]]) {
|
||||
for key in env_keys_with_prefixes(std::env::vars_os(), prefixes) {
|
||||
unsafe {
|
||||
std::env::remove_var(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[cfg(all(unix, test))]
|
||||
fn env_keys_with_prefix<I>(vars: I, prefix: &[u8]) -> Vec<OsString>
|
||||
where
|
||||
I: IntoIterator<Item = (OsString, OsString)>,
|
||||
{
|
||||
env_keys_with_prefixes(vars, &[prefix])
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn env_keys_with_prefixes<I>(vars: I, prefixes: &[&[u8]]) -> Vec<OsString>
|
||||
where
|
||||
I: IntoIterator<Item = (OsString, OsString)>,
|
||||
{
|
||||
vars.into_iter()
|
||||
.filter_map(|(key, _)| {
|
||||
key.as_os_str()
|
||||
.as_bytes()
|
||||
.starts_with(prefix)
|
||||
prefixes
|
||||
.iter()
|
||||
.any(|prefix| key.as_os_str().as_bytes().starts_with(prefix))
|
||||
.then_some(key)
|
||||
})
|
||||
.collect()
|
||||
@@ -197,4 +223,25 @@ mod tests {
|
||||
assert_eq!(keys.len(), 1);
|
||||
assert_eq!(keys[0].as_os_str(), ld_test_var);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_keys_with_prefixes_filters_all_matching_keys() {
|
||||
let keys = env_keys_with_prefixes(
|
||||
vec![
|
||||
(OsString::from("MallocStackLogging"), OsString::from("1")),
|
||||
(OsString::from("MallocLogFile"), OsString::from("/tmp/log")),
|
||||
(OsString::from("MallocNanoZone"), OsString::from("0")),
|
||||
],
|
||||
MACOS_MALLOC_DIAGNOSTIC_ENV_VAR_PREFIXES,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
keys,
|
||||
vec![
|
||||
OsString::from("MallocStackLogging"),
|
||||
OsString::from("MallocLogFile"),
|
||||
],
|
||||
"only macOS malloc diagnostic env entries should be retained"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ use std::collections::HashMap;
|
||||
|
||||
pub const CODEX_THREAD_ID_ENV_VAR: &str = "CODEX_THREAD_ID";
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
const NOISY_MACOS_MALLOC_ENV_PREFIXES: &[&str] = &["MallocStackLogging", "MallocLogFile"];
|
||||
|
||||
/// Construct a shell environment from the supplied process environment and
|
||||
/// shell-environment policy.
|
||||
pub fn create_env(
|
||||
@@ -106,6 +109,16 @@ where
|
||||
env_map.insert(CODEX_THREAD_ID_ENV_VAR.to_string(), thread_id.to_string());
|
||||
}
|
||||
|
||||
// Step 7 - On macOS, allocator diagnostic env vars can cause libmalloc to
|
||||
// print warnings into every spawned process. Always remove them from shell
|
||||
// tool environments, even if they were inherited or explicitly configured.
|
||||
#[cfg(target_os = "macos")]
|
||||
env_map.retain(|key, _| {
|
||||
!NOISY_MACOS_MALLOC_ENV_PREFIXES
|
||||
.iter()
|
||||
.any(|prefix| key.starts_with(prefix))
|
||||
});
|
||||
|
||||
env_map
|
||||
}
|
||||
|
||||
@@ -247,4 +260,39 @@ mod non_windows_tests {
|
||||
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn macos_allocator_diagnostic_env_vars_are_removed_after_policy_application() {
|
||||
let vars = make_vars(&[
|
||||
("PATH", "/usr/bin"),
|
||||
("MallocStackLogging", "0"),
|
||||
("MallocStackLoggingNoCompact", "1"),
|
||||
("MallocStackLoggingDirectory", "/tmp/stack-logs"),
|
||||
("MallocLogFile", "/tmp/malloc.log"),
|
||||
("MallocNanoZone", "0"),
|
||||
]);
|
||||
|
||||
let mut policy = ShellEnvironmentPolicy {
|
||||
inherit: ShellEnvironmentPolicyInherit::All,
|
||||
ignore_default_excludes: true,
|
||||
include_only: vec![EnvironmentVariablePattern::new_case_insensitive("*")],
|
||||
..Default::default()
|
||||
};
|
||||
policy
|
||||
.r#set
|
||||
.insert("MallocStackLoggingNoCompact".to_string(), "0".to_string());
|
||||
policy
|
||||
.r#set
|
||||
.insert("EXPLICIT_OK".to_string(), "1".to_string());
|
||||
|
||||
let result = populate_env(vars, &policy, /*thread_id*/ None);
|
||||
let expected = HashMap::from([
|
||||
("PATH".to_string(), "/usr/bin".to_string()),
|
||||
("MallocNanoZone".to_string(), "0".to_string()),
|
||||
("EXPLICIT_OK".to_string(), "1".to_string()),
|
||||
]);
|
||||
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { sanitizeMacosMallocDiagnosticEnv } from "./sanitize-macos-malloc-env.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -57,8 +58,11 @@ const binaryPath = path.join(
|
||||
process.platform === "win32" ? `${binaryBaseName}.exe` : binaryBaseName,
|
||||
);
|
||||
|
||||
const env = sanitizeMacosMallocDiagnosticEnv({ ...process.env });
|
||||
|
||||
const child = spawn(binaryPath, process.argv.slice(2), {
|
||||
stdio: "inherit",
|
||||
env,
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
const MACOS_MALLOC_DIAGNOSTIC_ENV_PREFIXES = [
|
||||
"MallocStackLogging",
|
||||
"MallocLogFile",
|
||||
];
|
||||
|
||||
export function sanitizeMacosMallocDiagnosticEnv(env, platform = process.platform) {
|
||||
if (platform !== "darwin") {
|
||||
return env;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(env)) {
|
||||
if (
|
||||
MACOS_MALLOC_DIAGNOSTIC_ENV_PREFIXES.some((prefix) =>
|
||||
key.startsWith(prefix),
|
||||
)
|
||||
) {
|
||||
delete env[key];
|
||||
}
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
@@ -49,6 +49,7 @@ codex-model-provider-info = { workspace = true }
|
||||
codex-models-manager = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-plugin = { workspace = true }
|
||||
codex-process-hardening = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-realtime-webrtc = { workspace = true }
|
||||
codex-rollout = { workspace = true }
|
||||
|
||||
@@ -44,6 +44,9 @@ struct TopCli {
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
codex_process_hardening::remove_macos_malloc_diagnostic_env_vars();
|
||||
|
||||
arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move {
|
||||
let top_cli = TopCli::parse();
|
||||
let mut inner = top_cli.inner;
|
||||
|
||||
Reference in New Issue
Block a user