diff --git a/install.sh b/install.sh index 84d09edc5..850197e9d 100755 --- a/install.sh +++ b/install.sh @@ -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" diff --git a/launcher/start.sh.template b/launcher/start.sh.template index 81a55b082..9c0950bce 100644 --- a/launcher/start.sh.template +++ b/launcher/start.sh.template @@ -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" @@ -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" diff --git a/scripts/build-appimage.sh b/scripts/build-appimage.sh index fc82883e9..7efd2b1ba 100755 --- a/scripts/build-appimage.sh +++ b/scripts/build-appimage.sh @@ -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() { diff --git a/scripts/lib/bundled-plugins.sh b/scripts/lib/bundled-plugins.sh index 63c574763..632091161 100644 --- a/scripts/lib/bundled-plugins.sh +++ b/scripts/lib/bundled-plugins.sh @@ -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" @@ -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" } diff --git a/scripts/patch-linux-window-ui.test.js b/scripts/patch-linux-window-ui.test.js index 82398cb3e..bb7f3789f 100644 --- a/scripts/patch-linux-window-ui.test.js +++ b/scripts/patch-linux-window-ui.test.js @@ -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}" ); } @@ -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(), @@ -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", diff --git a/scripts/patches/impl/main-process/browser.js b/scripts/patches/impl/main-process/browser.js index a6a6c5ad0..690f580dc 100644 --- a/scripts/patches/impl/main-process/browser.js +++ b/scripts/patches/impl/main-process/browser.js @@ -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; } @@ -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) { @@ -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) ); } diff --git a/tests/scripts_smoke.sh b/tests/scripts_smoke.sh index e362cafab..c9745d263 100755 --- a/tests/scripts_smoke.sh +++ b/tests/scripts_smoke.sh @@ -1340,6 +1340,7 @@ test_appimage_builder_smoke() { make_fake_app "$app_dir" mkdir -p "$app_dir/resources/codex-cli" printf '%s\n' 'upstream-payload' > "$app_dir/resources/codex-cli/preserve.txt" + chmod 0775 "$app_dir" "$app_dir/resources" cat > "$bin_dir/appimagetool" <<'SCRIPT' #!/usr/bin/env bash @@ -1390,6 +1391,8 @@ SCRIPT assert_file_exists "$capture_dir/AppDir/opt/codex-desktop/.codex-linux/codex-packaged-runtime.sh" assert_file_exists "$capture_dir/AppDir/opt/codex-desktop/resources/node-runtime/bin/node" assert_file_exists "$capture_dir/AppDir/opt/codex-desktop/resources/codex-cli/preserve.txt" + assert_mode "$capture_dir/AppDir/opt/codex-desktop" "755" + assert_mode "$capture_dir/AppDir/opt/codex-desktop/resources" "755" assert_file_not_exists "$capture_dir/AppDir/opt/codex-desktop/resources/codex-cli/bin/codex" assert_file_not_exists "$capture_dir/AppDir/usr/bin/codex-update-manager" assert_file_not_exists "$capture_dir/AppDir/usr/lib/systemd/user/codex-update-manager.service" @@ -2795,6 +2798,7 @@ test_transactional_install_uses_managed_node_and_isolated_reports() { assert_contains "$REPO_DIR/install.sh" 'CODEX_ACCEPTANCE_NODE="\$CODEX_MANAGED_NODE_RUNTIME_DIR/bin/node"' assert_contains "$REPO_DIR/install.sh" 'report_dir="\$report_base/transactions/\$transaction_id"' assert_contains "$REPO_DIR/install.sh" '"\$CODEX_ACCEPTANCE_NODE" "\$SCRIPT_DIR/scripts/validate-upstream-dmg.js"' + assert_contains "$REPO_DIR/install.sh" "harden_bundled_plugin_source_tree" } test_installer_cleanup_handles_readonly_trees() { @@ -5096,15 +5100,47 @@ test_launcher_marketplace_metadata_atomic_staging() ( local observed_temp local observed_target - mkdir -p "$(dirname "$source_marketplace")" "$target_dir" + mkdir -p \ + "$(dirname "$source_marketplace")" \ + "$target_dir" \ + "$codex_home/.tmp/bundled-marketplaces/openai-bundled/plugins" printf '%s\n' 'new metadata' > "$source_marketplace" - awk '/^stage_bundled_marketplace_metadata\(\) \{/{copy=1} copy{print} copy && /^}/{exit}' \ + awk '/^make_path_owner_trusted\(\) \{/{copy=1} copy{print} copy && /^}/{exit}' \ "$REPO_DIR/launcher/start.sh.template" > "$function_file" + awk '/^path_has_unsafe_write\(\) \{/{copy=1} copy{print} copy && /^}/{exit}' \ + "$REPO_DIR/launcher/start.sh.template" >> "$function_file" + awk '/^prepare_bundled_marketplace_tmp_paths\(\) \{/{copy=1} copy{print} copy && /^}/{exit}' \ + "$REPO_DIR/launcher/start.sh.template" >> "$function_file" + awk '/^stage_bundled_marketplace_metadata\(\) \{/{copy=1} copy{print} copy && /^}/{exit}' \ + "$REPO_DIR/launcher/start.sh.template" >> "$function_file" # shellcheck source=/dev/null source "$function_file" SCRIPT_DIR="$app_dir" CODEX_HOME="$codex_home" + umask 0002 + chmod 0775 \ + "$codex_home" \ + "$codex_home/.tmp" \ + "$codex_home/.tmp/bundled-marketplaces" \ + "$codex_home/.tmp/bundled-marketplaces/openai-bundled" \ + "$codex_home/.tmp/bundled-marketplaces/openai-bundled/.agents" \ + "$codex_home/.tmp/bundled-marketplaces/openai-bundled/.agents/plugins" \ + "$codex_home/.tmp/bundled-marketplaces/openai-bundled/plugins" + prepare_bundled_marketplace_tmp_paths full + for trusted_path in \ + "$codex_home" \ + "$codex_home/.tmp" \ + "$codex_home/.tmp/bundled-marketplaces" \ + "$codex_home/.tmp/bundled-marketplaces/openai-bundled" \ + "$codex_home/.tmp/bundled-marketplaces/openai-bundled/.agents" \ + "$codex_home/.tmp/bundled-marketplaces/openai-bundled/.agents/plugins" \ + "$codex_home/.tmp/bundled-marketplaces/openai-bundled/plugins"; do + if find "$trusted_path" -maxdepth 0 -perm /022 -print -quit | grep -q .; then + fail "Bundled marketplace parent remained group/world writable: $trusted_path" + fi + done + for failing_command in cp chmod mv; do printf '%s\n' 'existing metadata' > "$target_marketplace" observed_temp="" @@ -5379,7 +5415,7 @@ if "second_instance_handoff_ready" not in runtime_body: raise SystemExit("second-instance handoff must skip cold-start setup") if "clear_bundled_marketplace_tmp_cache\nreconcile_runtime_state" in runtime_body: raise SystemExit("warm-start path must not clear bundled marketplace temp cache") -if not re.search(r'if needs_cold_start; then\s+log_phase "cold_start_cache_sync_start"\s+clear_bundled_marketplace_tmp_cache.*?stage_bundled_marketplace_metadata.*?sync_browser_use_bundled_plugin_cache &.*?sync_chrome_bundled_plugin_cache &.*?sync_computer_use_bundled_plugin_cache &.*?sync_read_aloud_bundled_plugin_cache &.*?sync_extra_bundled_plugin_cache &.*?run_cold_start_hooks.*?log_phase "cold_start_hooks_dispatched"\s+await_webview_server_ready\s+fi', runtime_body, re.S): +if not re.search(r'if needs_cold_start; then\s+log_phase "cold_start_cache_sync_start"\s+if ! prepare_bundled_marketplace_tmp_paths; then.*?clear_bundled_marketplace_tmp_cache.*?if ! prepare_bundled_marketplace_tmp_paths full; then.*?stage_bundled_marketplace_metadata.*?sync_browser_use_bundled_plugin_cache &.*?sync_chrome_bundled_plugin_cache &.*?sync_computer_use_bundled_plugin_cache &.*?sync_read_aloud_bundled_plugin_cache &.*?sync_extra_bundled_plugin_cache &.*?run_cold_start_hooks.*?log_phase "cold_start_hooks_dispatched"\s+await_webview_server_ready\s+fi', runtime_body, re.S): raise SystemExit("bundled marketplace cleanup, staged metadata, concurrent plugin syncs, cold-start hooks, and the webview readiness wait must run only on cold start") # The plugin syncs run concurrently, so the shared marketplace.json is staged # exactly once beforehand and every sync is awaited before cold-start hooks. @@ -8039,6 +8075,7 @@ test_chrome_plugin_staging() { local host="$chrome_dir/extension-host/linux/x64/extension-host" mkdir -p "$workspace" "$install_dir/resources" + chmod 0775 "$install_dir" "$install_dir/resources" make_fake_chrome_upstream_app "$app_dir" ( @@ -8048,6 +8085,7 @@ test_chrome_plugin_staging() { ARCH="x86_64" ICON_SOURCE="$workspace/missing-icon.png" CODEX_APP_ID="codex-desktop" + umask 0002 mkdir -p "$WORK_DIR" warn() { echo "[WARN] $*" >&2; } info() { echo "[INFO] $*" >&2; } @@ -8061,6 +8099,7 @@ test_chrome_plugin_staging() { printf '%s\n' "$fake_host" } install_bundled_plugin_resources "$app_dir" + harden_bundled_plugin_source_tree ) >"$output_log" 2>&1 assert_file_exists "$host" @@ -8114,6 +8153,9 @@ test_chrome_plugin_staging() { assert_contains "$chrome_dir/skills/control-chrome/SKILL.md" "agent.browsers.list()" assert_contains "$chrome_dir/skills/control-chrome/SKILL.md" "browser.tabs.new()" assert_contains "$install_dir/resources/plugins/openai-bundled/.agents/plugins/marketplace.json" '"name": "chrome"' + assert_mode "$install_dir" "755" + assert_mode "$install_dir/resources" "755" + assert_mode "$install_dir/resources/plugins" "755" [ -z "$(find "$install_dir/resources/plugins/openai-bundled" -perm /022 -print -quit)" ] \ || fail "Expected staged bundled plugin resources to reject group/other writes" assert_contains "$output_log" "Chrome plugin staged from upstream DMG"