-
Notifications
You must be signed in to change notification settings - Fork 701
fix(sdk): keep restored scan artifacts within their output directory #468
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6e0e19c
5cf4ee7
4ca4c82
3368d07
e8fc845
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -996,6 +996,10 @@ async function secureWindowsCredentialHome(path: string): Promise<void> { | |
| } | ||
| } | ||
|
|
||
| interface CredentialLockReadRetry { | ||
| deadline?: number; | ||
| } | ||
|
|
||
| export async function acquireCodexSecurityCredentialHomeLock( | ||
| codexHome: string, | ||
| signal?: AbortSignal, | ||
|
|
@@ -1010,9 +1014,11 @@ export async function acquireCodexSecurityCredentialHomeLock( | |
| ); | ||
| const expectedDevice = homeMetadata.dev; | ||
| const expectedInode = homeMetadata.ino; | ||
| const platform = securityOptions.platform ?? process.platform; | ||
| const lock = join(codexHome, CREDENTIAL_LOCK_NAME); | ||
| const ownerPath = join(lock, "owner.json"); | ||
| const token = randomUUID(); | ||
| const readRetry: CredentialLockReadRetry = {}; | ||
|
|
||
| while (true) { | ||
| throwIfSignalAborted(signal); | ||
|
|
@@ -1027,10 +1033,12 @@ export async function acquireCodexSecurityCredentialHomeLock( | |
| throw error; | ||
| }); | ||
| if (existingLock !== null) { | ||
| if (await recoverStaleCredentialHomeLock(lock)) continue; | ||
| if (await recoverStaleCredentialHomeLock(lock, platform, readRetry)) | ||
| continue; | ||
| await delay(CREDENTIAL_LOCK_POLL_MILLISECONDS, undefined, { signal }); | ||
| continue; | ||
| } | ||
| delete readRetry.deadline; | ||
| await requireSecureCredentialHome(codexHome, { | ||
| ...securityOptions, | ||
| expectedDevice, | ||
|
|
@@ -1040,7 +1048,8 @@ export async function acquireCodexSecurityCredentialHomeLock( | |
| await mkdir(lock, { mode: 0o700 }); | ||
| } catch (error) { | ||
| if (nodeErrorCode(error) !== "EEXIST") throw error; | ||
| if (await recoverStaleCredentialHomeLock(lock)) continue; | ||
| if (await recoverStaleCredentialHomeLock(lock, platform, readRetry)) | ||
| continue; | ||
| await delay(CREDENTIAL_LOCK_POLL_MILLISECONDS, undefined, { signal }); | ||
| continue; | ||
| } | ||
|
|
@@ -1078,23 +1087,45 @@ export async function acquireCodexSecurityCredentialHomeLock( | |
| } | ||
| } | ||
|
|
||
| async function recoverStaleCredentialHomeLock(lock: string): Promise<boolean> { | ||
| async function recoverStaleCredentialHomeLock( | ||
| lock: string, | ||
| platform: NodeJS.Platform, | ||
| readRetry: CredentialLockReadRetry, | ||
| ): Promise<boolean> { | ||
| const metadata = await lstat(lock).catch((error: unknown) => { | ||
| if (nodeErrorCode(error) === "ENOENT") return null; | ||
| throw error; | ||
| }); | ||
| if (metadata === null) return true; | ||
| if (metadata === null) { | ||
| delete readRetry.deadline; | ||
| return true; | ||
| } | ||
| if (!metadata.isDirectory() || metadata.isSymbolicLink()) { | ||
| throw new OutputDirectoryError( | ||
| `Codex Security credential-home lock is not a directory: ${lock}`, | ||
| ); | ||
| } | ||
|
|
||
| let owner: unknown; | ||
| const deadline = | ||
| platform === "win32" | ||
| ? (readRetry.deadline ??= | ||
| Date.now() + INCOMPLETE_CREDENTIAL_LOCK_MILLISECONDS) | ||
| : undefined; | ||
|
Comment on lines
+1110
to
+1114
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On Windows, if one lock remains unreadable for most of this deadline and is then released and quickly replaced before the waiter observes an absent directory, AGENTS.md reference: sdk/typescript/AGENTS.md:L24-L24 Useful? React with 👍 / 👎. |
||
| try { | ||
| owner = JSON.parse(await readFile(join(lock, "owner.json"), "utf8")); | ||
| } catch (error) { | ||
| if (nodeErrorCode(error) !== "ENOENT" && !(error instanceof SyntaxError)) { | ||
| const code = nodeErrorCode(error); | ||
| // Re-enter the acquisition loop so a Windows read retry rechecks the lock. | ||
| if ( | ||
| deadline !== undefined && | ||
| (code === "EPERM" || code === "EBUSY") && | ||
| Date.now() < deadline | ||
| ) { | ||
| return false; | ||
| } | ||
| delete readRetry.deadline; | ||
| if (code !== "ENOENT" && !(error instanceof SyntaxError)) { | ||
| throw error; | ||
| } | ||
| if ( | ||
|
|
@@ -1104,6 +1135,7 @@ async function recoverStaleCredentialHomeLock(lock: string): Promise<boolean> { | |
| return false; | ||
| } | ||
| } | ||
| delete readRetry.deadline; | ||
|
|
||
| if (isRecord(owner) && typeof owner["pid"] === "number") { | ||
| try { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a background command started during the post-scan phase replaces a validated parent directory with a symlink after this check returns, the subsequent
writeFile(temporary, ...)andrename(temporary, path)follow that replacement and can overwrite a matching file outside the scan directory. The helper returns only a pathname, so none of the captured parent identities are bound to or revalidated around the actual restoration write; keep the write and rename tied to the checked directory identity so a concurrent swap fails safely.AGENTS.md reference: sdk/typescript/AGENTS.md:L19-L20
Useful? React with 👍 / 👎.