Repository navigation
Conversation
…output reset() on a ZstdCompress stream dropped the context of a frame whose first bytes were already in the output. The next frame followed that fragment, and the output failed to decode with ZSTD_error_corruption_detected. Node.js v26.11.0 and later refuse that reset (nodejs/node#66088). The native handle reports ERR_ZLIB_INCOMPLETE_FRAME through onerror, and the stream is destroyed. The zstd Context now records that the open frame has given output, and Context::reset returns the same error before it makes a new context.
Collaborator
Author
|
Updated 3:33 PM PT - Oct 29th, 2026
❌ @robobun, your commit 4209187 has 2 failures in
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.179fba3f-2659-4cd2-939f-f91e94fcd9eb
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.36311fb5-da3e-429c-98be-6289af773e7d
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.c49912e8-7b08-4b3e-9541-d216c99ad995
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.259ee84d-200f-4627-b61d-365e046e5f1c
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.225e25a4-d621-413d-80b1-dc7b79572acb
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.51b8b657-5e5b-42ad-8989-3b572e52690c
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.2fb8d7e0-436f-4081-a867-5c9edb912dd0
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.5cee64ea-0540-4bf8-afa9-6e24792eb0ab
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.43174e24-f0d9-4dfd-925c-2689f1f84b4c
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.0ec4aa86-2fa3-47b5-9ef3-1942664471f6
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.32ae2cba-5b58-4ddb-97c5-7dc2dddac3a0
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.d811035f-5394-4113-ad9f-11054b97d9ee
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.0d83313a-7717-4f45-95b1-12e09d49ad90
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.9d1d943e-eec5-4b4a-a24b-dacb40ef30c1
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.31310f44-decb-4e98-9ec9-8ad63de9adfc
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.5a692a21-d9cb-481d-9e10-f3b6099619eb
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.f29294fa-d5da-4931-809e-4eeaf97eb31c
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.b582dfa7-fe04-48f8-a142-040a635d8409
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.6bc036ac-a3a9-4cc8-b875-6e99638b9c3b
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.532fa035-012d-4002-9427-3704d9a226dd
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.b7837298-2b32-4523-a6f7-89b7ccb9c23b
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.9ddd0afc-f54f-470a-8849-105b0c17ae6c
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.86d8d61f-9624-4db9-af64-74dd07c68a7c
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.621c32d2-98ad-4f96-9270-b9220af2a8d3
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.4c6f63c3-6bfc-48f3-90a1-5e69fa17830f
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.207ec54f-58d7-4b39-b28e-1541fc7161c5
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.0eeda004-c236-44f6-bbf1-3d2d64e38707
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.1658c3a2-8a8e-4176-b661-c3e41212b225
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.8fa720d7-e1b2-4211-b811-97873f0267a6
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.73fa0887-934a-4434-923d-99b8fce51087
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.74293272-1bd8-40eb-bb33-1aa7557b143d
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.fa26e2aa-09f3-4718-91e4-b6a918d67b3e
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.8ea34329-adf7-443f-b43a-c1dd84a9e6ff
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.9fe21cff-95cc-4fe8-ae71-039038a0fd6d
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.7e4789c9-5494-4070-9172-798e87e6f93a
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.48301cb0-5b3a-4295-97af-c2237cd277e8
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.351d8acb-41e4-4706-808c-99501924640d
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.82e56685-baf0-452a-b4ef-973a994672a6
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.c580e287-54d7-471c-99b6-58ad35ff58db
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.edad8791-40b6-490e-af1e-56d2b30f411b
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.b077096c-233b-4391-b45b-c4cb2f845f04
CapacityLimitExceededThis account has reached its capacity limit. Reach out to your account team for assistance.6a4ecbf7-5907-4147-8c4b-ea5b29f0c5e6🧪 To try this PR locally: bunx bun-pr 47376That installs a local version of the PR into your bun-47376 --bun |
Collaborator
Author
StatusHow the problem reproduces, on main (da867c6) and on Bun 1.4.3: import zlib from "node:zlib";
const z = zlib.createZstdCompress(), out = [];
z.on("error", e => console.log("error", e.code));
z.on("data", d => out.push(d));
await new Promise(r => z.write("hello", r));
await new Promise(r => z.flush(r));
z.reset();
if (!z.destroyed) {
z.end("world");
await new Promise(r => z.on("end", r));
console.log(zlib.zstdDecompressSync(Buffer.concat(out)).toString());
}
The fix is this pull request, #47376. It is a draft on top of #47079. The gzip half is #47364. CI: no failure comes from this change. The two builds of this tree (135244, 135361) are red for two reasons:
In build 135244 every build job passes, and the full test suite passes on Alpine x64, Alpine aarch64, Debian x64 with ASAN, Windows x64 and Windows aarch64. The second commit is empty and only started the second build. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft, on top of #47079: the zstd half of #47364. @dylan-conway: take this commit into #47079, or I retarget it to main after #47079 lands.
Problem
reset()on a ZstdCompress stream that has given part of a frame returns with no error, and the output fails to decode (ZSTD_error_corruption_detected). No user reported it.ERR_ZLIB_INCOMPLETE_FRAMEand destroy the stream (zlib: reject reset while a zstd frame is incomplete nodejs/node#66088).Context::reset(src/runtime/node/zlib/NativeZstd.rs:447on Replace the zstd C library with a port to safe Rust #47079) makes a new context with no check.Fix
Contextrecords that the open frame has given output.Context::resetreturns Node's error before it makes a new context. The existingonerrorpath destroys the stream.test/js/node/zlib/zlib-zstd-reset-incomplete-frame.test.ts(main fails 8 of 14, Node.js v26.12.1 passes all) and Node'stest-zlib-zstd-reset-incomplete-frame.js, unmodified. Also 8test-zlib-zstd*.jsfiles.Background
Contextis the codec state behind the native handle (_handle) of one stream.do_workruns once per native write.Downsides
reset()per segment on one ZstdCompress stream now destroys the stream. With no'error'listener the process exits.do_work. Not measured on the port. On the C library: 29 → 34 or 35 instructions.reset()still drops the parameters and the dictionary (node:zlib: reset a zstd stream in place, keep its dictionary and parameters #44261). A laterreset()can then differ from Node.js (see Notes).Notes
What each runtime does
hellois written first, andworldis written withend()if the stream is still open. "main" is da867c6.Context::resetof #47079 has no check either.createZstdCompress()reset()worldworldworldcreateZstdCompress()reset()ZSTD_error_corruption_detected'error'ERR_ZLIB_INCOMPLETE_FRAME, destroyedcreateZstdCompress()reset()ZSTD_error_corruption_detectedERR_ZLIB_INCOMPLETE_FRAMEcreateZstdCompress()ZSTD_e_end),reset()helloworldhelloworldhelloworld_handlewriteSync(flush),reset()onerror(message, 60, "ERR_ZLIB_INCOMPLETE_FRAME")createZstdDecompress()reset(), all of the inputThe error
Error, codeERR_ZLIB_INCOMPLETE_FRAME, errno 60 (ZSTD_error_stage_wrong), message "Cannot reset a zstd stream with an incomplete frame; end the frame or discard the output produced so far".onerror, as Node.js does. The code is not in Node'slib/internal/errors.js, so there is noErrorCode.tsentry.reset()does not change the codec. The frame can still be finished, and it decodes.Source:
ZstdCompressContext::ResetStream.One flag where Node.js has two
Node.js keeps
frame_complete_andframe_output_emitted_, and refuses if!frame_complete_ && frame_output_emitted_. The code setsframe_complete_only where it clearsframe_output_emitted_. So the two are never both true, and the test is the same asframe_output_emitted_.The flag is updated one time after the compress loop of
do_work, from the position of the output buffer.Tests
test/js/node/zlib/zlib-zstd-reset-incomplete-frame.test.tsusesnode:testandnode:assert. The same file passes under Node.js v26.12.1 (node --test, 14 pass). A debug build of this branch: 14 pass. A release build of main (da867c6): 8 fail, 6 pass.test-zlib-zstd-reset-incomplete-frame.jsintest/js/node/test/parallel/is the file of Node.js v26.12.1, byte for byte. main fails 2 of 5. This branch passes 5.detect_leaks=1,BUN_DESTRUCT_VM_ON_EXIT=1,BUN_JSC_validateExceptionChecks=1).test-zlib*.js, 6test-stream-iter-transform*.jsand 3test-webstreams-*compression*.jsvendored files,zlib-handle-bounds-check.test.ts(41 pass, with its zstd workers case),zlib-reset-race.test.ts,zlib-onerror-reentrancy.test.ts,zlib-estimated-size-gc.test.ts,zlib.test.js(473 pass),test/js/bun/util/zstd.test.ts(253 pass) andtest/js/web/streams/compression.test.ts(74 pass).do_workwent from 29 to 34 or 35 instructions per write). I repeat them when this PR targets main.After an accepted
reset()Context::resetstill makes a new context, as Node.js did before v26.10.0. So an acceptedreset()drops the parameters and the dictionary. #44261 changes that. Until then, the check can give a different answer from Node.js for the frame after an acceptedreset(). A probe of 7 handle sequences differs in 3:reset()ZSTD_c_checksumFlagZSTD_e_endinto a buffer 4 bytes short of the frame,reset()ZSTD_c_windowLog10reset()ZSTD_e_endinto a buffer of the size of the first frame,reset()The two control rows with no
reset()before them agree with Node.js.Rows that still differ from Node.js
_handle.writeSync(flush),_handle.close(),_handle.reset(). Node.js callsonerrorwith errno 60, because its flags outliveClose(). Bun returns for each closed handle (node_zlib_binding.rs). The stream API cannot reach this state.reset()of a destroyed stream. node:zlib: refuse reset() of a gzip member that is open and has given output #47364 changes it toERR_INTERNAL_ASSERTION, as in Node.js. This PR does not.[human-review] gate passed · iteration 1 · 3 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 1 passed · 0 rejected · iteration 1
evidence per changed file