Skip to content

Commit bcff47d

Browse files
authored
Merge pull request ilysenko#1016 from danielcadev/codex/scope-browser-sockets-per-user
fix(browser): scope socket directory per Linux user
2 parents b24fa4f + 8494ee9 commit bcff47d

7 files changed

Lines changed: 318 additions & 17 deletions

.github/workflows/upstream-build-app.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@ on:
1414
- scripts/ci/upstream-dmg-*.js
1515
- scripts/validate-upstream-dmg.js
1616
- scripts/lib/candidate-install.sh
17+
- scripts/lib/bundled-plugins.sh
18+
- scripts/lib/browser-client-node-repl-runtime.test.js
19+
- scripts/lib/patch-browser-client-iab-socket-scope.js
1720
- scripts/lib/patch-validation.js
1821
- scripts/lib/upstream-dmg-acceptance.js
1922
- scripts/lib/upstream-dmg-release-profile.js
2023
- scripts/lib/linux-features.js
24+
- computer-use-linux/src/bin/codex-chrome-extension-host.rs
2125
- linux-features/**
2226
- .github/workflows/upstream-build-app.yml
2327
push:
@@ -33,10 +37,14 @@ on:
3337
- scripts/ci/upstream-dmg-*.js
3438
- scripts/validate-upstream-dmg.js
3539
- scripts/lib/candidate-install.sh
40+
- scripts/lib/bundled-plugins.sh
41+
- scripts/lib/browser-client-node-repl-runtime.test.js
42+
- scripts/lib/patch-browser-client-iab-socket-scope.js
3643
- scripts/lib/patch-validation.js
3744
- scripts/lib/upstream-dmg-acceptance.js
3845
- scripts/lib/upstream-dmg-release-profile.js
3946
- scripts/lib/linux-features.js
47+
- computer-use-linux/src/bin/codex-chrome-extension-host.rs
4048
- linux-features/**
4149
- .github/workflows/upstream-build-app.yml
4250
workflow_dispatch:
@@ -164,6 +172,13 @@ jobs:
164172
REBUILD_REPORT_DIR: ${{ github.workspace }}/upstream-acceptance
165173
run: make build-app DMG=/tmp/codex-upstream-ci/Codex.dmg
166174

175+
- name: Import staged Browser clients through node_repl
176+
if: steps.acceptance-build.outcome == 'success'
177+
env:
178+
CODEX_NODE_REPL_PATH: ${{ github.workspace }}/codex-app/resources/node_repl
179+
CODEX_STAGED_BUNDLED_PLUGINS_ROOT: ${{ github.workspace }}/codex-app/resources/plugins/openai-bundled/plugins
180+
run: node --test scripts/lib/browser-client-node-repl-runtime.test.js
181+
167182
- name: Upload upstream DMG metadata artifact
168183
# The transactional installer records a decision before returning a
169184
# rejected status, so diagnostics remain available on failed runs.

computer-use-linux/src/bin/codex-chrome-extension-host.rs

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::{
77
io::{self, BufRead, BufReader, ErrorKind, Read, Seek, SeekFrom, Write},
88
net::Shutdown,
99
os::unix::{
10+
ffi::OsStrExt,
1011
fs::{MetadataExt, PermissionsExt},
1112
io::AsRawFd,
1213
net::{UnixListener, UnixStream},
@@ -26,7 +27,8 @@ use chrome_runtime::RuntimeManager;
2627
const HOST_NAME: &str = "com.openai.codexextension";
2728
const SOCKET_DIR_ENV: &str = "CODEX_BROWSER_USE_SOCKET_DIR";
2829
const SESSIONS_DIR_ENV: &str = "CODEX_BROWSER_USE_SESSIONS_DIR";
29-
const DEFAULT_SOCKET_DIR: &str = "/tmp/codex-browser-use";
30+
const SOCKET_DIR_NAME: &str = "codex-browser-use";
31+
const MAX_UNIX_SOCKET_PATH_BYTES: usize = 107;
3032
const ROLLOUT_POLL_INTERVAL: Duration = Duration::from_millis(500);
3133
const OBSERVED_TURN_TTL: Duration = Duration::from_secs(6 * 60 * 60);
3234
const ROLLOUT_SEARCH_MAX_DEPTH: usize = 5;
@@ -330,9 +332,10 @@ impl RolloutTracker {
330332
}
331333

332334
fn main() -> Result<()> {
333-
let socket_dir = socket_dir();
335+
let effective_uid = unsafe { libc::geteuid() };
336+
let socket_dir = socket_dir(effective_uid);
337+
let socket_path = socket_path(&socket_dir)?;
334338
prepare_socket_dir(&socket_dir)?;
335-
let socket_path = socket_path(&socket_dir);
336339
remove_socket_if_present(&socket_path)?;
337340

338341
let listener = UnixListener::bind(&socket_path)
@@ -364,10 +367,16 @@ fn main() -> Result<()> {
364367
result
365368
}
366369

367-
fn socket_dir() -> PathBuf {
368-
env::var_os(SOCKET_DIR_ENV)
369-
.map(PathBuf::from)
370-
.unwrap_or_else(|| PathBuf::from(DEFAULT_SOCKET_DIR))
370+
fn socket_dir(effective_uid: u32) -> PathBuf {
371+
if let Some(path) = env::var_os(SOCKET_DIR_ENV).filter(|value| !value.is_empty()) {
372+
return PathBuf::from(path);
373+
}
374+
375+
default_socket_dir(effective_uid)
376+
}
377+
378+
fn default_socket_dir(effective_uid: u32) -> PathBuf {
379+
PathBuf::from(format!("/tmp/{SOCKET_DIR_NAME}-{effective_uid}"))
371380
}
372381

373382
fn sessions_root() -> Option<PathBuf> {
@@ -397,12 +406,23 @@ fn is_extension_id(value: &str) -> bool {
397406
value.len() == 32 && value.bytes().all(|byte| matches!(byte, b'a'..=b'p'))
398407
}
399408

400-
fn socket_path(socket_dir: &Path) -> PathBuf {
409+
fn socket_path(socket_dir: &Path) -> Result<PathBuf> {
401410
let nonce = SystemTime::now()
402411
.duration_since(UNIX_EPOCH)
403412
.map(|duration| duration.as_nanos())
404413
.unwrap_or_default();
405-
socket_dir.join(format!("extension-{}-{nonce}.sock", process::id()))
414+
socket_path_for(socket_dir, process::id(), nonce)
415+
}
416+
417+
fn socket_path_for(socket_dir: &Path, process_id: u32, nonce: u128) -> Result<PathBuf> {
418+
let path = socket_dir.join(format!("extension-{process_id}-{nonce}.sock"));
419+
if path.as_os_str().as_bytes().len() > MAX_UNIX_SOCKET_PATH_BYTES {
420+
bail!(
421+
"unix socket path exceeds the {MAX_UNIX_SOCKET_PATH_BYTES}-byte Linux limit: {}",
422+
path.display()
423+
);
424+
}
425+
Ok(path)
406426
}
407427

408428
fn prepare_socket_dir(path: &Path) -> Result<()> {
@@ -1027,6 +1047,27 @@ fn log(message: &str) {
10271047
mod tests {
10281048
use super::*;
10291049

1050+
#[test]
1051+
fn socket_directory_default_is_scoped_by_uid() {
1052+
assert_eq!(
1053+
default_socket_dir(1000),
1054+
PathBuf::from("/tmp/codex-browser-use-1000")
1055+
);
1056+
}
1057+
1058+
#[test]
1059+
fn complete_socket_path_is_guarded_against_the_linux_limit() {
1060+
let short = socket_path_for(Path::new("/tmp/codex-browser-use-1000"), 42, 7).unwrap();
1061+
assert_eq!(
1062+
short,
1063+
PathBuf::from("/tmp/codex-browser-use-1000/extension-42-7.sock")
1064+
);
1065+
1066+
let long_dir = PathBuf::from("/tmp").join("x".repeat(MAX_UNIX_SOCKET_PATH_BYTES));
1067+
let error = socket_path_for(&long_dir, 42, 7).unwrap_err();
1068+
assert!(error.to_string().contains("unix socket path exceeds"));
1069+
}
1070+
10301071
#[test]
10311072
fn frame_round_trip_uses_native_length_prefix() {
10321073
let message = json!({ "jsonrpc": "2.0", "id": "1", "method": "ping" });
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#!/usr/bin/env node
2+
"use strict";
3+
4+
const assert = require("node:assert/strict");
5+
const { spawn } = require("node:child_process");
6+
const fs = require("node:fs");
7+
const path = require("node:path");
8+
const readline = require("node:readline");
9+
const test = require("node:test");
10+
const { pathToFileURL } = require("node:url");
11+
12+
const runtimePath = process.env.CODEX_NODE_REPL_PATH;
13+
const pluginsRoot = process.env.CODEX_STAGED_BUNDLED_PLUGINS_ROOT;
14+
15+
function runNodeReplImport(runtime, clients) {
16+
return new Promise((resolve, reject) => {
17+
const child = spawn(runtime, [], {
18+
env: {
19+
...process.env,
20+
CODEX_BROWSER_USE_SOCKET_DIR: "/tmp/codex-browser-use-runtime-test",
21+
},
22+
stdio: ["pipe", "pipe", "pipe"],
23+
});
24+
const stdout = readline.createInterface({ input: child.stdout });
25+
let stderr = "";
26+
let settled = false;
27+
28+
const finish = (error, value) => {
29+
if (settled) return;
30+
settled = true;
31+
clearTimeout(timer);
32+
stdout.close();
33+
child.kill("SIGTERM");
34+
if (error) reject(error);
35+
else resolve(value);
36+
};
37+
38+
const send = (message) => child.stdin.write(`${JSON.stringify(message)}\n`);
39+
const code = `${clients
40+
.map((client) => `await import(${JSON.stringify(pathToFileURL(client).href)});`)
41+
.join("")}nodeRepl.write("imports-ok")`;
42+
const timer = setTimeout(
43+
() => finish(new Error(`node_repl import timed out: ${stderr}`)),
44+
20_000,
45+
);
46+
47+
child.stderr.setEncoding("utf8");
48+
child.stderr.on("data", (chunk) => {
49+
stderr += chunk;
50+
});
51+
child.on("error", (error) => finish(error));
52+
child.on("exit", (codeValue, signal) => {
53+
if (!settled) {
54+
finish(
55+
new Error(
56+
`node_repl exited before the import response (code=${codeValue}, signal=${signal}): ${stderr}`,
57+
),
58+
);
59+
}
60+
});
61+
stdout.on("line", (line) => {
62+
let message;
63+
try {
64+
message = JSON.parse(line);
65+
} catch {
66+
return;
67+
}
68+
69+
if (message.id === 1) {
70+
send({ jsonrpc: "2.0", method: "notifications/initialized", params: {} });
71+
send({
72+
jsonrpc: "2.0",
73+
id: 2,
74+
method: "tools/call",
75+
params: {
76+
name: "js",
77+
arguments: {
78+
code,
79+
timeout_ms: 10_000,
80+
title: "Import staged Browser clients",
81+
},
82+
},
83+
});
84+
}
85+
86+
if (message.id === 2) {
87+
const text = message.result?.content
88+
?.filter((item) => item.type === "text")
89+
.map((item) => item.text)
90+
.join("");
91+
if (message.result?.isError) {
92+
finish(new Error(`node_repl import failed: ${text || stderr}`));
93+
} else {
94+
finish(null, text);
95+
}
96+
}
97+
});
98+
99+
send({
100+
jsonrpc: "2.0",
101+
id: 1,
102+
method: "initialize",
103+
params: {
104+
protocolVersion: "2025-06-18",
105+
capabilities: {},
106+
clientInfo: { name: "codex-browser-client-runtime-test", version: "1" },
107+
},
108+
});
109+
});
110+
}
111+
112+
test(
113+
"staged Browser and Chrome clients import through the real node_repl runtime",
114+
{ skip: !runtimePath || !pluginsRoot },
115+
async () => {
116+
const clients = ["browser", "chrome"].map((plugin) =>
117+
path.join(pluginsRoot, plugin, "scripts", "browser-client.mjs"),
118+
);
119+
assert.ok(fs.existsSync(runtimePath), `node_repl runtime not found: ${runtimePath}`);
120+
for (const client of clients) {
121+
assert.ok(fs.existsSync(client), `staged Browser client not found: ${client}`);
122+
}
123+
124+
assert.equal(await runNodeReplImport(runtimePath, clients), "imports-ok");
125+
},
126+
);

scripts/lib/bundled-plugins.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,20 @@ patch_browser_client_iab_socket_scope() {
10211021
fi
10221022
}
10231023

1024+
patch_browser_client_linux_socket_dir() {
1025+
local client="$1"
1026+
local patcher="$SCRIPT_DIR/scripts/lib/patch-browser-client-iab-socket-scope.js"
1027+
1028+
if [ ! -f "$patcher" ]; then
1029+
warn "Browser socket-directory patch helper not found at $patcher; leaving browser-client.mjs unchanged"
1030+
return 0
1031+
fi
1032+
1033+
if ! node "$patcher" "$client" --socket-dir-only >&2; then
1034+
warn "Browser socket-directory patch helper failed; leaving browser-client.mjs unchanged"
1035+
fi
1036+
}
1037+
10241038
normalize_plugin_script_executable_modes() {
10251039
local target_plugin="$1"
10261040
local scripts_dir="$target_plugin/scripts"
@@ -1066,6 +1080,7 @@ stage_chrome_plugin_from_upstream() {
10661080
patch_browser_use_node_repl_config_shim "$target_plugin/scripts/browser-client.mjs"
10671081
patch_browser_use_native_pipe_import_meta_bridge "$target_plugin/scripts/browser-client.mjs"
10681082
patch_browser_use_site_status_allowlist_fallback "$target_plugin/scripts/browser-client.mjs"
1083+
patch_browser_client_linux_socket_dir "$target_plugin/scripts/browser-client.mjs"
10691084
normalize_plugin_script_executable_modes "$target_plugin"
10701085
if ! install_chrome_extension_host_resource "$target_plugin"; then
10711086
rm -rf "$target_plugin"

scripts/lib/patch-browser-client-iab-socket-scope.js

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,41 @@ const fs = require("node:fs");
55

66
const clientPath = process.argv[2];
77
if (!clientPath) {
8-
throw new Error("Usage: patch-browser-client-iab-socket-scope.js /path/to/browser-client.mjs");
8+
throw new Error(
9+
"Usage: patch-browser-client-iab-socket-scope.js /path/to/browser-client.mjs [--socket-dir-only]",
10+
);
911
}
12+
const socketDirOnly = process.argv.includes("--socket-dir-only");
1013

11-
const marker = "/*codexLinuxIabSocketScope*/";
12-
const source = fs.readFileSync(clientPath, "utf8");
13-
if (source.includes(marker)) {
14+
const socketDirMarker = "/*codexLinuxPerUserBrowserSocketDir*/";
15+
const iabMarker = "/*codexLinuxIabSocketScope*/";
16+
let source = fs.readFileSync(clientPath, "utf8");
17+
18+
if (!source.includes(socketDirMarker)) {
19+
const socketDirectoryPattern =
20+
/([A-Za-z_$][\w$]*)=([A-Za-z_$][\w$]*)=>\2==="win32"\?("(?:\\.|[^"\\])*codex-browser-use"):"\/tmp\/codex-browser-use"/g;
21+
const socketDirectoryMatches = [...source.matchAll(socketDirectoryPattern)];
22+
if (socketDirectoryMatches.length === 1) {
23+
const [target, resolver, platform, windowsSocket] = socketDirectoryMatches[0];
24+
const userInfoImport =
25+
'import{userInfo as codexLinuxBrowserUseUserInfo}from"node:os";';
26+
const helper =
27+
`function codexLinuxBrowserUseSocketDir(){let e=globalThis.nodeRepl?.env?.CODEX_BROWSER_USE_SOCKET_DIR;` +
28+
`if(typeof e==="string"&&e.length>0)return e;let t=codexLinuxBrowserUseUserInfo().uid;` +
29+
`if(Number.isInteger(t)&&t>=0)return \`/tmp/codex-browser-use-\${t}\`;` +
30+
`throw Error("Browser Use cannot resolve a per-user Linux socket directory")}${socketDirMarker}`;
31+
const replacement =
32+
`${resolver}=${platform}=>${platform}==="win32"?${windowsSocket}:codexLinuxBrowserUseSocketDir()`;
33+
source = userInfoImport + helper + source.replace(target, replacement);
34+
} else if (source.includes("/tmp/codex-browser-use")) {
35+
process.stderr.write(
36+
`WARN: Expected one Browser Use socket-directory resolver, found ${socketDirectoryMatches.length}; leaving its path unchanged\n`,
37+
);
38+
}
39+
}
40+
41+
if (socketDirOnly || source.includes(iabMarker)) {
42+
fs.writeFileSync(clientPath, source, "utf8");
1443
process.exit(0);
1544
}
1645

