Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions src/codex/cli-update-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,15 @@ function npmTarget(args: readonly string[], env?: NodeJS.ProcessEnv): NpmTarget
* reverse: they are output paths, so ambient values would write Node artifacts to
* arbitrary external directories on exit. ELF/Darwin loader controls (LD_* and
* DYLD_*) are also removed: they can load code or redirect loader output before
* npm runs. This is a denylist for process-startup/output channels, not a claim
* that every ambient variable is allowlisted. Ordinary OS environment, proxy
* settings and the supported NODE_EXTRA_CA_CERTS extension remain available.
* npm runs. OpenSSL's startup inputs are removed with them: OPENSSL_CONF names
* a configuration file the crypto library reads at startup, OPENSSL_CONF_INCLUDE
* resolves relative includes inside that configuration, and OPENSSL_MODULES /
* OPENSSL_ENGINES name the directories its provider and engine search loads
* shared modules from. None of these is input the updater child needs, so
* ambient values stay out of the spawned npm. This is a denylist for
* process-startup/output channels, not a claim that every ambient variable is
* allowlisted. Ordinary OS environment, proxy settings and the supported
* NODE_EXTRA_CA_CERTS extension remain available.
*/
const NPM_ENV_DROP_KEYS: ReadonlySet<string> = new Set([
"node_options",
Expand All @@ -227,6 +233,15 @@ const NPM_ENV_DROP_KEYS: ReadonlySet<string> = new Set([
// escaping the owned temp root regardless of the isolated --cache.
"node_v8_coverage",
"node_redirect_warnings",
// OpenSSL startup inputs: OPENSSL_CONF points the library at an ambient
// startup configuration file (OPENSSL_CONF_INCLUDE resolves that config's
// relative includes); OPENSSL_MODULES and OPENSSL_ENGINES point its
// provider/engine search at ambient directories that supply loadable
// shared modules.
"openssl_conf",
"openssl_conf_include",
"openssl_modules",
"openssl_engines",
]);

function codexCliUpdateNpmEnv(base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
Expand Down
3 changes: 2 additions & 1 deletion structure/codex-cli-update.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ The read-only CLI update plan binds npm executable selection to the inspected la
Registry integrity requires a canonical base64 SHA-512 digest of exactly 64 bytes; malformed or truncated tokens refuse the target.
Selection observation uses the runtime resolver with provenance's bounded no-follow, nonblocking state reader and local executable-path policy. Windows returns before invoking the resolver; its deferred inspection performs no candidate or configuration I/O.
Its child environment drops npm configuration overrides, named Node startup/output controls,
and the `LD_*`/`DYLD_*` loader families; proxy settings and `NODE_EXTRA_CA_CERTS` remain supported.
the `LD_*`/`DYLD_*` loader families and the OpenSSL startup configuration and
provider/engine search variables; proxy settings and `NODE_EXTRA_CA_CERTS` remain supported.
Distinct npm config files, cache and logs stay under the owned temporary root with best-effort
cleanup; a cleanup error does not replace the computed plan/refusal. The POSIX update
scan includes all readable users by default, retaining an explicitly injected UID scope.
Expand Down
49 changes: 49 additions & 0 deletions tests/codex-integration/codex-cli-update-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,55 @@ describe("registry configuration isolation", () => {
}
});

test.each([
"OPENSSL_CONF", "OPENSSL_CONF_INCLUDE", "OPENSSL_MODULES", "OPENSSL_ENGINES",
"openssl_conf", "openssl_modules",
])("ambient OpenSSL startup control %s cannot reach the spawned npm", key => {
const previous = process.env[key];
process.env[key] = join(tmpdir(), "ocx-ambient-openssl.cnf");
try {
// Capture only: no child process, native provider or network is invoked.
const { calls, spawn } = capturingSpawn(RESOLVE_OUTPUTS);
expect(resolveCodexCliUpdateTarget("latest", spawn).kind).toBe("resolved");
expect(calls.length).toBe(3);
for (const call of calls) {
const env = call.options.env as NodeJS.ProcessEnv;
expect(Object.keys(env).some(name => name.toLowerCase() === key.toLowerCase())).toBe(false);
}
} finally {
if (previous === undefined) delete process.env[key];
else process.env[key] = previous;
}
});

test("supported proxy and CA channels still reach the spawned npm", () => {
const sentinels = {
HTTP_PROXY: "http://127.0.0.1:9/ocx-proxy-sentinel",
NO_PROXY: "registry.npmjs.org.invalid,127.0.0.1",
NODE_EXTRA_CA_CERTS: join(tmpdir(), "ocx-corporate-ca.pem"),
};
const previous = Object.fromEntries(Object.keys(sentinels).map(key => [key, process.env[key]]));
try {
Object.assign(process.env, sentinels);
const { calls, spawn } = capturingSpawn(RESOLVE_OUTPUTS);
expect(resolveCodexCliUpdateTarget("latest", spawn).kind).toBe("resolved");
for (const call of calls) {
const env = call.options.env as NodeJS.ProcessEnv;
for (const [key, value] of Object.entries(sentinels)) {
// Match case-insensitively: the child env is a plain object, so a
// Windows-normalized casing must not make a supported channel look absent.
const entry = Object.entries(env).find(([name]) => name.toLowerCase() === key.toLowerCase());
expect(entry?.[1]).toBe(value);
}
}
} finally {
for (const key of Object.keys(sentinels)) {
if (previous[key] === undefined) delete process.env[key];
else process.env[key] = previous[key];
}
}
});

test("real Node cannot write output sentinels outside the owned npm root", () => {
const fixture = fs.realpathSync.native(mkdtempSync(join(tmpdir(), "ocx-update-node-output-")));
const sentinels = {
Expand Down
Loading