Skip to content
Merged
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
1 change: 1 addition & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ main() {
stage_linux_notification_actions_bridge
install_bundled_plugin_resources "$app_dir"
run_linux_feature_stage_hooks "$app_dir"
harden_bundled_plugin_source_tree
create_start_script
if [ -n "${CODEX_PATCH_REPORT_RESOLVED:-}" ] && [ -f "$CODEX_PATCH_REPORT_RESOLVED" ]; then
cp "$CODEX_PATCH_REPORT_RESOLVED" "$INSTALL_DIR/.codex-linux/patch-report.json"
Expand Down
39 changes: 39 additions & 0 deletions launcher/start.sh.template
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,37 @@ fix_bundled_marketplace_tmp_permissions() {
find "$marketplace_tmp_root" -type f -exec chmod u+rw {} + 2>/dev/null || true
}

prepare_bundled_marketplace_tmp_paths() {
local codex_home="${CODEX_HOME:-$HOME/.codex}"
local marketplace_tmp_root="$codex_home/.tmp/bundled-marketplaces"
local scope="${1:-root}"
local path
local -a paths=(
"$codex_home"
"$codex_home/.tmp"
"$marketplace_tmp_root"
)

if [ "$scope" = "full" ]; then
paths+=(
"$marketplace_tmp_root/openai-bundled"
"$marketplace_tmp_root/openai-bundled/.agents"
"$marketplace_tmp_root/openai-bundled/.agents/plugins"
"$marketplace_tmp_root/openai-bundled/plugins"
)
fi

for path in "${paths[@]}"; do
if [ -e "$path" ] || [ -L "$path" ]; then
[ -d "$path" ] && [ ! -L "$path" ] || return 1
else
(umask 0077; mkdir -p "$path") || return 1
fi
make_path_owner_trusted "$path" || return 1
! path_has_unsafe_write "$path" || return 1
done
}

clear_bundled_marketplace_tmp_cache() {
local codex_home="${CODEX_HOME:-$HOME/.codex}"
local marketplace_tmp_root="$codex_home/.tmp/bundled-marketplaces"
Expand Down Expand Up @@ -4244,7 +4275,15 @@ fi
export_packaged_runtime_env
if needs_cold_start; then
log_phase "cold_start_cache_sync_start"
if ! prepare_bundled_marketplace_tmp_paths; then
notify_error "Bundled plugin staging paths are not trusted. Refusing to start Electron."
exit 1
fi
clear_bundled_marketplace_tmp_cache
if ! prepare_bundled_marketplace_tmp_paths full; then
notify_error "Bundled plugin staging paths are not trusted. Refusing to start Electron."
exit 1
fi
log_phase "bundled_marketplace_tmp_cleared"
stage_bundled_marketplace_metadata
log_phase "bundled_marketplace_metadata_staged"
Expand Down
2 changes: 2 additions & 0 deletions scripts/build-appimage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ prepare_appdir() {
"$APPIMAGE_RUNTIME_TEMPLATE" \
"$APPDIR/opt/$PACKAGE_NAME/.codex-linux/codex-packaged-runtime.sh"
chmod 0644 "$APPDIR/opt/$PACKAGE_NAME/.codex-linux/codex-packaged-runtime.sh"
normalize_package_payload_permissions "$APPDIR"
restore_linux_feature_payload_permissions "$APPDIR"
}

main() {
Expand Down
14 changes: 9 additions & 5 deletions scripts/lib/bundled-plugins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1732,6 +1732,15 @@ fs.writeFileSync(destinationPath, `${JSON.stringify(marketplace, null, 2)}\n`);
NODE
}

harden_bundled_plugin_source_tree() {
local resources_dir="$INSTALL_DIR/resources"
local bundled_plugins_dir="$resources_dir/plugins/openai-bundled"

[ -d "$bundled_plugins_dir" ] || return 0
chmod go-w "$INSTALL_DIR" "$resources_dir" "$resources_dir/plugins"
chmod -R u+rwX,go-w "$bundled_plugins_dir"
}

install_bundled_plugin_resources() {
local app_dir="$1"
local upstream_resources="$app_dir/Contents/Resources"
Expand Down Expand Up @@ -1805,10 +1814,5 @@ install_bundled_plugin_resources() {
install_linux_executable_resource "$upstream_resources/node" "$resources_dir/node" "node runtime" "info" || true
install_browser_use_node_repl_resource "$upstream_resources" "$resources_dir/node_repl" || true

# These files become the trust root for user-cache refreshes at runtime.
# Normalize them while staging from the accepted DMG instead of blessing a
# potentially modified installed tree during launcher startup.
chmod -R u+rwX,go-w "$bundled_plugins_dir"

info "Linux-safe bundled plugins installed"
}
118 changes: 99 additions & 19 deletions scripts/patch-linux-window-ui.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1325,8 +1325,11 @@ function currentBundledPluginCopyBundleFixture() {
return (
"let p=require(`node:path`);" +
"let m=require(`node:fs/promises`);m={default:m};" +
"let h=require(`node:crypto`);" +
"let g={default:{platform:process.platform}};" +
"async function fl(e,t){if(g.default.platform===`darwin`){return}if(g.default.platform!==`win32`){await m.default.cp(e,t,{recursive:!0,verbatimSymlinks:!0});return}}"
"let cc=[`.agents`,`plugins`,`marketplace.json`];" +
"async function fl(e,t){if(g.default.platform===`darwin`){return}if(g.default.platform!==`win32`){await m.default.cp(e,t,{recursive:!0,verbatimSymlinks:!0});return}}" +
"async function Ac(e){let a=`${e.targetMarketplaceRoot}.staging-${h.randomUUID()}`;await m.default.mkdir((0,p.join)(a,...cc.slice(0,-1)),{recursive:!0});await m.default.writeFile((0,p.join)(a,...cc),`{}\\n`,`utf8`);let n=e.sourcePlugin,t=(0,p.join)(a,`plugins`,`chrome`);await m.default.mkdir((0,p.dirname)(t),{recursive:!0}),await fl(n,t);return a}"
);
}

Expand Down Expand Up @@ -7323,7 +7326,7 @@ test("auto-installs the current Chrome plugin gate shape", () => {
assert.equal((patched.match(/installWhenMissing:!0,name:o\.s/g) || []).length, 0);
});

test("makes Linux bundled plugin staging writable after copying read-only resources", async () => {
test("materializes trusted Linux bundled plugins through a private staging root", async () => {
const patched = applyPatchTwice(
applyLinuxBundledPluginCopyPermissionsPatch,
currentBundledPluginCopyBundleFixture(),
Expand All @@ -7332,41 +7335,118 @@ test("makes Linux bundled plugin staging writable after copying read-only resour
const sourcePlugin = path.join(root, "source-plugin");
const sourceManifestDir = path.join(sourcePlugin, ".codex-plugin");
const sourceManifest = path.join(sourceManifestDir, "plugin.json");
const externalFile = path.join(root, "external-read-only-file");
const sourceLink = path.join(sourcePlugin, "external-link");
const targetPlugin = path.join(root, "target-plugin");
const targetManifest = path.join(targetPlugin, ".codex-plugin", "plugin.json");
const targetLink = path.join(targetPlugin, "external-link");
const targetMarketplaceRoot = path.join(root, "runtime", "nested", "openai-bundled");
const originalUmask = process.umask();

try {
fs.mkdirSync(sourceManifestDir, { recursive: true });
fs.writeFileSync(sourceManifest, '{"name":"computer-use"}\n');
fs.writeFileSync(externalFile, "external\n");
fs.chmodSync(externalFile, 0o444);
fs.symlinkSync(externalFile, sourceLink);
fs.chmodSync(sourceManifest, 0o444);
fs.chmodSync(sourceManifestDir, 0o555);
fs.chmodSync(sourcePlugin, 0o555);

const copyPlugin = new Function("process", "require", `${patched};return fl;`)(
{ platform: "linux" },
require,
);
await copyPlugin(sourcePlugin, targetPlugin);
process.umask(0o002);

const materializePlugin = new Function(
"process",
"require",
`${patched};return Ac;`,
)({ ...process, platform: "linux" }, require);
const stagingRoot = await materializePlugin({ sourcePlugin, targetMarketplaceRoot });
const targetPlugin = path.join(stagingRoot, "plugins", "chrome");
const targetManifest = path.join(targetPlugin, ".codex-plugin", "plugin.json");
fs.appendFileSync(targetManifest, "\n");

assert.match(patched, /async function codexLinuxMakeBundledPluginTreeWritable/);
assert.match(patched, /async function codexLinuxValidateBundledPluginSource/);
assert.match(patched, /async function codexLinuxPrepareBundledPluginStage/);
assert.equal(fs.statSync(stagingRoot).mode & 0o777, 0o700);
assert.equal(fs.statSync(path.dirname(targetPlugin)).mode & 0o777, 0o700);
assert.equal(fs.statSync(targetPlugin).mode & 0o200, 0o200);
assert.equal(fs.statSync(targetManifest).mode & 0o200, 0o200);
assert.equal(fs.lstatSync(targetLink).isSymbolicLink(), true);
assert.equal(fs.statSync(externalFile).mode & 0o200, 0);
assert.equal(fs.statSync(targetPlugin).mode & 0o022, 0);
assert.equal(fs.statSync(targetManifest).mode & 0o022, 0);
} finally {
process.umask(originalUmask);
fs.chmodSync(sourcePlugin, 0o755);
fs.chmodSync(sourceManifestDir, 0o755);
fs.rmSync(root, { recursive: true, force: true });
}
});

test("applies the Linux bundled plugin trust patch atomically", () => {
const source = currentBundledPluginCopyBundleFixture();
const withoutStageMkdir = source.replace(
"await m.default.mkdir((0,p.join)(a,...cc.slice(0,-1)),{recursive:!0});",
"await Promise.resolve();",
);
const withoutPluginParentMkdir = source.replace(
"await m.default.mkdir((0,p.dirname)(t),{recursive:!0}),await fl(n,t)",
"await fl(n,t)",
);

assert.equal(applyLinuxBundledPluginCopyPermissionsPatch(withoutStageMkdir), withoutStageMkdir);
assert.equal(
applyLinuxBundledPluginCopyPermissionsPatch(withoutPluginParentMkdir),
withoutPluginParentMkdir,
);
});

test("rejects an untrusted Linux bundled plugin before copying it", async () => {
const patched = applyPatchTwice(
applyLinuxBundledPluginCopyPermissionsPatch,
currentBundledPluginCopyBundleFixture(),
);
const root = fs.mkdtempSync(path.join(os.tmpdir(), "codex-bundled-plugin-trust-"));
const sourceParent = path.join(root, "source-parent");
const sourcePlugin = path.join(sourceParent, "source-plugin");
const sourceClient = path.join(sourcePlugin, "scripts", "browser-client.mjs");
const targetPlugin = path.join(root, "target-plugin");

try {
fs.mkdirSync(path.dirname(sourceClient), { recursive: true });
fs.writeFileSync(sourceClient, "tampered\n");
fs.chmodSync(sourceClient, 0o664);

const copyPlugin = new Function("process", "require", `${patched};return fl;`)(
{ ...process, platform: "linux" },
require,
);
await assert.rejects(copyPlugin(sourcePlugin, targetPlugin), /not trusted/);
assert.equal(fs.existsSync(targetPlugin), false);

fs.chmodSync(sourceClient, 0o644);
fs.chmodSync(sourceParent, 0o775);
await assert.rejects(copyPlugin(sourcePlugin, targetPlugin), /not trusted/);
assert.equal(fs.existsSync(targetPlugin), false);

fs.chmodSync(sourceParent, 0o755);
const unsafeStagingParent = path.join(root, "runtime");
fs.mkdirSync(unsafeStagingParent);
fs.chmodSync(unsafeStagingParent, 0o775);
const materializePlugin = new Function(
"process",
"require",
`${patched};return Ac;`,
)({ ...process, platform: "linux" }, require);
await assert.rejects(
materializePlugin({
sourcePlugin,
targetMarketplaceRoot: path.join(unsafeStagingParent, "openai-bundled"),
}),
/not trusted/,
);
assert.equal(
fs.readdirSync(unsafeStagingParent).some((entry) => entry.includes(".staging-")),
false,
);

fs.symlinkSync(sourceClient, path.join(sourcePlugin, "client-link"));
await assert.rejects(copyPlugin(sourcePlugin, targetPlugin), /not trusted/);
assert.equal(fs.existsSync(targetPlugin), false);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});

function bundledPluginReconcileRaceFixture({
capturedHashVar = "c",
capturedSnapshotVar = "n",
Expand Down
71 changes: 63 additions & 8 deletions scripts/patches/impl/main-process/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,16 @@ const {
} = require("../../lib/minified-js.js");

function applyLinuxBundledPluginCopyPermissionsPatch(currentSource) {
const helperName = "codexLinuxMakeBundledPluginTreeWritable";
if (currentSource.includes(`async function ${helperName}(`)) {
const ancestorHelperName = "codexLinuxValidateBundledPluginAncestors";
const sourceHelperName = "codexLinuxValidateBundledPluginSource";
const stageHelperName = "codexLinuxPrepareBundledPluginStage";
const writableHelperName = "codexLinuxMakeBundledPluginTreeWritable";
if (
currentSource.includes(`async function ${ancestorHelperName}(`) &&
currentSource.includes(`async function ${sourceHelperName}(`) &&
currentSource.includes(`async function ${stageHelperName}(`) &&
currentSource.includes(`async function ${writableHelperName}(`)
) {
return currentSource;
}

Expand All @@ -31,7 +39,7 @@ function applyLinuxBundledPluginCopyPermissionsPatch(currentSource) {
copyBranchRegex,
(_match, platformVar, fsPromisesVar, sourceVar, targetVar) => {
patchedCopyBranch = true;
return `if(${platformVar}.default.platform!==\`win32\`){await ${fsPromisesVar}.default.cp(${sourceVar},${targetVar},{recursive:!0,verbatimSymlinks:!0});if(process.platform===\`linux\`)await ${helperName}(${targetVar},${fsPromisesVar}.default);return}`;
return `if(${platformVar}.default.platform!==\`win32\`){if(process.platform===\`linux\`){await ${fsPromisesVar}.default.cp(await ${sourceHelperName}(${sourceVar},${fsPromisesVar}.default),${targetVar},{recursive:!0,verbatimSymlinks:!0});await ${writableHelperName}(${targetVar},${fsPromisesVar}.default);return}await ${fsPromisesVar}.default.cp(${sourceVar},${targetVar},{recursive:!0,verbatimSymlinks:!0});return}`;
},
);
if (!patchedCopyBranch) {
Expand All @@ -43,16 +51,63 @@ function applyLinuxBundledPluginCopyPermissionsPatch(currentSource) {
return currentSource;
}

const helper =
`async function ${helperName}(e,t){let n=await t.lstat(e);if(n.isSymbolicLink())return;await t.chmod(e,n.mode|128);if(n.isDirectory())for(let n of await t.readdir(e))await ${helperName}((0,${pathVar}.join)(e,n),t)}`;
const stagingMkdirRegex = new RegExp(
`await ([A-Za-z_$][\\w$]*)\\.default\\.mkdir\\(\\(0,${escapeRegExp(pathVar)}\\.join\\)\\(([A-Za-z_$][\\w$]*),\\.\\.\\.([A-Za-z_$][\\w$]*)\\.slice\\(0,-1\\)\\),\\{recursive:!0\\}\\)`,
);
let patchedStagingMkdir = false;
const stagingPatchedSource = patchedSource.replace(
stagingMkdirRegex,
(_match, fsPromisesVar, stageRootVar, manifestPartsVar) => {
patchedStagingMkdir = true;
return `await ${stageHelperName}(${stageRootVar},${fsPromisesVar}.default),await ${fsPromisesVar}.default.mkdir((0,${pathVar}.join)(${stageRootVar},...${manifestPartsVar}.slice(0,-1)),{recursive:!0,mode:448})`;
},
);
if (!patchedStagingMkdir) {
if (currentSource.includes("staging_marketplace")) {
console.warn(
"WARN: Could not find bundled marketplace staging creation — skipping Linux plugin permissions patch",
);
}
return currentSource;
}

const pluginParentMkdirRegex = new RegExp(
`await ([A-Za-z_$][\\w$]*)\\.default\\.mkdir\\(\\(0,${escapeRegExp(pathVar)}\\.dirname\\)\\(([A-Za-z_$][\\w$]*)\\),\\{recursive:!0\\}\\),await ([A-Za-z_$][\\w$]*)\\(([A-Za-z_$][\\w$]*),([A-Za-z_$][\\w$]*)\\)`,
);
let patchedPluginParentMkdir = false;
const fullyPatchedSource = stagingPatchedSource.replace(
pluginParentMkdirRegex,
(_match, fsPromisesVar, targetVar, copyFunctionVar, sourceVar, copyTargetVar) => {
if (copyTargetVar !== targetVar) {
return _match;
}
patchedPluginParentMkdir = true;
return `await ${fsPromisesVar}.default.mkdir((0,${pathVar}.dirname)(${targetVar}),{recursive:!0,mode:448}),await ${copyFunctionVar}(${sourceVar},${targetVar})`;
},
);
if (!patchedPluginParentMkdir) {
if (currentSource.includes("copy_plugins")) {
console.warn(
"WARN: Could not find bundled plugin target parent creation — skipping Linux plugin permissions patch",
);
}
return currentSource;
}

const helpers = [
`async function ${ancestorHelperName}(e,t){let n=await t.realpath(e),r=process.geteuid?.();if(!Number.isInteger(r))throw Error(\`Linux bundled plugin path is not trusted\`);for(let e=n;;){let n=await t.lstat(e),i=n.mode;if(n.isSymbolicLink()||!n.isDirectory()||n.uid!==r&&n.uid!==0||i&18&&!(n.uid===0&&i&512))throw Error(\`Linux bundled plugin path is not trusted\`);let a=(0,${pathVar}.dirname)(e);if(a===e)break;e=a}return n}`,
`async function ${sourceHelperName}(e,t){let n=await ${ancestorHelperName}(e,t),r=process.geteuid(),i=async e=>{let n=await t.lstat(e);if(n.isSymbolicLink()||!n.isDirectory()&&!n.isFile()||n.uid!==r&&n.uid!==0||n.mode&18)throw Error(\`Linux bundled plugin source is not trusted\`);if(n.isDirectory())for(let n of await t.readdir(e))await i((0,${pathVar}.join)(e,n))};return await i(n),n}`,
`async function ${stageHelperName}(e,t){let n=(0,${pathVar}.dirname)(e),r=n;for(;;)try{await t.lstat(r);break}catch(e){if(e?.code!==\`ENOENT\`)throw e;let t=(0,${pathVar}.dirname)(r);if(t===r)throw e;r=t}await ${ancestorHelperName}(r,t),await t.mkdir(n,{recursive:!0,mode:448}),await ${ancestorHelperName}(n,t),await t.mkdir(e,{mode:448});let i=process.geteuid(),a=await t.lstat(e);if(a.isSymbolicLink()||!a.isDirectory()||a.uid!==i)throw Error(\`Linux bundled plugin staging root is not private\`);await t.chmod(e,448),a=await t.lstat(e);if((a.mode&511)!==448)throw Error(\`Linux bundled plugin staging root is not private\`)}`,
`async function ${writableHelperName}(e,t){let n=await t.lstat(e);if(n.isSymbolicLink())throw Error(\`Linux bundled plugin copy contains a symbolic link\`);await t.chmod(e,(n.mode|128)&~18);if(n.isDirectory())for(let n of await t.readdir(e))await ${writableHelperName}((0,${pathVar}.join)(e,n),t)}`,
].join("");
const strictDirective = '"use strict";';
const helperInsertionIndex = currentSource.startsWith(strictDirective)
? strictDirective.length
: 0;
return (
patchedSource.slice(0, helperInsertionIndex) +
helper +
patchedSource.slice(helperInsertionIndex)
fullyPatchedSource.slice(0, helperInsertionIndex) +
helpers +
fullyPatchedSource.slice(helperInsertionIndex)
);
}

Expand Down
Loading