fix: bound capsule token writes across Node versions

This commit is contained in:
haelyra committed 2026-10-12 01:27:02 -04:00
1 parent e598c85dde
commit 2b17c89f99
2 files changed
+134 -25

No files matched your search

+11 -7
View File
@@ -103,6 +103,15 @@ function releaseOwnedLock(lockPath, fd, identity, ownerToken) {
}
}
function writeAll(fd, bytes, target) {
let offset = 0;
while (offset < bytes.length) {
const written = fs.writeSync(fd, bytes, offset, bytes.length - offset, null);
if (written <= 0) throw new CapsuleError('capsule.write_failed', `${target} write made no progress`);
offset += written;
}
}
/** Exclusive cooperative append lock. Never waits or infers stale ownership. */
function withAppendLock(dir, operation) {
const lockPath = path.join(dir, APPEND_LOCK_FILE);
@@ -118,7 +127,7 @@ function withAppendLock(dir, operation) {
try {
identity = fs.fstatSync(fd);
const token = crypto.randomBytes(16).toString('hex');
fs.writeFileSync(fd, token, 'utf8');
writeAll(fd, Buffer.from(token, 'utf8'), 'append lock');
ownerToken = token;
return operation();
} finally {
@@ -220,12 +229,7 @@ class Capsule {
const bytes = Buffer.from(canonicalJson(entry) + '\n', 'utf8');
const fd = fs.openSync(this.journalPath, 'a');
try {
let offset = 0;
while (offset < bytes.length) {
const written = fs.writeSync(fd, bytes, offset, bytes.length - offset, null);
if (written <= 0) throw new CapsuleError('capsule.write_failed', 'journal write made no progress');
offset += written;
}
writeAll(fd, bytes, 'journal');
fs.fsyncSync(fd);
} finally {
fs.closeSync(fd);
+123 -18
View File
@@ -304,11 +304,18 @@ test('validation or clock exceptions release ownership so a later append can pro
test('short writes complete the entire UTF-8 journal entry before acknowledgement', () => {
const dir = tempDir('short-write');
const originalWrite = fs.writeSync;
const originalOpen = fs.openSync;
let journalFd;
let chunks = 0;
try {
const c = capsule.Capsule.create(dir);
fs.openSync = (file, flags, ...args) => {
const fd = originalOpen(file, flags, ...args);
if (file === path.join(dir, capsule.JOURNAL_FILE) && flags === 'a') journalFd = fd;
return fd;
};
fs.writeSync = (fd, data, offset, length, position) => {
if (!Buffer.isBuffer(data)) return originalWrite(fd, data, offset, length);
if (fd !== journalFd) return originalWrite(fd, data, offset, length, position);
chunks += 1;
return originalWrite(fd, data, offset, Math.min(length, 7), position);
};
@@ -317,17 +324,24 @@ test('short writes complete the entire UTF-8 journal entry before acknowledgemen
assert.ok(capsule.verify(dir).ok);
assert.strictEqual(c.entries()[0].payload.message, 'snow \u2603');
assert.ok(!fs.existsSync(appendLock(dir)));
} finally { fs.writeSync = originalWrite; cleanup(dir); }
} finally { fs.writeSync = originalWrite; fs.openSync = originalOpen; cleanup(dir); }
});
test('partial write failure leaves evidence and prevents later append from hiding the tail', () => {
const dir = tempDir('partial-write');
const originalWrite = fs.writeSync;
const originalOpen = fs.openSync;
let journalFd;
let chunks = 0;
try {
const c = capsule.Capsule.create(dir);
fs.openSync = (file, flags, ...args) => {
const fd = originalOpen(file, flags, ...args);
if (file === path.join(dir, capsule.JOURNAL_FILE) && flags === 'a') journalFd = fd;
return fd;
};
fs.writeSync = (fd, data, offset, length, position) => {
if (!Buffer.isBuffer(data)) return originalWrite(fd, data, offset, length);
if (fd !== journalFd) return originalWrite(fd, data, offset, length, position);
if (chunks++ > 0) throw new Error('write fixture');
return originalWrite(fd, data, offset, Math.min(length, 9), position);
};
@@ -340,22 +354,29 @@ test('partial write failure leaves evidence and prevents later append from hidin
assert.ok(!fs.existsSync(appendLock(dir)));
assert.throws(() => c.append('plan', 'later', {}), error => error.code === 'capsule.truncated_tail');
assert.deepStrictEqual(fs.readFileSync(journal), bytes);
} finally { fs.writeSync = originalWrite; cleanup(dir); }
} finally { fs.writeSync = originalWrite; fs.openSync = originalOpen; cleanup(dir); }
});
test('a zero-progress write fails and releases the lock without pretending success', () => {
const dir = tempDir('zero-write');
const originalWrite = fs.writeSync;
const originalOpen = fs.openSync;
let journalFd;
try {
const c = capsule.Capsule.create(dir);
fs.writeSync = () => 0;
fs.openSync = (file, flags, ...args) => {
const fd = originalOpen(file, flags, ...args);
if (file === path.join(dir, capsule.JOURNAL_FILE) && flags === 'a') journalFd = fd;
return fd;
};
fs.writeSync = (fd, ...args) => fd === journalFd ? 0 : originalWrite(fd, ...args);
assert.throws(() => c.append('plan', 'zero', {}), error => error.code === 'capsule.write_failed');
fs.writeSync = originalWrite;
assert.strictEqual(capsule.verify(dir).entry_count, 0);
assert.ok(!fs.existsSync(appendLock(dir)));
assert.strictEqual(c.append('plan', 'later', {}).seq, 0);
} finally { fs.writeSync = originalWrite; cleanup(dir); }
} finally { fs.writeSync = originalWrite; fs.openSync = originalOpen; cleanup(dir); }
});
test('fsync failure is an ambiguous acknowledgement and the next append reloads disk', () => {
@@ -457,11 +478,11 @@ for (const scenario of [
test(`lock release: ${scenario.name}`, () => {
const dir = tempDir('lock-close-boundary');
const lock = appendLock(dir);
const original = { open: fs.openSync, close: fs.closeSync, stat: fs.lstatSync, unlink: fs.unlinkSync, write: fs.writeFileSync };
const original = { open: fs.openSync, close: fs.closeSync, stat: fs.lstatSync, unlink: fs.unlinkSync, write: fs.writeSync };
const permissionError = Object.assign(new Error('synthetic lock inspection denied'), { code: scenario.code || 'EPERM' });
const closeError = Object.assign(new Error('synthetic ambiguous close failure'), { code: 'EIO' });
let ownedFd;
let ownedToken;
let ownedToken = '';
let closed = false;
let closes = 0;
let inspections = 0;
@@ -473,9 +494,9 @@ for (const scenario of [
if (file === lock && args[0] === 'wx') ownedFd = fd;
return fd;
};
fs.writeFileSync = function(file, data, ...args) {
const result = original.write.call(this, file, data, ...args);
if (file === ownedFd) ownedToken = data;
fs.writeSync = function(fd, data, offset, length, position) {
const result = original.write.call(this, fd, data, offset, length, position);
if (fd === ownedFd) ownedToken += data.subarray(offset, offset + result).toString('utf8');
return result;
};
fs.lstatSync = function(file, ...args) {
@@ -516,7 +537,7 @@ for (const scenario of [
fs.closeSync = original.close;
fs.lstatSync = original.stat;
fs.unlinkSync = original.unlink;
fs.writeFileSync = original.write;
fs.writeSync = original.write;
if (scenario.outcome === 'missing') assert.strictEqual(fs.existsSync(lock), false);
else assert.strictEqual(fs.readFileSync(lock, 'utf8'), scenario.outcome === 'replacement' ? 'replacement owner' : ownedToken);
// Release failure can follow a complete durable append; never infer rollback.
@@ -527,7 +548,7 @@ for (const scenario of [
fs.closeSync = original.close;
fs.lstatSync = original.stat;
fs.unlinkSync = original.unlink;
fs.writeFileSync = original.write;
fs.writeSync = original.write;
cleanup(dir);
}
});
@@ -536,7 +557,7 @@ for (const scenario of [
test('failed ownership-token initialization preserves the primary error and partial barrier', () => {
const dir = tempDir('lock-token-init');
const lock = appendLock(dir);
const original = { open: fs.openSync, write: fs.writeFileSync, close: fs.closeSync };
const original = { open: fs.openSync, write: fs.writeSync, close: fs.closeSync };
const failure = Object.assign(new Error('synthetic token initialization failure'), { code: 'EIO' });
let ownedFd;
let closed = false;
@@ -547,9 +568,9 @@ test('failed ownership-token initialization preserves the primary error and part
if (file === lock && flags === 'wx') ownedFd = fd;
return fd;
};
fs.writeFileSync = (file, ...args) => {
if (file === ownedFd) throw failure;
return original.write(file, ...args);
fs.writeSync = (fd, ...args) => {
if (fd === ownedFd) throw failure;
return original.write(fd, ...args);
};
fs.closeSync = fd => {
const result = original.close(fd);
@@ -563,12 +584,96 @@ test('failed ownership-token initialization preserves the primary error and part
assert.throws(() => capsule.Capsule.open(dir).append('plan', 'blocked', {}), error => error.code === 'capsule.busy');
} finally {
fs.openSync = original.open;
fs.writeFileSync = original.write;
fs.writeSync = original.write;
fs.closeSync = original.close;
cleanup(dir);
}
});
test('partial ownership-token failure preserves exact bytes and the primary error', () => {
const dir = tempDir('partial-token');
const lock = appendLock(dir);
const original = { open: fs.openSync, write: fs.writeSync, close: fs.closeSync };
const failure = Object.assign(new Error('partial token write fixture'), { code: 'EIO' });
let ownedFd;
let prefix;
let closes = 0;
try {
const c = capsule.Capsule.create(dir);
fs.openSync = (file, flags, ...args) => {
const fd = original.open(file, flags, ...args);
if (file === lock && flags === 'wx') ownedFd = fd;
return fd;
};
fs.writeSync = (fd, data, offset, length, position) => {
if (fd !== ownedFd) return original.write(fd, data, offset, length, position);
if (prefix) throw failure;
const written = original.write(fd, data, offset, Math.min(length, 7), position);
prefix = data.subarray(offset, offset + written).toString('utf8');
return written;
};
fs.closeSync = fd => { if (fd === ownedFd) closes += 1; return original.close(fd); };
assert.throws(() => c.append('plan', 'owner', {}), error => error === failure);
assert.strictEqual(closes, 1);
assert.match(prefix, /^[0-9a-f]{7}$/);
assert.strictEqual(fs.readFileSync(lock, 'utf8'), prefix);
assert.strictEqual(capsule.verify(dir).entry_count, 0);
assert.throws(() => capsule.Capsule.open(dir).append('plan', 'blocked', {}), error => error.code === 'capsule.busy');
assert.strictEqual(fs.readFileSync(lock, 'utf8'), prefix);
} finally {
fs.openSync = original.open; fs.writeSync = original.write; fs.closeSync = original.close; cleanup(dir);
}
});
test('zero-progress ownership-token write refuses before journal mutation and retains its barrier', () => {
const dir = tempDir('zero-token');
const lock = appendLock(dir);
const original = { open: fs.openSync, write: fs.writeSync, close: fs.closeSync };
let ownedFd;
let closes = 0;
try {
const c = capsule.Capsule.create(dir);
fs.openSync = (file, flags, ...args) => {
const fd = original.open(file, flags, ...args);
if (file === lock && flags === 'wx') ownedFd = fd;
return fd;
};
fs.writeSync = (fd, ...args) => fd === ownedFd ? 0 : original.write(fd, ...args);
fs.closeSync = fd => { if (fd === ownedFd) closes += 1; return original.close(fd); };
assert.throws(() => c.append('plan', 'owner', {}), error => error.code === 'capsule.write_failed');
assert.strictEqual(closes, 1);
assert.strictEqual(fs.readFileSync(lock, 'utf8'), '');
assert.strictEqual(capsule.verify(dir).entry_count, 0);
assert.throws(() => capsule.Capsule.open(dir).append('plan', 'blocked', {}), error => error.code === 'capsule.busy');
} finally {
fs.openSync = original.open; fs.writeSync = original.write; fs.closeSync = original.close; cleanup(dir);
}
});
test('short ownership-token writes finish before the journal is acknowledged', () => {
const dir = tempDir('short-token');
const lock = appendLock(dir);
const original = { open: fs.openSync, write: fs.writeSync };
let ownedFd;
let tokenChunks = 0;
try {
const c = capsule.Capsule.create(dir);
fs.openSync = (file, flags, ...args) => {
const fd = original.open(file, flags, ...args);
if (file === lock && flags === 'wx') ownedFd = fd;
return fd;
};
fs.writeSync = (fd, data, offset, length, position) => {
if (fd === ownedFd) { tokenChunks += 1; return original.write(fd, data, offset, Math.min(length, 3), position); }
return original.write(fd, data, offset, length, position);
};
assert.strictEqual(c.append('plan', 'owner', {}).seq, 0);
assert.ok(tokenChunks > 1);
assert.strictEqual(capsule.verify(dir).entry_count, 1);
assert.strictEqual(fs.existsSync(lock), false);
} finally { fs.openSync = original.open; fs.writeSync = original.write; cleanup(dir); }
});
test('invalid payloads leave journal unchanged and release the append lock', () => {
const dir = tempDir();
try {