Skip to content

Commit 0590212

Browse files
rom1504claude
andcommitted
Fix zlib crashes on Node 24 with truncated compressed data
Node 24's stricter zlib can throw synchronously or emit errors asynchronously when processing truncated data during disconnection. - Wrap zlib.unzip() and zlib.deflate() in try/catch for sync throws - Wrap gunzipSync in try/catch in minecraft.js NBT parsing - Suppress compressor/decompressor errors during client shutdown (this.ended check prevents unhandled error events) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dfecf1e commit 0590212

3 files changed

Lines changed: 44 additions & 23 deletions

File tree

src/client.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,11 +224,15 @@ class Client extends EventEmitter {
224224
setCompressionThreshold (threshold) {
225225
if (this.compressor == null) {
226226
this.compressor = compression.createCompressor(threshold)
227-
this.compressor.on('error', (err) => this.emit('error', err))
227+
this.compressor.on('error', (err) => {
228+
if (!this.ended) this.emit('error', err)
229+
})
228230
this.serializer.unpipe(this.framer)
229231
this.serializer.pipe(this.compressor).pipe(this.framer)
230232
this.decompressor = compression.createDecompressor(threshold, this.hideErrors)
231-
this.decompressor.on('error', (err) => this.emit('error', err))
233+
this.decompressor.on('error', (err) => {
234+
if (!this.ended) this.emit('error', err)
235+
})
232236
this.splitter.unpipe(this.deserializer)
233237
this.splitter.pipe(this.decompressor).pipe(this.deserializer)
234238
} else {

src/datatypes/minecraft.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,12 @@ function readCompressedNbt (buffer, offset) {
5656

5757
const compressedNbt = buffer.slice(offset + 2, offset + 2 + length)
5858

59-
const nbtBuffer = zlib.gunzipSync(compressedNbt) // TODO: async
59+
let nbtBuffer
60+
try {
61+
nbtBuffer = zlib.gunzipSync(compressedNbt) // TODO: async
62+
} catch (err) {
63+
throw new PartialReadError('zlib decompress failed: ' + err.message)
64+
}
6065

6166
const results = nbt.proto.read(nbtBuffer, 0, 'nbt')
6267
return {

src/transforms/compression.js

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,18 @@ class Compressor extends Transform {
2020

2121
_transform (chunk, enc, cb) {
2222
if (chunk.length >= this.compressionThreshold) {
23-
zlib.deflate(chunk, (err, newChunk) => {
24-
if (err) { return cb(err) }
25-
const buf = Buffer.alloc(sizeOfVarInt(chunk.length) + newChunk.length)
26-
const offset = writeVarInt(chunk.length, buf, 0)
27-
newChunk.copy(buf, offset)
28-
this.push(buf)
23+
try {
24+
zlib.deflate(chunk, (err, newChunk) => {
25+
if (err) { return cb(err) }
26+
const buf = Buffer.alloc(sizeOfVarInt(chunk.length) + newChunk.length)
27+
const offset = writeVarInt(chunk.length, buf, 0)
28+
newChunk.copy(buf, offset)
29+
this.push(buf)
30+
return cb()
31+
})
32+
} catch (err) {
2933
return cb()
30-
})
34+
}
3135
} else {
3236
const buf = Buffer.alloc(sizeOfVarInt(0) + chunk.length)
3337
const offset = writeVarInt(0, buf, 0)
@@ -52,23 +56,31 @@ class Decompressor extends Transform {
5256
this.push(chunk.slice(size))
5357
return cb()
5458
} else {
55-
zlib.unzip(chunk.slice(size), { finishFlush: 2 /* Z_SYNC_FLUSH = 2, but when using Browserify/Webpack it doesn't exist */ }, (err, newBuf) => { /** Fix by lefela4. */
56-
if (err) {
57-
if (!this.hideErrors) {
58-
console.error('problem inflating chunk')
59-
console.error('uncompressed length ' + value)
60-
console.error('compressed length ' + chunk.length)
61-
console.error('hex ' + chunk.toString('hex'))
62-
console.log(err)
59+
try {
60+
zlib.unzip(chunk.slice(size), { finishFlush: 2 /* Z_SYNC_FLUSH = 2, but when using Browserify/Webpack it doesn't exist */ }, (err, newBuf) => { /** Fix by lefela4. */
61+
if (err) {
62+
if (!this.hideErrors) {
63+
console.error('problem inflating chunk')
64+
console.error('uncompressed length ' + value)
65+
console.error('compressed length ' + chunk.length)
66+
console.error('hex ' + chunk.toString('hex'))
67+
console.log(err)
68+
}
69+
return cb()
70+
}
71+
if (newBuf.length !== value && !this.hideErrors) {
72+
console.error('uncompressed length should be ' + value + ' but is ' + newBuf.length)
6373
}
74+
this.push(newBuf)
6475
return cb()
76+
})
77+
} catch (err) {
78+
// Node 24's stricter zlib can throw synchronously on truncated data
79+
if (!this.hideErrors) {
80+
console.error('zlib.unzip threw:', err.message)
6581
}
66-
if (newBuf.length !== value && !this.hideErrors) {
67-
console.error('uncompressed length should be ' + value + ' but is ' + newBuf.length)
68-
}
69-
this.push(newBuf)
7082
return cb()
71-
})
83+
}
7284
}
7385
}
7486
}

0 commit comments

Comments
 (0)