Skip to content
23 changes: 14 additions & 9 deletions src/lib/shields/transition-lock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,26 +312,31 @@ describe("host shields transition lock", () => {
});
});

it("returns no inspected owner when the canonical path changes identity before open", () => {
it("returns no inspected owner when the canonical path changes identity after open", () => {
const locker = manager();
const original = owner("alpha", 202, "proc:original", "original", TAKEOVER_TOKEN);
const replacement = owner("alpha", 303, "proc:replacement", "replacement", TAKEOVER_TOKEN);
const lockPath = writeOwner("alpha", original);
const displacedPath = `${lockPath}.displaced`;
const originalOpenSync = fs.openSync;
const originalFstatSync = fs.fstatSync;
let swapped = false;
vi.spyOn(fs, "openSync").mockImplementation(((file, flags, mode) => {
runWhen(String(file) === lockPath && !swapped, () => {
const fstatSpy = vi.spyOn(fs, "fstatSync").mockImplementation(((fd, options) => {
const stat = originalFstatSync(fd, options as { bigint: true });
runWhen(!swapped, () => {
swapped = true;
fs.renameSync(lockPath, displacedPath);
fs.writeFileSync(lockPath, JSON.stringify(replacement), { mode: 0o600 });
});
return originalOpenSync(file, flags, mode);
}) as typeof fs.openSync);
return stat;
}) as typeof fs.fstatSync);

expect(locker.inspectShieldsTransitionLockOwner("alpha", TAKEOVER_TOKEN)).toBeNull();
expect(JSON.parse(fs.readFileSync(lockPath, "utf8"))).toEqual(replacement);
expect(JSON.parse(fs.readFileSync(displacedPath, "utf8"))).toEqual(original);
try {
expect(locker.inspectShieldsTransitionLockOwner("alpha", TAKEOVER_TOKEN)).toBeNull();
expect(JSON.parse(fs.readFileSync(lockPath, "utf8"))).toEqual(replacement);
expect(JSON.parse(fs.readFileSync(displacedPath, "utf8"))).toEqual(original);
} finally {
fstatSpy.mockRestore();
}
});

it("rejects takeover with the wrong token without moving the owner", () => {
Expand Down
32 changes: 18 additions & 14 deletions src/lib/shields/transition-lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,22 +239,9 @@ function unsafeLockPathError(lockPath: string, reason: string): Error {
}

function readExistingLock(lockPath: string, sandboxName: string): ExistingLockSnapshot | null {
let pathStat: fs.BigIntStats;
try {
pathStat = fs.lstatSync(lockPath, { bigint: true });
} catch (error) {
if (isErrnoException(error) && error.code === "ENOENT") return null;
throw error;
}
if (pathStat.isSymbolicLink()) {
throw unsafeLockPathError(lockPath, "symbolic links are not allowed");
}
if (!pathStat.isFile()) {
throw unsafeLockPathError(lockPath, "path is not a regular file");
}

let fd: number;
try {
// Open first so every subsequent decision is anchored to one no-follow descriptor.
fd = fs.openSync(
lockPath,
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK,
Expand All @@ -272,6 +259,23 @@ function readExistingLock(lockPath: string, sandboxName: string): ExistingLockSn
if (!fdStat.isFile()) {
throw unsafeLockPathError(lockPath, "path is not a regular file");
}

let pathStat: fs.BigIntStats;
try {
pathStat = fs.lstatSync(lockPath, { bigint: true });
} catch (error) {
if (isErrnoException(error) && error.code === "ENOENT") {
fs.closeSync(fd);
return null;
}
throw error;
}
if (pathStat.isSymbolicLink()) {
throw unsafeLockPathError(lockPath, "symbolic links are not allowed");
}
if (!pathStat.isFile()) {
throw unsafeLockPathError(lockPath, "path is not a regular file");
}
if (!sameInode(inodeIdentity(pathStat), inodeIdentity(fdStat))) {
fs.closeSync(fd);
return null;
Expand Down
Loading