Skip to content

Commit 92f7630

Browse files
fix(auth): verify credential lock ownership
1 parent c404a5b commit 92f7630

1 file changed

Lines changed: 36 additions & 13 deletions

File tree

src/lib/credentials.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
writeFileSync,
1010
} from 'node:fs';
1111
import { spawnSync, type SpawnSyncReturns } from 'node:child_process';
12+
import { randomUUID } from 'node:crypto';
1213
import { homedir } from 'node:os';
1314
import { dirname, join } from 'node:path';
1415
import { localValidationError } from './errors.js';
@@ -69,6 +70,11 @@ interface CredentialsLockInfo {
6970
token?: string;
7071
}
7172

73+
interface CredentialsLock {
74+
assertHeld: () => void;
75+
release: () => void;
76+
}
77+
7278
interface RestrictiveModeOptions {
7379
platform?: NodeJS.Platform;
7480
env?: NodeJS.ProcessEnv;
@@ -261,29 +267,29 @@ function mutateCredentialsFile(
261267
mutate: (file: CredentialsFile) => CredentialsFile | undefined,
262268
): void {
263269
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
264-
const releaseLock = acquireCredentialsLock(path);
270+
const lock = acquireCredentialsLock(path);
265271
try {
266272
const file = readCredentialsFile({ path });
267273
const nextFile = mutate(file);
268274
if (nextFile === undefined) return;
275+
lock.assertHeld();
269276
writeCredentialsAtomic(path, nextFile);
270277
} finally {
271-
releaseLock();
278+
lock.release();
272279
}
273280
}
274281

275282
function writeCredentialsAtomic(path: string, file: CredentialsFile): void {
276-
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
277283
const tmp = `${path}.tmp.${process.pid}`;
278284
writeFileSync(tmp, serializeCredentials(file), { mode: 0o600, encoding: 'utf8' });
279285
renameSync(tmp, path);
280286
ensureRestrictiveMode(path);
281287
}
282288

283-
function acquireCredentialsLock(path: string): () => void {
289+
function acquireCredentialsLock(path: string): CredentialsLock {
284290
const lockPath = `${path}.lock`;
285291
const deadline = Date.now() + CREDENTIALS_LOCK_WAIT_MS;
286-
const token = `${process.pid}:${Date.now()}:${Math.random().toString(16).slice(2)}`;
292+
const token = `${process.pid}:${Date.now()}:${randomUUID()}`;
287293
const lockInfo: Required<CredentialsLockInfo> = {
288294
pid: process.pid,
289295
createdAt: Date.now(),
@@ -297,7 +303,10 @@ function acquireCredentialsLock(path: string): () => void {
297303
flag: 'wx',
298304
mode: 0o600,
299305
});
300-
return () => releaseCredentialsLock(lockPath, token);
306+
return {
307+
assertHeld: () => assertCredentialsLockHeld(lockPath, token),
308+
release: () => releaseCredentialsLock(lockPath, token),
309+
};
301310
} catch (error) {
302311
if (!isErrnoException(error) || error.code !== 'EEXIST') {
303312
throw error;
@@ -341,14 +350,20 @@ function reclaimStaleCredentialsLock(lockPath: string): void {
341350
}
342351
}
343352

353+
function assertCredentialsLockHeld(lockPath: string, token: string): void {
354+
const lockInfo = readCredentialsLockInfo(lockPath);
355+
if (lockInfo?.token === token) return;
356+
throw localValidationError(
357+
'credentialsLock',
358+
'lost ownership of the credential update lock; retry the command',
359+
undefined,
360+
'field',
361+
);
362+
}
363+
344364
function releaseCredentialsLock(lockPath: string, token: string): void {
345-
let lockInfo: CredentialsLockInfo | undefined;
346-
try {
347-
lockInfo = JSON.parse(readFileSync(lockPath, 'utf-8')) as CredentialsLockInfo;
348-
} catch {
349-
return;
350-
}
351-
if (lockInfo.token !== token) return;
365+
const lockInfo = readCredentialsLockInfo(lockPath);
366+
if (lockInfo?.token !== token) return;
352367
try {
353368
unlinkSync(lockPath);
354369
} catch (error) {
@@ -358,6 +373,14 @@ function releaseCredentialsLock(lockPath: string, token: string): void {
358373
}
359374
}
360375

376+
function readCredentialsLockInfo(lockPath: string): CredentialsLockInfo | undefined {
377+
try {
378+
return JSON.parse(readFileSync(lockPath, 'utf-8')) as CredentialsLockInfo;
379+
} catch {
380+
return undefined;
381+
}
382+
}
383+
361384
function isProcessAlive(pid: number): boolean {
362385
if (pid === process.pid) return true;
363386
try {

0 commit comments

Comments
 (0)