mirror of https://github.com/openclaw/openclaw.git
Infra: relax win32 guarded-open identity checks
This commit is contained in:
parent
96fc5ddfb3
commit
de94126771
|
|
@ -0,0 +1,116 @@
|
|||
import fs from "node:fs";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { openVerifiedFileSync } from "./safe-open-sync.js";
|
||||
|
||||
type TestIoFs = NonNullable<Parameters<typeof openVerifiedFileSync>[0]["ioFs"]>;
|
||||
|
||||
function createStats(params: {
|
||||
dev: number | bigint;
|
||||
ino: number | bigint;
|
||||
nlink?: number;
|
||||
size?: number;
|
||||
file?: boolean;
|
||||
symlink?: boolean;
|
||||
}): fs.Stats {
|
||||
return {
|
||||
dev: params.dev,
|
||||
ino: params.ino,
|
||||
nlink: params.nlink ?? 1,
|
||||
size: params.size ?? 16,
|
||||
isFile: () => params.file ?? true,
|
||||
isSymbolicLink: () => params.symlink ?? false,
|
||||
} as unknown as fs.Stats;
|
||||
}
|
||||
|
||||
function createIoFs(params: { realPath?: string; lstat: fs.Stats; fstat: fs.Stats }): {
|
||||
ioFs: TestIoFs;
|
||||
mocks: {
|
||||
lstatSync: ReturnType<typeof vi.fn>;
|
||||
realpathSync: ReturnType<typeof vi.fn>;
|
||||
openSync: ReturnType<typeof vi.fn>;
|
||||
fstatSync: ReturnType<typeof vi.fn>;
|
||||
closeSync: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
} {
|
||||
const realPath = params.realPath ?? "C:/tmp/demo.json";
|
||||
const lstatSync = vi.fn(() => params.lstat);
|
||||
const realpathSync = vi.fn(() => realPath);
|
||||
const openSync = vi.fn(() => 42);
|
||||
const fstatSync = vi.fn(() => params.fstat);
|
||||
const closeSync = vi.fn(() => undefined);
|
||||
|
||||
const ioFs = {
|
||||
constants: { O_RDONLY: 0, O_NOFOLLOW: 0 },
|
||||
lstatSync,
|
||||
realpathSync,
|
||||
openSync,
|
||||
fstatSync,
|
||||
closeSync,
|
||||
} as unknown as TestIoFs;
|
||||
|
||||
return {
|
||||
ioFs,
|
||||
mocks: {
|
||||
lstatSync,
|
||||
realpathSync,
|
||||
openSync,
|
||||
fstatSync,
|
||||
closeSync,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("openVerifiedFileSync", () => {
|
||||
it("rejects identity mismatch on non-windows", () => {
|
||||
const preOpen = createStats({ dev: 1, ino: 100 });
|
||||
const opened = createStats({ dev: 2, ino: 200 });
|
||||
const { ioFs, mocks } = createIoFs({ lstat: preOpen, fstat: opened });
|
||||
|
||||
const result = openVerifiedFileSync({
|
||||
filePath: "C:/tmp/demo.json",
|
||||
ioFs,
|
||||
platform: "linux",
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: false, reason: "validation" });
|
||||
expect(mocks.openSync).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.closeSync).toHaveBeenCalledWith(42);
|
||||
});
|
||||
|
||||
it("allows win32 identity mismatch for regular non-hardlinked files", () => {
|
||||
const preOpen = createStats({ dev: 1, ino: 100, nlink: 1 });
|
||||
const opened = createStats({ dev: 2, ino: 200, nlink: 1 });
|
||||
const { ioFs, mocks } = createIoFs({ lstat: preOpen, fstat: opened });
|
||||
|
||||
const result = openVerifiedFileSync({
|
||||
filePath: "C:/tmp/demo.json",
|
||||
ioFs,
|
||||
platform: "win32",
|
||||
rejectHardlinks: true,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) {
|
||||
throw new Error("expected open result");
|
||||
}
|
||||
expect(result.path).toBe("C:/tmp/demo.json");
|
||||
expect(result.fd).toBe(42);
|
||||
expect(mocks.closeSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still rejects win32 files when hardlink guard fails", () => {
|
||||
const preOpen = createStats({ dev: 1, ino: 100, nlink: 1 });
|
||||
const opened = createStats({ dev: 2, ino: 200, nlink: 2 });
|
||||
const { ioFs, mocks } = createIoFs({ lstat: preOpen, fstat: opened });
|
||||
|
||||
const result = openVerifiedFileSync({
|
||||
filePath: "C:/tmp/demo.json",
|
||||
ioFs,
|
||||
platform: "win32",
|
||||
rejectHardlinks: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: false, reason: "validation" });
|
||||
expect(mocks.closeSync).toHaveBeenCalledWith(42);
|
||||
});
|
||||
});
|
||||
|
|
@ -18,8 +18,23 @@ function isExpectedPathError(error: unknown): boolean {
|
|||
return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
|
||||
}
|
||||
|
||||
export function sameFileIdentity(left: fs.Stats, right: fs.Stats): boolean {
|
||||
return hasSameFileIdentity(left, right);
|
||||
function shouldBypassWin32IdentityMismatch(params: {
|
||||
platform: NodeJS.Platform;
|
||||
preOpenStat: fs.Stats;
|
||||
openedStat: fs.Stats;
|
||||
}): boolean {
|
||||
if (params.platform !== "win32") {
|
||||
return false;
|
||||
}
|
||||
return params.preOpenStat.nlink <= 1 && params.openedStat.nlink <= 1;
|
||||
}
|
||||
|
||||
export function sameFileIdentity(
|
||||
left: fs.Stats,
|
||||
right: fs.Stats,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): boolean {
|
||||
return hasSameFileIdentity(left, right, platform);
|
||||
}
|
||||
|
||||
export function openVerifiedFileSync(params: {
|
||||
|
|
@ -29,8 +44,10 @@ export function openVerifiedFileSync(params: {
|
|||
rejectHardlinks?: boolean;
|
||||
maxBytes?: number;
|
||||
ioFs?: SafeOpenSyncFs;
|
||||
platform?: NodeJS.Platform;
|
||||
}): SafeOpenSyncResult {
|
||||
const ioFs = params.ioFs ?? fs;
|
||||
const platform = params.platform ?? process.platform;
|
||||
const openReadFlags =
|
||||
ioFs.constants.O_RDONLY |
|
||||
(typeof ioFs.constants.O_NOFOLLOW === "number" ? ioFs.constants.O_NOFOLLOW : 0);
|
||||
|
|
@ -66,7 +83,15 @@ export function openVerifiedFileSync(params: {
|
|||
if (params.maxBytes !== undefined && openedStat.size > params.maxBytes) {
|
||||
return { ok: false, reason: "validation" };
|
||||
}
|
||||
if (!sameFileIdentity(preOpenStat, openedStat)) {
|
||||
|
||||
if (
|
||||
!sameFileIdentity(preOpenStat, openedStat, platform) &&
|
||||
!shouldBypassWin32IdentityMismatch({
|
||||
platform,
|
||||
preOpenStat,
|
||||
openedStat,
|
||||
})
|
||||
) {
|
||||
return { ok: false, reason: "validation" };
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue