diff --git a/src/codex/shim.ts b/src/codex/shim.ts index 312c946a09..88ad80c657 100644 --- a/src/codex/shim.ts +++ b/src/codex/shim.ts @@ -403,6 +403,14 @@ function stableShimPathProbe(path: string): StableShimPathProbe | null { return contentSize > 0 ? { fingerprint, prefix } : null; } +function shimPathFingerprint(path: string): ShimPathFingerprint | null { + const fingerprint = statFingerprint(path, false); + if (!fingerprint) return null; + if (fingerprint.kind !== "symlink") return fingerprint; + const target = statFingerprint(path, true); + return target ? { ...fingerprint, target } : null; +} + function sameStableShimPathProbe(left: StableShimPathProbe, right: StableShimPathProbe): boolean { return left.prefix === right.prefix && sameFingerprint(left.fingerprint, right.fingerprint); } @@ -830,9 +838,9 @@ function rollbackFreshShimInstall(journal: readonly FreshShimInstallJournalEntry } try { if (entry.originalMovedToBackup && existsSync(target.backupPath)) { - const movedOriginal = stableShimPathProbe(target.backupPath); + const movedOriginal = shimPathFingerprint(target.backupPath); if (!movedOriginal || !entry.movedOriginalFingerprint - || !sameFingerprint(movedOriginal.fingerprint, entry.movedOriginalFingerprint)) { + || !sameFingerprint(movedOriginal, entry.movedOriginalFingerprint)) { throw new Error("Codex shim fresh-install backup changed during rollback"); } if (sourceOccupied) { @@ -1833,9 +1841,14 @@ function installCodexShimInternal(options: InstallCodexShimInternalOptions): { i renameSync(target.originalPath, target.backupPath); entry.originalMovedToBackup = true; if (process.platform !== "win32") { + const movedOriginalFingerprint = shimPathFingerprint(target.backupPath); + if (!movedOriginalFingerprint) throw new Error("Codex shim fresh install could not fingerprint the staged launcher"); + entry.movedOriginalFingerprint = movedOriginalFingerprint; const movedOriginal = stableShimPathProbe(target.backupPath); if (!movedOriginal) throw new Error("Codex shim fresh install could not fingerprint the staged launcher"); - entry.movedOriginalFingerprint = movedOriginal.fingerprint; + if (!sameFingerprint(movedOriginal.fingerprint, movedOriginalFingerprint)) { + throw new Error("Codex shim fresh install staged launcher changed while being fingerprinted"); + } } } if (!target.preserveOnly) { diff --git a/tests/codex-shim.test.ts b/tests/codex-shim.test.ts index dbf74d38ab..8e073cd184 100644 --- a/tests/codex-shim.test.ts +++ b/tests/codex-shim.test.ts @@ -791,6 +791,36 @@ wait "$child" } }); + test("Unix fresh install restores an original that cannot be content-probed", () => { + if (process.platform === "win32") return; + + const binDir = mkdtempSync(join(tmpdir(), "ocx-shim-install-empty-bin-")); + const home = mkdtempSync(join(tmpdir(), "ocx-shim-install-empty-home-")); + const oldPath = process.env.PATH; + const oldHome = process.env.OPENCODEX_HOME; + const codexPath = join(binDir, "codex"); + try { + process.env.PATH = prependPath(binDir, oldPath); + process.env.OPENCODEX_HOME = home; + writeFileSync(codexPath, "", "utf8"); + chmodSync(codexPath, 0o755); + + expect(() => installCodexShim()).toThrow("could not fingerprint the staged launcher"); + + expect(existsSync(codexPath)).toBe(true); + expect(readFileSync(codexPath, "utf8")).toBe(""); + expect(existsSync(`${codexPath}.opencodex-real`)).toBe(false); + expect(existsSync(join(home, "codex-shim.json"))).toBe(false); + } finally { + if (oldPath === undefined) delete process.env.PATH; + else process.env.PATH = oldPath; + if (oldHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = oldHome; + rmSync(binDir, { recursive: true, force: true }); + rmSync(home, { recursive: true, force: true }); + } + }); + test("Unix fresh install removes its marker-bearing partial wrapper before rollback", () => { if (process.platform === "win32") return;