-
Notifications
You must be signed in to change notification settings - Fork 5k
node:http: convert write()/end() arguments before reading response state #39386
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Jarred-Sumner
wants to merge
4
commits into
main
Choose a base branch
from
claude/ledger-12373-http-res-write-encoding-reentrancy
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+99
−67
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
938c181
NodeHTTPResponse: re-check response state after converting write()/en…
Jarred-Sumner c907de2
NodeHTTPResponse: convert write()/end() arguments before reading resp…
Jarred-Sumner abb85e6
NodeHTTPResponse: convert writeHead/writeInformational args before re…
Jarred-Sumner df8ca56
write_or_end: a destroyed socket returns false before the write-after…
Jarred-Sumner File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 The
it.eachonly passes a hostile encoding tores.write()/res.end()— both route intowrite_or_end. Neither row sets a hostileres.statusMessage(sowrite_head_impl'sto_bun_stringat :948 stays on theis_undefined()short-circuit) and neither reacheswrite_informational, so reverting either of those two reorderings would not break any test in this PR (REVIEW.md: "Confirm deleting each load-bearing clause of your fix breaks at least one test"). Consider a third row, e.g.["writeHead via statusMessage", 'res.statusMessage = enc; result = res.write("x");', "threw ERR_STREAM_ALREADY_FINISHED"], to pin thewrite_head_implfix.Extended reasoning...
What the gap is
This PR now applies the same "convert arguments before reading response state" reordering to three functions in
NodeHTTPResponse.rs:write_or_end<IS_END>— the original crash fix.write_head_impl— state checks (is_requested_completed_or_ended(),SOCKET_CLOSED,raw_response,handle_ended_if_necessary) moved from :912 down to :959–978, afterstatus_message_value.to_bun_string(global_object)?at :948.write_informational—is_done()/raw_response/handle_ended_if_necessarymoved to :1251–1259, afterEncoding::from_js/from_js_with_encoding_into.The last two were added in response to the earlier "fix the whole class in the same PR" review comment. But the new
it.eachatnode-http.test.ts:2284-2325still only exercises the first.Step-by-step: why neither row reaches the sibling fixes
["write", …]row:res.write(payload, enc)enters the JSwritepath →handle.cork(() => { handle.writeHead(this.statusCode, this[kSnapshotStatusMessage] ?? this.statusMessage, headers); handle.write(chunk, enc, …) }). The fixture never assignsres.statusMessage, so it isundefinedandkSnapshotStatusMessageis unset (only set inside an explicitwriteHead()). Nativewrite_head_implreceivesstatus_message_value = undefined, and at :946!status_message_value.is_undefined()is false —to_bun_string()at :948 never runs, so the moved re-check at :959–978 is never the thing that observes the destroyed state. The hostileenconly fires later insidehandle.write→write_or_end<false>, which is the covered path.["end", …]row:res.flushHeaders()callswriteHeadwith the samestatusMessage = undefined(benign), thenres.end(payload, enc)seesheadersSent === trueand goes straight towrite_or_end<true>without re-enteringwrite_head_impl. Again onlywrite_or_endis exercised.Neither row calls anything that reaches
write_informational(that is only entered viares._writeRawfor 1xx responses).Why REVIEW.md flags this
REVIEW.md, Tests reviewers reject: "Every behavioral change ships an automated test in the same PR" and "Confirm deleting each load-bearing clause of your fix breaks at least one test — a test that passes both ways is worse than no test." The reordering in
write_head_implandwrite_informationalis a behavioral change (it turns a use-of-stale-raw_responseinto a clean early-return/throw), but reverting either block to its pre-PR position would leave every test in this PR green.This is not a duplicate of the existing timeline comments: the earlier sibling-site comment asked for the fix (now applied), and the earlier "cover
end()" comment asked for theIS_END = truearm (now the second row). This is the remaining gap: tests for the two applied sibling fixes.Suggested fix
The fixture already builds a hostile
enc = Object.assign(new String("hex"), { [Symbol.toPrimitive]() { res.destroy(); Bun.gc(true); return "hex"; } }), so a third row can reuse it as the status message:Trace:
res.statusMessage = enc(plain data property, no validating setter in http1) →res.write("x")→handle.cork→handle.writeHead(200, enc, …)→write_head_impl:946!is_undefined()is true → :948to_bun_stringinvokesSymbol.toPrimitive→res.destroy()→abort()setsSOCKET_CLOSEDand, viaon_request_complete(),REQUEST_HAS_COMPLETED→ back at the moved :959is_requested_completed_or_ended()is true → throwsERR_STREAM_ALREADY_FINISHED→ propagates out ofcork→ fixture'scatchprintsthrew ERR_STREAM_ALREADY_FINISHED. (Confirm the exact expected string withbun bd test; if theSOCKET_CLOSEDbranch at :972 wins instead, the expected becomes"returned boolean"— either way the row pins the reordering.)Covering
write_informationalfrom public API is harder (it's reached via internal_writeRawfor 1xx); if the author considers it not publicly reachable with a hostile object, saying so in the PR description satisfies REVIEW.md's "if a site is intentionally excluded, say so in the PR."Severity
Nit. The primary crash (
write_or_end) is properly tested for bothwrite()andend(), and the two sibling reorderings are structurally identical moves of the same guard block — the risk of one being wrong whilewrite_or_endis right is low. The PR is strictly safer than before; this only tightens the mutation-testing bar the repo's own review guide sets.