mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-27 12:05:01 +02:00
fix(memory-vault): compare dev only when both stats report one (#2637)
ecc memory writes and --body-file reads fail on Windows. sameFileIdentity() compares the dev field of a path-based stat against a handle-based fstat, and libuv 1.49.0 through 1.50.x resolve path-based stat() and lstat() on Windows through GetFileInformationByName, which leaves the volume serial unset while fstat() reports it. The comparison never matches, so the TOCTOU guard rejects every operation. Keep the inode strict and compare dev only when both sides report one. POSIX always reports a non-zero dev, so the original strict behaviour is preserved there. Request the guard's stats as BigInt. On the affected libuv versions dev is 0, which leaves the inode as the only identity signal, and Windows file IDs run past Number.MAX_SAFE_INTEGER where two distinct files can collapse to the same number-valued inode. Fixes #2626
This commit is contained in:
@@ -122,7 +122,21 @@ function assertMemoryDirectorySafe(directory, root) {
|
||||
}
|
||||
|
||||
function sameFileIdentity(left, right) {
|
||||
return left.dev === right.dev && left.ino === right.ino;
|
||||
// The inode is the primary identity signal and must always match.
|
||||
if (left.ino !== right.ino) {
|
||||
return false;
|
||||
}
|
||||
// libuv 1.49.0 through 1.50.x resolve path-based stat() and lstat() on Windows
|
||||
// through GetFileInformationByName, which leaves the volume serial unset, while
|
||||
// fstat() on an open handle reports it. Comparing the two then never matches and
|
||||
// every vault read and write is rejected. libuv 82cdfb75f fixed this in 1.51.0,
|
||||
// so only Node 22.12-22.16 and 24.0-24.1 are affected, but the guard should not
|
||||
// depend on the runtime's patch level. Compare dev only when both sides report
|
||||
// one; POSIX always does, so the original strict behaviour is preserved there.
|
||||
if (!left.dev || !right.dev) {
|
||||
return true;
|
||||
}
|
||||
return left.dev === right.dev;
|
||||
}
|
||||
|
||||
function readRegularTextFile(filePath, options = {}) {
|
||||
@@ -137,11 +151,11 @@ function readRegularTextFile(filePath, options = {}) {
|
||||
| (fs.constants.O_NONBLOCK || 0);
|
||||
const descriptor = fs.openSync(filePath, flags);
|
||||
try {
|
||||
const opened = fs.fstatSync(descriptor);
|
||||
const opened = fs.fstatSync(descriptor, { bigint: true });
|
||||
if (!opened.isFile()) {
|
||||
throw new Error(`${label} must be a regular, non-symlink file.`);
|
||||
}
|
||||
const after = fs.lstatSync(filePath);
|
||||
const after = fs.lstatSync(filePath, { bigint: true });
|
||||
if (
|
||||
after.isSymbolicLink()
|
||||
|| !after.isFile()
|
||||
@@ -152,7 +166,7 @@ function readRegularTextFile(filePath, options = {}) {
|
||||
if (options.trustedRoot) {
|
||||
assertWithinTrustedRoot(filePath, options.trustedRoot, `read ${label}`);
|
||||
}
|
||||
if (opened.size > maxBytes) {
|
||||
if (opened.size > BigInt(maxBytes)) {
|
||||
throw new Error(`${label} is too large (${opened.size} bytes).`);
|
||||
}
|
||||
|
||||
@@ -189,8 +203,8 @@ function writeCreateOnlyTextFile(filePath, content, trustedRoot) {
|
||||
let cleanupError;
|
||||
try {
|
||||
descriptor = fs.openSync(temporaryPath, flags, 0o600);
|
||||
const opened = fs.fstatSync(descriptor);
|
||||
const after = fs.lstatSync(temporaryPath);
|
||||
const opened = fs.fstatSync(descriptor, { bigint: true });
|
||||
const after = fs.lstatSync(temporaryPath, { bigint: true });
|
||||
assertWithinTrustedRoot(temporaryPath, trustedRoot, 'write memory');
|
||||
if (
|
||||
!opened.isFile()
|
||||
@@ -770,6 +784,7 @@ module.exports = {
|
||||
readMemoryById,
|
||||
readMemoryFiles,
|
||||
resolveVaultRoots,
|
||||
sameFileIdentity,
|
||||
saveMemory,
|
||||
scoreMemory,
|
||||
searchMemories,
|
||||
|
||||
Reference in New Issue
Block a user