mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
fix: align windows safe-open file identity checks
This commit is contained in:
@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Windows/Media safety checks: align async local-file identity validation with sync-safe-open behavior by treating win32 `dev=0` stats as unknown-device fallbacks (while keeping strict dev checks when both sides are non-zero), fixing false `Local media path is not safe to read` drops for local attachments/TTS/images. (#25708, #21989, #25699, #25878) Thanks @kevinWangSheng.
|
||||
- macOS/WebChat panel: fix rounded-corner clipping by using panel-specific visual-effect blending and matching corner masking on both effect and hosting layers. (#22458) Thanks @apethree and @agisilaos.
|
||||
- macOS/IME input: when marked text is active, treat Return as IME candidate confirmation first in both the voice overlay composer and shared chat composer to prevent accidental sends while composing CJK text. (#25178) Thanks @bottotl.
|
||||
- macOS/Voice wake routing: default forwarded voice-wake transcripts to the `webchat` channel (instead of ambiguous `last` routing) so local voice prompts stay pinned to the control chat surface unless explicitly overridden. (#25440) Thanks @chilu18.
|
||||
|
||||
33
src/infra/file-identity.test.ts
Normal file
33
src/infra/file-identity.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { sameFileIdentity, type FileIdentityStat } from "./file-identity.js";
|
||||
|
||||
function stat(dev: number | bigint, ino: number | bigint): FileIdentityStat {
|
||||
return { dev, ino };
|
||||
}
|
||||
|
||||
describe("sameFileIdentity", () => {
|
||||
it("accepts exact dev+ino match", () => {
|
||||
expect(sameFileIdentity(stat(7, 11), stat(7, 11), "linux")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects inode mismatch", () => {
|
||||
expect(sameFileIdentity(stat(7, 11), stat(7, 12), "linux")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects dev mismatch on non-windows", () => {
|
||||
expect(sameFileIdentity(stat(7, 11), stat(8, 11), "linux")).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts win32 dev mismatch when either side is 0", () => {
|
||||
expect(sameFileIdentity(stat(0, 11), stat(8, 11), "win32")).toBe(true);
|
||||
expect(sameFileIdentity(stat(7, 11), stat(0, 11), "win32")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps dev strictness on win32 when both dev values are non-zero", () => {
|
||||
expect(sameFileIdentity(stat(7, 11), stat(8, 11), "win32")).toBe(false);
|
||||
});
|
||||
|
||||
it("handles bigint stats", () => {
|
||||
expect(sameFileIdentity(stat(0n, 11n), stat(8n, 11n), "win32")).toBe(true);
|
||||
});
|
||||
});
|
||||
25
src/infra/file-identity.ts
Normal file
25
src/infra/file-identity.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export type FileIdentityStat = {
|
||||
dev: number | bigint;
|
||||
ino: number | bigint;
|
||||
};
|
||||
|
||||
function isZero(value: number | bigint): boolean {
|
||||
return value === 0 || value === 0n;
|
||||
}
|
||||
|
||||
export function sameFileIdentity(
|
||||
left: FileIdentityStat,
|
||||
right: FileIdentityStat,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): boolean {
|
||||
if (left.ino !== right.ino) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// On Windows, path-based stat calls can report dev=0 while fd-based stat
|
||||
// reports a real volume serial; treat either-side dev=0 as "unknown device".
|
||||
if (left.dev === right.dev) {
|
||||
return true;
|
||||
}
|
||||
return platform === "win32" && (isZero(left.dev) || isZero(right.dev));
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { constants as fsConstants } from "node:fs";
|
||||
import type { FileHandle } from "node:fs/promises";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { sameFileIdentity } from "./file-identity.js";
|
||||
import { isNotFoundPathError, isPathInside, isSymlinkOpenError } from "./path-guards.js";
|
||||
|
||||
export type SafeOpenErrorCode =
|
||||
@@ -40,23 +41,6 @@ const OPEN_READ_FLAGS = fsConstants.O_RDONLY | (SUPPORTS_NOFOLLOW ? fsConstants.
|
||||
|
||||
const ensureTrailingSep = (value: string) => (value.endsWith(path.sep) ? value : value + path.sep);
|
||||
|
||||
/**
|
||||
* Compare file stats for identity verification.
|
||||
* On Windows, device IDs (dev) are unreliable and may differ between
|
||||
* handle.stat() and fs.lstat() for the same file. We skip dev comparison
|
||||
* on Windows and rely solely on inode (ino) matching.
|
||||
*/
|
||||
function statsMatch(stat1: Stats, stat2: Stats): boolean {
|
||||
if (stat1.ino !== stat2.ino) {
|
||||
return false;
|
||||
}
|
||||
// On Windows, dev values are unreliable across different stat sources
|
||||
if (process.platform !== "win32" && stat1.dev !== stat2.dev) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function openVerifiedLocalFile(filePath: string): Promise<SafeOpenResult> {
|
||||
let handle: FileHandle;
|
||||
try {
|
||||
@@ -79,13 +63,13 @@ async function openVerifiedLocalFile(filePath: string): Promise<SafeOpenResult>
|
||||
if (!stat.isFile()) {
|
||||
throw new SafeOpenError("not-file", "not a file");
|
||||
}
|
||||
if (!statsMatch(stat, lstat)) {
|
||||
if (!sameFileIdentity(stat, lstat)) {
|
||||
throw new SafeOpenError("path-mismatch", "path changed during read");
|
||||
}
|
||||
|
||||
const realPath = await fs.realpath(filePath);
|
||||
const realStat = await fs.stat(realPath);
|
||||
if (!statsMatch(stat, realStat)) {
|
||||
if (!sameFileIdentity(stat, realStat)) {
|
||||
throw new SafeOpenError("path-mismatch", "path mismatch");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
import { sameFileIdentity as hasSameFileIdentity } from "./file-identity.js";
|
||||
|
||||
export type SafeOpenSyncFailureReason = "path" | "validation" | "io";
|
||||
|
||||
@@ -17,12 +18,7 @@ function isExpectedPathError(error: unknown): boolean {
|
||||
}
|
||||
|
||||
export function sameFileIdentity(left: fs.Stats, right: fs.Stats): boolean {
|
||||
// On Windows, lstatSync (by path) may return dev=0 while fstatSync (by fd)
|
||||
// returns the real volume serial number. When either dev is 0, fall back to
|
||||
// ino-only comparison which is still unique within a single volume.
|
||||
const devMatch =
|
||||
left.dev === right.dev || (process.platform === "win32" && (left.dev === 0 || right.dev === 0));
|
||||
return devMatch && left.ino === right.ino;
|
||||
return hasSameFileIdentity(left, right);
|
||||
}
|
||||
|
||||
export function openVerifiedFileSync(params: {
|
||||
|
||||
Reference in New Issue
Block a user