-
Notifications
You must be signed in to change notification settings - Fork 5k
node:http/http2 hardening: enforce h2 request pseudo-headers, never report a failed handler as success, and bound/validate the parser paths #33191
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
base: main
Are you sure you want to change the base?
Changes from all commits
aaaa057
a8d3064
8d1e6a3
e22f9d6
aafedba
9ebcfa4
92050fb
8ce10c8
18c1c46
bb4a012
3946ff4
0e6f8b1
738780e
73cf3b6
c0ce5fe
cd4aa05
dd5400e
4f0e779
e3090fd
e63fd76
b429076
62d8523
3946946
7aae0b8
5ab67eb
aaaef71
a370101
6961bb8
27ac2f9
5e4ac63
8b4066c
eabefc4
02968cf
c2a68b3
cd563f1
20c07b3
d0ed19d
d43eb06
13aaf3c
8bf7e03
178c628
cb4f2dd
e9a8337
ab86b7d
9129c09
b9b1ae1
05988e6
93063de
9c480b5
9d760d2
3763778
9ab92d4
3898852
5e249f0
c22eec8
94255ac
1355715
b7b7572
f4542e7
a390358
6944eca
4a8adab
4a3cf89
4d7f32f
c1d3957
d7f2781
8e7d70c
b505f79
c376074
99eac9e
61f0b45
30392cc
9c8dd2c
f2d6f54
15b1db2
b0433f5
ebfa237
e12dfb7
d2f54aa
bb67830
dc7d640
c14281e
b8e66e2
bd8fc38
4990051
d93f3d9
2194f30
67f943d
ac6d1ae
252aebb
a47f450
23965a8
d66bf87
46a59e0
8fffce0
7095981
302a333
7a2fe94
20bbfb0
269c81a
42ba97c
d49cff1
fdd1762
5fb4a01
0274b1c
d1dbe3e
74b1081
cceac70
c2ce18d
06e9db8
3680876
bc0b464
d5ea586
0bd6587
9a53587
031ab0b
072d922
79d630d
dd56a89
9dbfb99
deb66a0
4d8585e
a61111d
45480cf
bd10ee6
08e091b
116577c
4bee8a2
adaa711
8ef31c8
d7bfa55
9c17084
0d1e01e
2d4a23f
b4fa757
8061d7f
07be335
f636e4a
5cecac8
e67ff52
f61faf4
246c1dc
3807122
c59f9e9
8d03151
4b97a68
76c1a4b
2663f0f
929ac1e
55062db
affdc52
f74f866
326d048
d5d0254
29d1fe6
4768dd3
b4b9de5
8763fd9
507986e
5a312c3
af4d380
8a658d7
916afff
7f1b0f2
49e01a3
8905777
3db5527
cb1b961
173ca07
2a9a332
b7e7338
671f12e
c1f6b65
349b56b
662ee8c
759e4b2
0f4370e
f28abe6
12d4e41
209e15c
6300e69
be17e34
d6a6e43
deb14ba
a4a3e07
4f94249
b4b48f3
bee36cb
a5ba437
776cb49
8939a7e
4e96ac2
312e071
69ac4fd
223aca6
c51f8a4
a0e2dfb
30dd839
eb693c9
947cef1
00eaa58
60bce3b
79d4801
eb7b498
93867b2
62a2fec
2256eed
fbe7dd9
1057479
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1011,7 +1011,8 @@ impl Connection { | |||||||||||||||||||||||||
| let cap = (self.enforced_max_header_list_size as usize).max(65536); | ||||||||||||||||||||||||||
| if self.header_block.len().saturating_add(payload.len()) > cap { | ||||||||||||||||||||||||||
| // nghttp2's NGHTTP2_MAX_HEADERSLEN (65536) overflow returns NGHTTP2_ERR_HEADER_COMP, | ||||||||||||||||||||||||||
| // which node surfaces as a session COMPRESSION_ERROR. | ||||||||||||||||||||||||||
| // which node surfaces as a session COMPRESSION_ERROR | ||||||||||||||||||||||||||
| // (test-http2-options-max-headers-exceeds-nghttp2.js). | ||||||||||||||||||||||||||
| self.send_go_away(sink, ErrorCode::CompressionError, b"header block too large"); | ||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
@@ -1066,6 +1067,11 @@ impl Connection { | |||||||||||||||||||||||||
| let mut saw_connect = false; | ||||||||||||||||||||||||||
| let mut saw_host = false; | ||||||||||||||||||||||||||
| let mut informational = false; | ||||||||||||||||||||||||||
| // nghttp2 check_path() flags for the RFC 9113 §8.3.1 :path validation. | ||||||||||||||||||||||||||
| let mut path_regular = false; | ||||||||||||||||||||||||||
| let mut path_asterisk = false; | ||||||||||||||||||||||||||
| let mut scheme_http = false; | ||||||||||||||||||||||||||
| let mut meth_options = false; | ||||||||||||||||||||||||||
| let mut content_length: Option<u64> = None; | ||||||||||||||||||||||||||
| while off < block.len() { | ||||||||||||||||||||||||||
| match self.hpack.decode(&block[off..]) { | ||||||||||||||||||||||||||
|
|
@@ -1106,11 +1112,14 @@ impl Connection { | |||||||||||||||||||||||||
| b"protocol" => pseudo::PROTOCOL, | ||||||||||||||||||||||||||
| _ => pseudo::UNKNOWN, | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| // 8.3.1: requests never carry :status - a server seeing it inbound is | ||||||||||||||||||||||||||
| // a malformed block. (The client direction also constrains pseudo | ||||||||||||||||||||||||||
| // headers, but inbound PUSH_PROMISE blocks legitimately carry request | ||||||||||||||||||||||||||
| // pseudo-headers, so that check needs the push context first.) | ||||||||||||||||||||||||||
| let wrong_direction = self.is_server && rest == b"status"; | ||||||||||||||||||||||||||
| // RFC 9113 §8.3.1/§8.3.2: :status only in response blocks, request | ||||||||||||||||||||||||||
| // pseudo-headers only in request blocks. Key on `is_request` (not | ||||||||||||||||||||||||||
| // is_server) — a client-received PUSH_PROMISE is a request block. | ||||||||||||||||||||||||||
| let wrong_direction = if is_request { | ||||||||||||||||||||||||||
| bit == pseudo::STATUS | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| bit != pseudo::STATUS && bit != pseudo::UNKNOWN | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| // RFC 8441 §4: :protocol is only valid when SETTINGS_ENABLE_CONNECT_PROTOCOL | ||||||||||||||||||||||||||
| // has been enabled by this endpoint. nghttp2 (and so node) checks the | ||||||||||||||||||||||||||
| // submitted local value here, not the ACKed one — so a request that arrives | ||||||||||||||||||||||||||
|
|
@@ -1120,9 +1129,9 @@ impl Connection { | |||||||||||||||||||||||||
| let protocol_disabled = self.is_server | ||||||||||||||||||||||||||
| && rest == b"protocol" | ||||||||||||||||||||||||||
| && self.local_settings.enable_connect_protocol == 0; | ||||||||||||||||||||||||||
| // nghttp2 (check_pseudo_header) treats an empty pseudo-header value as | ||||||||||||||||||||||||||
| // malformed, so `:path: ""` never counts as a present :path (§8.3.1: | ||||||||||||||||||||||||||
| // `:path` "MUST NOT be empty" for http/https). | ||||||||||||||||||||||||||
| // RFC 9113 §8.1: pseudo-headers never appear in a trailer section. | ||||||||||||||||||||||||||
| // nghttp2 (check_pseudo_header) also treats an empty pseudo-header | ||||||||||||||||||||||||||
| // value as malformed, so `:path: ""` never counts as a present :path. | ||||||||||||||||||||||||||
| if seen_regular | ||||||||||||||||||||||||||
| || bit == pseudo::UNKNOWN | ||||||||||||||||||||||||||
| || (seen_pseudo & bit) != 0 | ||||||||||||||||||||||||||
|
|
@@ -1137,15 +1146,43 @@ impl Connection { | |||||||||||||||||||||||||
| informational = true; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| seen_pseudo |= bit; | ||||||||||||||||||||||||||
| if rest == b"method" && value_b == b"CONNECT" { | ||||||||||||||||||||||||||
| saw_connect = true; | ||||||||||||||||||||||||||
| // nghttp2 http_request_on_header: per-field flags for check_path()/ | ||||||||||||||||||||||||||
| // nghttp2_http_on_request_headers below; CONNECT on a pushed (even) | ||||||||||||||||||||||||||
| // stream is rejected up front ("we won't allow CONNECT for push"). | ||||||||||||||||||||||||||
| match rest { | ||||||||||||||||||||||||||
| b"method" => { | ||||||||||||||||||||||||||
| if value_b == b"CONNECT" { | ||||||||||||||||||||||||||
| if push_parent != 0 { | ||||||||||||||||||||||||||
| malformed = true; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| saw_connect = true; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| meth_options |= value_b == b"OPTIONS"; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| b"path" => { | ||||||||||||||||||||||||||
| path_regular |= value_b.first() == Some(&b'/'); | ||||||||||||||||||||||||||
| path_asterisk |= value_b == b"*"; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| b"scheme" => { | ||||||||||||||||||||||||||
| scheme_http |= value_b.eq_ignore_ascii_case(b"http") | ||||||||||||||||||||||||||
| || value_b.eq_ignore_ascii_case(b"https"); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| _ => {} | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| seen_regular = true; | ||||||||||||||||||||||||||
| match name_b { | ||||||||||||||||||||||||||
| b"connection" | b"keep-alive" | b"proxy-connection" | ||||||||||||||||||||||||||
| | b"transfer-encoding" | b"upgrade" => malformed = true, | ||||||||||||||||||||||||||
| b"host" if is_request => saw_host = true, | ||||||||||||||||||||||||||
| // nghttp2 http_request_on_header: in request blocks Host is checked | ||||||||||||||||||||||||||
| // like :authority (empty/repeated => malformed); in a response it | ||||||||||||||||||||||||||
| // is an ordinary field and node delivers it. | ||||||||||||||||||||||||||
| b"host" if self.is_server || is_request => { | ||||||||||||||||||||||||||
| if value_b.is_empty() || saw_host { | ||||||||||||||||||||||||||
| malformed = true; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| saw_host = true; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+1180
to
+1185
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Restrict the The guard is nghttp2 routes a trailer block through The comment above the arm states the intent as "in request blocks Host is checked like 🐛 Proposed fix to scope the check to request blocks- b"host" if self.is_server || is_request => {
+ b"host" if is_request => {
if value_b.is_empty() || saw_host {
malformed = true;
}
saw_host = true;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| b"te" => { | ||||||||||||||||||||||||||
| // RFC 9110 10.1.4: field values are case-insensitive. | ||||||||||||||||||||||||||
| if !value_b.eq_ignore_ascii_case(b"trailers") { | ||||||||||||||||||||||||||
|
|
@@ -1199,11 +1236,9 @@ impl Connection { | |||||||||||||||||||||||||
| sink.on_stream_reset(target, ErrorCode::StreamClosed.as_u32()); | ||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| // RFC 9113 §8.3.1 (nghttp2_http_on_request_headers): a request block needs exactly one | ||||||||||||||||||||||||||
| // non-empty :method, :scheme and :path plus an :authority or Host; plain CONNECT omits | ||||||||||||||||||||||||||
| // :scheme/:path and carries :authority; extended CONNECT (:protocol, RFC 8441) requires | ||||||||||||||||||||||||||
| // :method CONNECT and :authority. Without this a block with an empty or missing :path | ||||||||||||||||||||||||||
| // reaches JS as a request with an empty url (no compliant peer can produce that shape). | ||||||||||||||||||||||||||
| // RFC 9113 §8.3.1 / nghttp2_http_on_request_headers: request block = :method+:scheme | ||||||||||||||||||||||||||
| // +:path + (:authority|Host); plain CONNECT omits :scheme/:path with :authority; | ||||||||||||||||||||||||||
| // extended CONNECT (RFC 8441) needs :method CONNECT. Applies to HEADERS & PUSH_PROMISE. | ||||||||||||||||||||||||||
| if is_request && !rejected && !malformed { | ||||||||||||||||||||||||||
| use pseudo::{AUTHORITY, METHOD, PATH, PROTOCOL, SCHEME}; | ||||||||||||||||||||||||||
| let extended_connect = (seen_pseudo & PROTOCOL) != 0; | ||||||||||||||||||||||||||
|
|
@@ -1213,8 +1248,19 @@ impl Connection { | |||||||||||||||||||||||||
| (seen_pseudo & (METHOD | SCHEME | PATH)) != (METHOD | SCHEME | PATH) | ||||||||||||||||||||||||||
| || ((seen_pseudo & AUTHORITY) == 0 && !saw_host) | ||||||||||||||||||||||||||
|
alii marked this conversation as resolved.
|
||||||||||||||||||||||||||
| || (extended_connect && (!saw_connect || (seen_pseudo & AUTHORITY) == 0)) | ||||||||||||||||||||||||||
| // nghttp2 check_path(): under http/https, :path must start with '/' | ||||||||||||||||||||||||||
| // (or be '*' for OPTIONS). | ||||||||||||||||||||||||||
| || (scheme_http && !(path_regular || (meth_options && path_asterisk))) | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } else if !is_trailer && !rejected && !malformed && !informational { | ||||||||||||||||||||||||||
| // RFC 9113 §8.3.2 (nghttp2_http_on_response_headers): a final response block must | ||||||||||||||||||||||||||
| // carry exactly :status and no request pseudo-header. wrong_direction above already | ||||||||||||||||||||||||||
| // rejected a request pseudo per-field; this catches a block with :status omitted. | ||||||||||||||||||||||||||
| malformed = (seen_pseudo & pseudo::STATUS) == 0; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| // RFC 9113 §8.1.1: an inbound request's content-length must be coherent — the declared | ||||||||||||||||||||||||||
| // value is attached to the stream and, at END_STREAM, must equal the DATA received | ||||||||||||||||||||||||||
| // (plain CONNECT is exempt). | ||||||||||||||||||||||||||
| if push_parent == 0 && self.is_server && !malformed && !rejected { | ||||||||||||||||||||||||||
| if let Some(s) = self.streams.get_mut(&target) { | ||||||||||||||||||||||||||
| if !saw_connect && s.content_length.is_none() { | ||||||||||||||||||||||||||
|
|
@@ -1229,9 +1275,9 @@ impl Connection { | |||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| if malformed && !rejected { | ||||||||||||||||||||||||||
| // node (Http2Session::OnInvalidFrame): every locally-rejected invalid frame counts | ||||||||||||||||||||||||||
| // against maxSessionInvalidFrames; exceeding it tears the session down with | ||||||||||||||||||||||||||
| // ERR_HTTP2_TOO_MANY_INVALID_FRAMES (same post-increment comparison as node). | ||||||||||||||||||||||||||
| // nghttp2 session_handle_invalid_stream2 / RFC 9113 §8.4.1: malformed HEADERS or | ||||||||||||||||||||||||||
| // PUSH_PROMISE → RST_STREAM(PROTOCOL_ERROR) on the target id + invalid-frame count. | ||||||||||||||||||||||||||
| // node Http2Session::OnInvalidFrame tears down on maxSessionInvalidFrames overflow. | ||||||||||||||||||||||||||
| let count = self.invalid_frame_count; | ||||||||||||||||||||||||||
| self.invalid_frame_count = count.saturating_add(1); | ||||||||||||||||||||||||||
| if count > self.max_invalid_frames { | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5139,6 +5139,8 @@ impl H2FrameParser { | |
| if global.has_exception() { | ||
| return Some(stream); | ||
| } | ||
| // The `*mut Stream` above stays live across this call into JS. | ||
| let _dispatch = self.enter_dispatch(); | ||
| match callback.call( | ||
| &global, | ||
| ctx_value, | ||
|
|
@@ -5498,6 +5500,34 @@ impl H2FrameParser { | |
| }); | ||
| } | ||
|
|
||
| /// Free streams whose legacy lifecycle finished (queued by `free_resources`). Only runs | ||
| /// at a quiescent point — no JS dispatch on the stack and no in-progress receive() | ||
| /// borrowing the engine cell; otherwise ids stay queued for the next such point. | ||
| fn drain_pending_engine_stream_closes(&self) { | ||
| if self.dispatch_depth.get() != 0 || self.pending_engine_stream_closes.get().is_empty() { | ||
| return; | ||
| } | ||
| let Ok(mut engine_guard) = self.engine.try_borrow_mut() else { | ||
| return; | ||
| }; | ||
| let Some(engine) = engine_guard.as_mut() else { | ||
| return; | ||
| }; | ||
| self.pending_engine_stream_closes.with_mut(|v| { | ||
| for id in v.drain(..) { | ||
| engine.close_stream(id); | ||
| if let Some(stream) = self.streams.with_mut(|m| m.remove(&id)) { | ||
| // SAFETY: sole owner just removed from the map; free_resources already ran; | ||
| // dispatch-depth gate above proves no native frame still borrows it; stream | ||
| // ids never repeat in a session → frees exactly once. | ||
| unsafe { | ||
| drop(bun_core::heap::take(stream)); | ||
| } | ||
| } | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| /// Feed inbound bytes through the rewrite engine, buffering the unconsumed tail (design B). | ||
| fn rewrite_read(&self, bytes: &[u8]) { | ||
| bun_output::scoped_log!(H2FrameParser, "rewriteRead {}", bytes.len()); | ||
|
|
@@ -5560,28 +5590,11 @@ impl H2FrameParser { | |
| engine.pending_local_settings_acks.push_back(w); | ||
| } | ||
| }); | ||
| // Streams whose legacy lifecycle finished since the last batch: evict the engine | ||
| // entry and free the legacy slot. free_resources already ran for these (it is the | ||
| // only producer of this queue); duplicate ids are fine — remove() yields None. | ||
| if self.dispatch_depth.get() == 0 { | ||
| self.pending_engine_stream_closes.with_mut(|v| { | ||
| for id in v.drain(..) { | ||
| engine.close_stream(id); | ||
| if let Some(stream) = self.streams.with_mut(|m| m.remove(&id)) { | ||
| // SAFETY: stream is the heap::alloc'd *mut Stream owned by the | ||
| // map entry just removed; free_resources ran when it was queued, | ||
| // dispatch_depth == 0 means no caller below us on the stack holds | ||
| // a `&mut Stream` across anything that can run user JS (every | ||
| // such site arms enter_dispatch), ids never repeat within a | ||
| // session, so this frees exactly once. | ||
| unsafe { | ||
| drop(bun_core::heap::take(stream)); | ||
| } | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| // Streams whose legacy lifecycle finished since the last batch: evict the engine entry | ||
| // and free the legacy slot at this quiescent point (the helper enforces the safety | ||
| // rules; deferred ids are also reclaimed at the next host-call boundary). | ||
| self.drain_pending_engine_stream_closes(); | ||
| if self.rewrite_tail.get().is_empty() { | ||
| let feed = { | ||
| let mut guard = self.engine.borrow_mut(); | ||
|
|
@@ -7045,6 +7058,9 @@ impl H2FrameParser { | |
| callframe: &CallFrame, | ||
| ) -> JsResult<JSValue> { | ||
| bun_output::scoped_log!(H2FrameParser, "rstStream"); | ||
| // Quiescent host-call boundary: reclaim deferred stream closes before this frame | ||
| // materializes any `*mut Stream`. | ||
| this.drain_pending_engine_stream_closes(); | ||
| let [stream_arg, error_arg] = callframe.arguments_as_array::<2>(); | ||
| if callframe.arguments_count() < 2 { | ||
| return Err(global_object.throw(format_args!("Expected stream and code arguments"))); | ||
|
|
@@ -7912,6 +7928,10 @@ impl H2FrameParser { | |
| defer_callback_arg, | ||
| ] = args.ptr; | ||
|
|
||
| // Quiescent host-call boundary: reclaim deferred stream closes before this frame | ||
| // materializes any `*mut Stream`. | ||
| this.drain_pending_engine_stream_closes(); | ||
|
|
||
| if !stream_arg.is_number() { | ||
| return Err(global_object.throw(format_args!("Expected stream to be a number"))); | ||
| } | ||
|
|
@@ -7956,9 +7976,18 @@ impl H2FrameParser { | |
| } | ||
| }; | ||
|
|
||
| let buffer = match StringOrBuffer::from_js_with_encoding(global_object, data_arg, encoding)? | ||
| { | ||
| Some(b) => b, | ||
| // send_data can re-enter JS mid-payload (batch flushes, prior writes' callbacks): pin | ||
| // + protect ArrayBuffer payloads so they can't be detached under the borrowed slice. | ||
| // Strings are immutable (zero-copy path); ThreadSafe's Drop releases the pin/protect. | ||
| let pin_payload = data_arg.is_cell() && data_arg.js_type().is_array_buffer_like(); | ||
| let buffer = match StringOrBuffer::from_js_with_encoding_maybe_async( | ||
| global_object, | ||
| data_arg, | ||
| encoding, | ||
| pin_payload, | ||
| true, | ||
| )? { | ||
| Some(b) => bun_jsc::ThreadSafe::adopt(b), | ||
|
Comment on lines
+7979
to
+7990
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Resolve StringOrBuffer::from_js_with_encoding_maybe_async and ThreadSafe::adopt signatures and Drop.
set -euo pipefail
echo "== from_js_with_encoding_maybe_async =="
rg -n -C 20 'fn from_js_with_encoding_maybe_async' src
echo "== ThreadSafe definition, adopt, Drop =="
rg -n -C 10 'pub struct ThreadSafe|impl .*ThreadSafe|fn adopt' src/jsc src/bun_jsc 2>/dev/null || rg -n -C 10 'ThreadSafe' src
echo "== other call sites for comparison =="
rg -n -C 3 'from_js_with_encoding_maybe_async\(' srcRepository: oven-sh/bun Length of output: 50368 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Definitions =="
rg -n -C 18 --glob '*.rs' 'from_js_with_encoding_maybe_async|pub struct ThreadSafe|impl<.*ThreadSafe|impl ThreadSafe|fn adopt|impl.*Unprotect|fn unprotect' src/runtime src | head -n 1200
echo "== Target call sites =="
rg -n -C 8 --glob '*.rs' 'from_js_with_encoding_maybe_async\(' src/runtime/api/bun/h2_frame_parser.rs src/runtime/node src/runtime/api/BunObject.rs
echo "== ThreadSafe::adopt call sites =="
rg -n -C 8 --glob '*.rs' 'ThreadSafe::adopt\(' src/runtime src | head -n 800Repository: oven-sh/bun Length of output: 50368 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Target implementation and call =="
sed -n '7750,8010p' src/runtime/api/bun/h2_frame_parser.rs | cat -n
sed -n '285,325p' src/runtime/node/types.rs | cat -n
sed -n '495,580p' src/runtime/node/types.rs | cat -n
echo "== ThreadSafe definition and impl =="
rg -n --glob '*.rs' --glob '*.zig' --glob '*.ziggy' \
'pub struct ThreadSafe|struct ThreadSafe|impl[^\\n]*ThreadSafe|pub fn adopt|fn adopt|trait Unprotect' . \
| rg 'ThreadSafe|Unprotect' | head -n 300
echo "== Relevant ThreadSafe call sites =="
rg -n -C 12 --glob '*.rs' 'bun_jsc::ThreadSafe::adopt\(' src/runtime/api/bun/h2_frame_parser.rs src/runtime/api/JSTranspiler.rs src/runtime/api/BunObject.rs src/runtime/crypto/PBKDF2.rs src/runtimeRepository: oven-sh/bun Length of output: 35862 🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '1,110p' src/jsc/node_path.rs | cat -n
rg -n -C 8 --glob '*.rs' 'ThreadSafe<' src/jsc src/runtime | head -n 500
rg -n -C 12 --glob '*.rs' 'impl.*Unprotect for StringOrBuffer|from_js_maybe_async_into' src/runtime/node/types.rsRepository: oven-sh/bun Length of output: 35568 Label both conversion flags The fourth argument is 🤖 Prompt for AI Agents |
||
| None => { | ||
| return Err(global_object.throw_invalid_argument_type_value( | ||
| b"write", | ||
|
|
@@ -8347,6 +8376,8 @@ impl H2FrameParser { | |
| }; | ||
| let mut _count: u32 = 0; | ||
| let mut it = StreamResumableIterator::init(this); | ||
| // The iterator's `*mut Stream`s stay live across the callbacks below. | ||
| let _dispatch = this.enter_dispatch(); | ||
| while let Some(stream) = it.next() { | ||
| // SAFETY: stream is *mut Stream from self.streams; valid while the map entry exists | ||
| let Some(value) = (unsafe { (*stream).js_context.get() }) else { | ||
|
|
||
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.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Use
normalizeRejectUnauthorizedas the only normalizer.Line 576 already converts every defined non-boolean value to
true. Therefore, Lines 611-614 do not normalizenull,0, or""in this function.Replace the earlier conversion with the shared helper. Then remove the later duplicate block.
Proposed refactor
As per coding guidelines, “Prefer the simplest honest shape” and use “named helpers for repeated blocks.”
Also applies to: 608-614
🤖 Prompt for AI Agents
Source: Coding guidelines