mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-26 18:35:05 +00:00
fix: bound capsule token writes across Node versions
This commit is contained in:
2 files changed
+134
-25
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user