FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

test: cover ZIP fd lifecycle and add rollback · nodejs/node@00b687b · GitHub

/ node Public

Commit 00b687b

Browse files
authored andcommitted
test: cover ZIP fd lifecycle and add rollback
Add regression tests for two node:zlib ZipFile robustness issues: - A read in flight when close() is called must complete on a live descriptor; close() must not release the fd out from under it (which surfaces as EBADF, or an OS-reused-fd cross-file read). - If the central-directory rewrite fails after add() has written the member bytes, both the in-memory state and the on-disk archive must be rolled back, not left half-updated. Signed-off-by: Philipp Dunkel <pip@pipobscure.com> PR-URL: #65016 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
1 parent 2bda6db commit 00b687b

1 file changed

Lines changed: 92 additions & 0 deletions

File tree

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
'use strict';
2+
3+
// Lifecycle-hardening regression tests for node:zlib ZipFile (the on-disk,
4+
// fd-backed reader/writer). Each asserts the secure behavior, so it fails on
5+
// the pre-fix code and passes once its fix lands.
6+
//
7+
// 1. A read in flight when close() is called must complete on a live
8+
// descriptor - close() must not pull the fd out from under it (which would
9+
// surface as EBADF, or worse read another file once the fd number is
10+
// reused).
11+
// 2. If the central-directory rewrite fails after an add() has written the
12+
// member bytes, the in-memory state and the on-disk archive must be rolled
13+
// back to exactly what they were before the call, not left half-updated.
14+
15+
require('../common');
16+
const assert = require('assert');
17+
const { test } = require('node:test');
18+
const zlib = require('zlib');
19+
const fs = require('fs');
20+
const tmpdir = require('../common/tmpdir');
21+
22+
const SIG_CENTRAL = 0x02014b50;
23+
const SIG_EOCD = 0x06054b50;
24+
25+
function writeArchive(file, entries) {
26+
const chunks = [];
27+
for (const chunk of zlib.createZipArchiveSync(entries)) chunks.push(chunk);
28+
fs.writeFileSync(file, Buffer.concat(chunks));
29+
}
30+
31+
// 1. A read that is in flight when the ZipFile is closed still completes.
32+
test('close() waits for an in-flight read instead of closing under it', async () => {
33+
tmpdir.refresh();
34+
const file = tmpdir.resolve('inflight.zip');
35+
const payload = Buffer.alloc(8 * 1024 * 1024, 0x5a);
36+
writeArchive(file, [zlib.ZipEntry.createSync('big', payload, { method: 'store' })]);
37+
38+
const zf = zlib.ZipFile.openSync(file);
39+
const entry = zf.getSync('big');
40+
const reading = entry.content(); // In flight; do not await yet
41+
await zf.close(); // Must wait for the read, not close the fd under it
42+
const data = await reading; // Must resolve with correct bytes, not reject EBADF
43+
assert.strictEqual(data.length, payload.length);
44+
assert.ok(data.equals(payload));
45+
});
46+
47+
// 2. A failed central-directory rewrite during add() is rolled back.
48+
test('a failed directory rewrite during addEntrySync is rolled back', () => {
49+
tmpdir.refresh();
50+
const file = tmpdir.resolve('addfail.zip');
51+
writeArchive(file, [zlib.ZipEntry.createSync('first.txt', Buffer.from('original'),
52+
{ method: 'store' })]);
53+
54+
const zf = zlib.ZipFile.openSync(file, { writable: true });
55+
const toAdd = zlib.ZipEntry.createSync('second.txt', Buffer.from('added'), { method: 'store' });
56+
57+
// Fail only the first central-directory write (its buffer starts with the
58+
// central-header or EOCD signature); the member bytes start with the local
59+
// header signature and pass through, and the rollback rewrite that follows
60+
// succeeds so the on-disk archive is restored.
61+
const realWriteSync = fs.writeSync;
62+
let failNextDirectoryWrite = true;
63+
fs.writeSync = function(fd, buffer, offset, length, position) {
64+
if (failNextDirectoryWrite && Buffer.isBuffer(buffer) && buffer.length - offset >= 4) {
65+
const sig = buffer.readUInt32LE(offset);
66+
if (sig === SIG_CENTRAL || sig === SIG_EOCD) {
67+
failNextDirectoryWrite = false;
68+
const err = new Error('ENOSPC: simulated no space left on device');
69+
err.code = 'ENOSPC';
70+
throw err;
71+
}
72+
}
73+
return realWriteSync.call(fs, fd, buffer, offset, length, position);
74+
};
75+
try {
76+
assert.throws(() => zf.addEntrySync(toAdd), { code: 'ENOSPC' });
77+
} finally {
78+
fs.writeSync = realWriteSync;
79+
}
80+
81+
// In-memory: the half-added entry is gone and the original is still readable.
82+
assert.ok(!zf.has('second.txt'));
83+
assert.strictEqual(zf.getSync('first.txt').contentSync().toString(), 'original');
84+
zf.closeSync();
85+
86+
// On disk: reopening shows the original, uncorrupted archive.
87+
const reopened = zlib.ZipFile.openSync(file);
88+
assert.ok(reopened.has('first.txt'));
89+
assert.ok(!reopened.has('second.txt'));
90+
assert.strictEqual(reopened.getSync('first.txt').contentSync().toString(), 'original');
91+
reopened.closeSync();
92+
});

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL