fix(auth): harden shared-context isolation and config safety

This commit is contained in:
Tam Nhu Tran committed 2026-02-25 04:19:28 +07:00
1 parent 0b070a3f34
commit 0309630193
18 files changed
+695 -197

No files matched your search

+4 -98
View File
@@ -8,8 +8,8 @@
import * as fs from 'fs';
import * as path from 'path';
import { createHash } from 'crypto';
import SharedManager from './shared-manager';
import ProfileContextSyncLock from './profile-context-sync-lock';
import { AccountContextPolicy, DEFAULT_ACCOUNT_CONTEXT_MODE } from '../auth/account-context';
import { getCcsDir } from '../utils/config-manager';
@@ -18,13 +18,13 @@ import { getCcsDir } from '../utils/config-manager';
*/
class InstanceManager {
private readonly instancesDir: string;
private readonly locksDir: string;
private readonly sharedManager: SharedManager;
private readonly contextSyncLock: ProfileContextSyncLock;
constructor() {
this.instancesDir = path.join(getCcsDir(), 'instances');
this.locksDir = path.join(this.instancesDir, '.locks');
this.sharedManager = new SharedManager();
this.contextSyncLock = new ProfileContextSyncLock(this.instancesDir);
}
/**
@@ -37,7 +37,7 @@ class InstanceManager {
const instancePath = this.getInstancePath(profileName);
// Serialize context sync operations per profile across processes.
await this.withContextSyncLock(profileName, async () => {
await this.contextSyncLock.withLock(profileName, async () => {
// Lazy initialization
if (!fs.existsSync(instancePath)) {
this.initializeInstance(profileName, instancePath);
@@ -201,100 +201,6 @@ class InstanceManager {
// Replace unsafe characters with dash
return name.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
}
private getContextSyncLockPath(profileName: string): string {
const safeName = this.sanitizeName(profileName);
// Keep lock filenames deterministic while preventing normalized-name collisions.
const profileHash = createHash('sha1').update(profileName).digest('hex').slice(0, 8);
return path.join(this.locksDir, `${safeName}-${profileHash}.lock`);
}
private isProcessAlive(pid: number): boolean {
if (!Number.isInteger(pid) || pid <= 0) {
return false;
}
try {
process.kill(pid, 0);
return true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'EPERM') {
return true;
}
return false;
}
}
private tryRemoveDeadOwnerLock(lockPath: string): boolean {
try {
const lockContent = fs.readFileSync(lockPath, 'utf8').trim();
const lockPid = Number.parseInt(lockContent, 10);
if (!this.isProcessAlive(lockPid)) {
fs.unlinkSync(lockPath);
return true;
}
} catch {
// Best-effort stale lock cleanup.
}
return false;
}
private async withContextSyncLock<T>(
profileName: string,
callback: () => Promise<T>
): Promise<T> {
const lockPath = this.getContextSyncLockPath(profileName);
const retryDelayMs = 50;
const staleLockMs = 30000;
const timeoutMs = staleLockMs + 5000;
const start = Date.now();
fs.mkdirSync(this.locksDir, { recursive: true, mode: 0o700 });
while (true) {
try {
const fd = fs.openSync(lockPath, 'wx', 0o600);
fs.writeFileSync(fd, `${process.pid}`);
fs.closeSync(fd);
break;
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (err.code !== 'EEXIST') {
throw error;
}
try {
const lockStats = fs.statSync(lockPath);
if (Date.now() - lockStats.mtimeMs > staleLockMs) {
fs.unlinkSync(lockPath);
continue;
}
if (this.tryRemoveDeadOwnerLock(lockPath)) {
continue;
}
} catch {
// Best-effort stale lock cleanup.
}
if (Date.now() - start > timeoutMs) {
throw new Error(`Timed out waiting for profile context lock: ${profileName}`);
}
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
}
}
try {
return await callback();
} finally {
try {
fs.unlinkSync(lockPath);
} catch {
// Best-effort cleanup.
}
}
}
}
export { InstanceManager };