Compare commits

...

1 Commits

Author SHA1 Message Date
Felipe Coury
86641292bd fix(macos): scrub malloc diagnostic env 2026-05-15 12:26:19 -03:00
14 changed files with 239 additions and 12 deletions

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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",
});
});

2
codex-rs/Cargo.lock generated
View File

@@ -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",

View File

@@ -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 }

View File

@@ -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(())

View File

@@ -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

View File

@@ -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 {

View File

@@ -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"
);
}
}

View File

@@ -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);
}
}

View File

@@ -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) => {

View File

@@ -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;
}

View File

@@ -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 }

View File

@@ -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;