From 2058984c1f4bc333417a94320bf822e5ece76889 Mon Sep 17 00:00:00 2001 From: Ralph Date: Sun, 14 Jun 2026 23:00:18 -0700 Subject: [PATCH] fix(http): guard Buffer/TypedArray in dynamic add ToPrimitive (#5131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A `node:http` server that consumes the request body via `req.on("data", c => body += c)` on a POST carrying a body segfaulted (SIGSEGV / exit 139). The un-typed `data` chunk is a `Buffer`, so `body += c` lowers to the fully-dynamic add helper `js_dynamic_string_or_number_add` → `to_primitive_default_for_add`. That function had no Buffer/TypedArray guard, so it fell through to the `js_url_href_if_url` / `try_read_as_search_params` / `OrdinaryToPrimitive` probes, all of which bit-cast the operand pointer to an `ObjectHeader` and read its fields. A `BufferHeader` carries no `ObjectHeader`/`GcHeader`, so the probe dereferenced a fake header one word before the data and crashed. (The same concat with a statically-typed `const c = Buffer.from(...)` worked: codegen proves `c` non-string and uses the safe `js_string_concat_value` coerce path, which routes through `js_jsvalue_to_string`'s existing buffer guard.) Fix: detect Buffers/TypedArrays via their registries (by-value lookups, no deref) in `to_primitive_default_for_add` before the ObjectHeader probes and route them to `js_jsvalue_to_string` — yielding the same string form as an explicit `.toString()` (Buffer→utf8, TypedArray→`join(",")`) and matching the guards `js_jsvalue_to_string` itself runs. This also makes dynamic `+` consistent with `String()` and template-literal coercion. Adds regression test `test_issue_5131_http_body_concat` (expected `len:13`). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../perry-runtime/src/value/dynamic_arith.rs | 20 ++++++++ .../test_issue_5131_http_body_concat.ts | 47 +++++++++++++++++++ .../test_issue_5131_http_body_concat.txt | 1 + 3 files changed, 68 insertions(+) create mode 100644 test-files/test_issue_5131_http_body_concat.ts create mode 100644 test-parity/expected/test_issue_5131_http_body_concat.txt diff --git a/crates/perry-runtime/src/value/dynamic_arith.rs b/crates/perry-runtime/src/value/dynamic_arith.rs index 6f33df7d9d..511ec6924f 100644 --- a/crates/perry-runtime/src/value/dynamic_arith.rs +++ b/crates/perry-runtime/src/value/dynamic_arith.rs @@ -89,6 +89,26 @@ unsafe fn to_primitive_default_for_add(value: f64) -> f64 { return crate::value::js_nanbox_string(s as i64); } + // Buffers / TypedArrays carry NO `ObjectHeader` (a `BufferHeader` / + // `TypedArrayHeader` has a different, smaller layout). The + // `js_url_href_if_url` / `try_read_as_search_params` / + // `ordinary_to_primitive_number_for_add` probes below all bit-cast `ptr` + // to an `ObjectHeader` and read its fields, so a Buffer/TypedArray operand + // would deref a fake header one word before the data and segfault + // (issue #5131 — `req.on('data', c => body += c)` on a `node:http` server, + // where the chunk is an un-typed Buffer and `body += c` lowers to the + // fully-dynamic add path). Detect via the registries (by-value lookups, no + // deref) and route to `js_jsvalue_to_string`, which yields the same string + // form as an explicit `.toString()` (Buffer→utf8, TypedArray→`join(",")`). + // This matches the guards `js_jsvalue_to_string` itself runs before its + // ordinary-object dispatch. + if crate::buffer::is_registered_buffer(ptr) + || crate::typedarray::lookup_typed_array_kind(ptr).is_some() + { + let s = crate::value::js_jsvalue_to_string(value); + return crate::value::js_nanbox_string(s as i64); + } + let primitive = crate::symbol::js_to_primitive(value, 0); if primitive.to_bits() != value.to_bits() { if is_nonprimitive_object_value(primitive) { diff --git a/test-files/test_issue_5131_http_body_concat.ts b/test-files/test_issue_5131_http_body_concat.ts new file mode 100644 index 0000000000..057f5a76d7 --- /dev/null +++ b/test-files/test_issue_5131_http_body_concat.ts @@ -0,0 +1,47 @@ +// Issue #5131 — a `node:http` server that consumes the request body via +// `req.on("data", c => body += c)` on a POST that actually carries a body +// segfaulted (SIGSEGV / exit 139). +// +// Root cause: the un-typed `data` chunk is a `Buffer`, so `body += c` lowers +// to the fully-dynamic add helper `js_dynamic_string_or_number_add` → +// `to_primitive_default_for_add` (crates/perry-runtime/src/value/dynamic_arith.rs). +// That function lacked a Buffer/TypedArray guard, so it fell through to the +// `js_url_href_if_url` / `try_read_as_search_params` / `OrdinaryToPrimitive` +// probes, all of which bit-cast the operand pointer to an `ObjectHeader` and +// read its fields. A `BufferHeader` carries NO `ObjectHeader`/`GcHeader`, so +// the probe dereferenced a fake header one word before the data → crash. +// +// (The same pattern with a statically-typed `const c = Buffer.from(...)` +// worked because codegen proved `c` non-string and used the safe +// `js_string_concat_value` coerce path, which routes through +// `js_jsvalue_to_string`'s existing buffer guard.) +// +// Fix: detect Buffers/TypedArrays via their registries (by-value lookups, no +// deref) in `to_primitive_default_for_add` before the ObjectHeader probes and +// route them to `js_jsvalue_to_string`, mirroring the guard `js_jsvalue_to_string` +// itself runs. Expected output: `len:13`. + +import http from "node:http"; + +const server = http.createServer((req, res) => { + let body = ""; + req.on("data", (c) => (body += c)); + req.on("end", () => { + res.writeHead(200); + res.end("len:" + body.length); + }); +}); + +server.listen(0, () => { + const port = (server.address() as { port: number }).port; + const r = http.request({ port, method: "POST" }, (res) => { + let d = ""; + res.on("data", (c) => (d += c)); + res.on("end", () => { + console.log(d); + server.close(); + }); + }); + r.write("payload-bytes"); + r.end(); +}); diff --git a/test-parity/expected/test_issue_5131_http_body_concat.txt b/test-parity/expected/test_issue_5131_http_body_concat.txt new file mode 100644 index 0000000000..afcde97106 --- /dev/null +++ b/test-parity/expected/test_issue_5131_http_body_concat.txt @@ -0,0 +1 @@ +len:13