@@ -20,9 +49,10 @@ const matches = [...source.matchAll(socketListingPattern)];
2049
if (matches.length !== 1) {
2150
if (source.includes("codex-browser-use")) {
2251
process.stderr.write(
23-
`WARN: Expected one IAB Browser socket listing target, found ${matches.length}; leaving browser-client.mjs unchanged\n`,
52+
`WARN: Expected one IAB Browser socket listing target, found ${matches.length}; leaving IAB discovery unchanged\n`,
2453
);
2554
}
55+
fs.writeFileSync(clientPath, source, "utf8");
2656
process.exit(0);
2757
}
2858

@@ -40,7 +70,7 @@ const [
4070
const replacement =
4171
`${dispatcher}=()=>${platform}()==="win32"?${windowsListing}():${unixListing}(),` +
4272
`${unixListing}=async()=>(await ${readDirectory}(${socketDirectory}))` +
43-
`.filter(${entry}=>!${entry}.startsWith("extension-")${marker})` +
73+
`.filter(${entry}=>!${entry}.startsWith("extension-")${iabMarker})` +
4474
`.map(${entry}=>${pathModule}.resolve(${socketDirectory},${entry})),` +
4575
`${windowsListing}=async()=>`;
4676
fs.writeFileSync(clientPath, source.replace(target, replacement), "utf8");

0 commit comments

Comments
 (0)