Version
v24.16.0 (also reproduces on v26.1.0; last good v24.15.0)
Platform
Linux matt-desktop-cachyos 7.0.12-1-cachyos #1 SMP PREEMPT_DYNAMIC Sat, 13 Jun 2026 07:50:52 +0000 x86_64 GNU/Linux
Subsystem
stream and zlib
What steps will reproduce the bug?
Save the follow as repro.cjs then node repro.cjs:
const { Readable } = require('node:stream');
const zlib = require('node:zlib');
const raw = Buffer.allocUnsafe(384 * 1024);
let x = 0x9e3779b9 >>> 0;
for (let i = 0; i < raw.length; i++) {
x ^= x << 13; x ^= x >>> 17; x ^= x << 5; x >>>= 0;
raw[i] = x % 6 === 0 ? (x & 0xff) : ((i >> 4) & 0x3f);
}
const compressed = zlib.deflateRawSync(raw, { level: 6 });
class FdSliceLikeReadable extends Readable {
constructor(buf) { super(); this.buf = buf; this.pos = 0; this.ended = false; }
_read(n) {
if (this.ended) return;
const toRead = Math.min(this._readableState.highWaterMark, n, this.buf.length - this.pos);
if (toRead <= 0) {
this.destroyed = true; // remove this line and 'end' fires normally
this.ended = true;
this.push(null);
return;
}
const start = this.pos; this.pos += toRead;
setImmediate(() => this.push(this.buf.subarray(start, start + toRead)));
}
}
let bytes = 0;
const inflate = new FdSliceLikeReadable(compressed).pipe(zlib.createInflateRaw());
inflate.on('data', (c) => { bytes += c.length; });
inflate.on('end', () => console.log(`OK end fired: ${bytes}/${raw.length} bytes`));
process.on('exit', () => {
if (bytes !== raw.length) console.log(`BUG: no 'end'; delivered ${bytes}/${raw.length}`);
});
How often does it reproduce? Is there a required condition?
Happens every run deterministically.
What is the expected behavior? Why is that the expected behavior?
I expect end to fire and receive a OK end fired: 393216/393216 bytes in the repro snippet.
What do you see instead?
On 24.16.0/26.1.0 inflate stalls partway and end never fires; the process exits with the read unfinished: BUG: no 'end'; delivered 295677/393216. Real apps awaiting end see "Detected unsettled top-level await."
Additional information
Appears to be a regression between 24.15.0 to 24.16.0. I haven't had time to bisect the versions to find a likely commit but it's most likely around anything stream related.
Removing the this.destroyed = true assignment, or replacing createInflateRaw with a PassThrough, both make the hang disappear, so the trigger is a Readable mutating the managed destroyed flag before push(null) while feeding a zlib transform. This is the root cause of the yauzl/extract-zip hangs in #63487 (their fd-slicer dependency uses this exact _read pattern).
Version
v24.16.0 (also reproduces on v26.1.0; last good v24.15.0)
Platform
Subsystem
stream and zlib
What steps will reproduce the bug?
Save the follow as
repro.cjsthennode repro.cjs:How often does it reproduce? Is there a required condition?
Happens every run deterministically.
What is the expected behavior? Why is that the expected behavior?
I expect
endto fire and receive aOK end fired: 393216/393216 bytesin the repro snippet.What do you see instead?
On 24.16.0/26.1.0 inflate stalls partway and end never fires; the process exits with the read unfinished:
BUG: no 'end'; delivered 295677/393216. Real apps awaiting end see "Detected unsettled top-level await."Additional information
Appears to be a regression between 24.15.0 to 24.16.0. I haven't had time to bisect the versions to find a likely commit but it's most likely around anything stream related.
Removing the
this.destroyed = trueassignment, or replacing createInflateRaw with aPassThrough, both make the hang disappear, so the trigger is aReadablemutating the managed destroyed flag beforepush(null)while feeding a zlib transform. This is the root cause of theyauzl/extract-ziphangs in #63487 (theirfd-slicerdependency uses this exact_readpattern).