Skip to content

fix(fetch+runtime): generic object ops must not deref fetch-band Headers/Request handles#5606

Merged
proggeramlug merged 3 commits into
mainfrom
fix/fetch-headers-handle-stringify
Jun 24, 2026
Merged

fix(fetch+runtime): generic object ops must not deref fetch-band Headers/Request handles#5606
proggeramlug merged 3 commits into
mainfrom
fix/fetch-headers-handle-stringify

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Problem

A real HTTP client (axios's fetch adapter) passes a Headers/Request object — a perry fetch-band registry handle (a small id like 0x40007, not a heap pointer) — to generic object operations. Each one dereferences the id as a heap object → EXC_BAD_ACCESS (SIGSEGV), so fetch() crashes before reaching the network. Two sites, two commits:

1. js_json_stringify on a Headers value (43f212ec4)

The fetch thunk serializes init.headers to JSON for js_fetch_with_options. With headers: new Headers(...), the generic js_json_stringify walker reached the handle and deref'd it → crash in gc_obj_type. Fix: js_fetch_headers_to_json / js_headers_fetch_object_json read a Headers handle from its registry instead, and the fetch(url, { headers }) path is routed through it.

2. js_object_has_property on a handle (9cbb9a87f)

key in <handle> (the in operator) deref'd the fetch-band id as a heap object. key in <handle> has no own-property meaning for a registry handle, so return false for handle-band receivers (mirrors the existing Proxy guard in the same function).

Both are the same handle-band family as the string_from_header / inline-.length / js_get_iterator guards (#5559/#5560/#5549): fast/native/generic paths must reject handle-band ids before treating them as heap pointers.

Result

With these (plus the fetch(Request) dispatch fix), a real bundle's -p request path runs through startup → request dispatch → TLS handshake and opens an ESTABLISHED socket to the API host (...:443), instead of crashing.

Tests

cargo test -p perry-runtime --lib and -p perry-stdlib --lib pass; regression coverage added for the Headers-handle stringify path. fmt + check clean.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed fetch(url, { headers }) to reliably serialize Headers into the expected flat {name: value} JSON shape, avoiding failures caused by misinterpreting internal handle identifiers.
    • Prevented Object.prototype.toString from attempting to treat Web/registry handle-band values as heap objects.
  • Tests

    • Added unit tests covering safe stringification for handle-band identifiers.
    • Added tests validating Headers → JSON conversion, including correct handling of unknown/invalid handles.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0d5fc6d2-635e-49fd-bdf3-9996d74f8acd

📥 Commits

Reviewing files that changed from the base of the PR and between d4ad5ba and b1f1958.

📒 Files selected for processing (4)
  • crates/perry-runtime/src/object/field_get_set.rs
  • crates/perry-runtime/src/object/global_fetch.rs
  • crates/perry-stdlib/src/fetch/headers_json_test.rs
  • crates/perry-stdlib/src/fetch/mod.rs
✅ Files skipped from review due to trivial changes (1)
  • crates/perry-stdlib/src/fetch/headers_json_test.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/perry-runtime/src/object/field_get_set.rs
  • crates/perry-runtime/src/object/global_fetch.rs

📝 Walkthrough

Walkthrough

Introduces a handle-band-aware Headers→JSON conversion path for fetch(url, { headers }). A new js_headers_fetch_object_json stdlib function serializes Headers registry handles to flat JSON. A new GLOBAL_HEADERS_OBJECT_JSON runtime slot registers that producer; headers_init_json_ptr is rewritten to classify handle-band vs. heap values. Guards are added to js_object_to_string and js_object_has_property to prevent handle-band ids from being dereferenced as heap pointers. Codegen is updated to emit js_fetch_headers_to_json.

Changes

Headers Handle-Band JSON Conversion

Layer / File(s) Summary
Headers registry → flat JSON producer
crates/perry-stdlib/src/fetch/headers.rs, crates/perry-stdlib/src/fetch/mod.rs, crates/perry-stdlib/src/fetch/headers_json_test.rs
Adds js_headers_fetch_object_json FFI that locks HEADERS_REGISTRY, deduplicates header names preserving first-seen order, builds a serde_json::Map, serializes it, and returns a StringHeader pointer. Adds test module declaration and a new test verifying correct serialization and null return for unknown handles.
GLOBAL_HEADERS_OBJECT_JSON slot, registration, and headers_init_json_ptr rewrite
crates/perry-runtime/src/object/global_fetch.rs
Adds the GLOBAL_HEADERS_OBJECT_JSON atomic function-pointer slot, exports js_register_global_headers_object_json and call_global_headers_object_json, rewrites headers_init_json_ptr to branch on is_handle_band (delegating to the registered producer or falling back to "{}"), and exports js_fetch_headers_to_json as the codegen entry point.
Dispatch startup wiring
crates/perry-stdlib/src/common/dispatch.rs
Declares js_register_global_headers_object_json in the extern "C" block and calls it with js_headers_fetch_object_json during js_stdlib_init_dispatch startup under the web-fetch feature.
Handle-band guards in js_object_to_string and js_object_has_property
crates/perry-runtime/src/object/mod.rs, crates/perry-runtime/src/object/field_get_set.rs, crates/perry-runtime/src/object/tests.rs
Adds is_handle_band checks to the GC-header back-read path and POINTER_TAG ObjectHeader dereference in js_object_to_string, and an early nanbox_false return in js_object_has_property for handle-band receivers. New test object_to_string_rejects_handle_band_ids iterates handle-band ids and asserts "[object Object]" is returned without crashing.
Codegen FFI declaration and call-site update
crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs, crates/perry-codegen/src/expr/logical_collections.rs
Declares the js_fetch_headers_to_json FFI in the stdlib FFI module and updates the FetchWithOptions lowering to emit a call to js_fetch_headers_to_json instead of js_json_stringify.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PerryTS/perry#5189: Both PRs modify crates/perry-stdlib/src/common/dispatch.rs's js_stdlib_init_dispatch Fetch-related registration plumbing under the web-fetch feature (one adds the Headers -> JSON registration, the other re-gates Fetch/headers dispatch from http-client to web-fetch), so the changes are code-level related.
  • PerryTS/perry#5600: Both PRs change the fetch implementation's js_fetch_with_options input/headers handling—main PR adds a handle-band safe Headers→JSON conversion (js_fetch_headers_to_json/js_headers_fetch_object_json) that would be used by the fetch(Request) path and its extracted custom_headers/headers_json logic.

Poem

🐇 Hop hop, no more heap derefs today,
A handle-band check keeps the crashes away.
Headers live in their registry home,
JSON is built so fetch needn't roam.
The GC won't bite what it shouldn't chew —
Safe stringify paths, all shiny and new! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately and concisely describes the main fix: preventing generic object operations from dereferencing fetch-band registry handles, which was causing crashes.
Description check ✅ Passed The PR description comprehensively explains the problem, the two crash sites, the solutions, and testing approach, covering all essential aspects of the changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/fetch-headers-handle-stringify

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
crates/perry-runtime/src/object/tests.rs (1)

761-789: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the missing in-operator regression.

This covers js_object_to_string, but the PR also changes js_object_has_property. Add a companion test that a pointer-tagged fetch handle returns TAG_FALSE for a string key without dereferencing the id.

🧪 Suggested test coverage
 #[test]
 fn object_to_string_rejects_handle_band_ids() {
     use crate::value::addr_class;
     for &id in &[
         addr_class::FETCH_HANDLE_BAND_START,     // 0x40000
@@
         );
     }
 }
+
+#[test]
+fn object_has_property_rejects_fetch_handle_band_ids() {
+    use crate::value::{addr_class, JSValue};
+
+    let handle = crate::value::js_nanbox_pointer(addr_class::FETCH_HANDLE_BAND_START as i64);
+    let key_ptr = crate::string::js_string_from_bytes(b"x".as_ptr(), 1);
+    let key = f64::from_bits(JSValue::string_ptr(key_ptr).bits());
+
+    let result = js_object_has_property(handle, key);
+    assert_eq!(result.to_bits(), crate::value::TAG_FALSE);
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/object/tests.rs` around lines 761 - 789, Add a
companion test function to verify that js_object_has_property also safely
rejects handle-band ids without dereferencing them. Create a new test (similar
in structure to object_to_string_rejects_handle_band_ids) that iterates over the
same handle-band id values, creates nanboxed pointers using js_nanbox_pointer,
and calls js_object_has_property with a string key to verify that it returns
TAG_FALSE without attempting to dereference the bogus pointer address.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-runtime/src/object/field_get_set.rs`:
- Around line 2266-2278: The current handle-band check in the code block
starting at the is_handle_band call is catching both fetch handles and small
handles indiscriminately, which causes the small-handle property dispatcher to
be bypassed and always return false. Keep the crash guard for fetch handles, but
modify the logic to allow small handles to proceed to the registered
handle-property dispatcher before falling back to false. Refine the condition by
either checking specifically for fetch handle types (Web Fetch
Headers/Request/Response, net/http handles, zlib streams) instead of the entire
handle band, or by checking whether the handle is a small handle first and
allowing those to continue through the function before applying the fetch-handle
crash guard.

In `@crates/perry-runtime/src/object/global_fetch.rs`:
- Around line 157-158: The function js_fetch_headers_to_json calls
headers_init_json_ptr directly without handling null/undefined values, causing
incorrect serialization as "null" string instead of an empty object. Move the
null/undefined normalization logic that currently exists in
fetch_headers_json_ptr into the headers_init_json_ptr helper function itself, so
both js_fetch_headers_to_json and the other calling path (around lines 303-321)
share the same contract for handling null/undefined headers and consistently
produce the expected empty object JSON output.

---

Nitpick comments:
In `@crates/perry-runtime/src/object/tests.rs`:
- Around line 761-789: Add a companion test function to verify that
js_object_has_property also safely rejects handle-band ids without dereferencing
them. Create a new test (similar in structure to
object_to_string_rejects_handle_band_ids) that iterates over the same
handle-band id values, creates nanboxed pointers using js_nanbox_pointer, and
calls js_object_has_property with a string key to verify that it returns
TAG_FALSE without attempting to dereference the bogus pointer address.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: af1a7b50-d524-499b-b0cf-3ec3151edf6a

📥 Commits

Reviewing files that changed from the base of the PR and between b264d36 and 9cbb9a8.

📒 Files selected for processing (9)
  • crates/perry-codegen/src/expr/logical_collections.rs
  • crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs
  • crates/perry-runtime/src/object/field_get_set.rs
  • crates/perry-runtime/src/object/global_fetch.rs
  • crates/perry-runtime/src/object/mod.rs
  • crates/perry-runtime/src/object/tests.rs
  • crates/perry-stdlib/src/common/dispatch.rs
  • crates/perry-stdlib/src/fetch/headers.rs
  • crates/perry-stdlib/src/fetch/mod.rs

Comment thread crates/perry-runtime/src/object/field_get_set.rs Outdated
Comment thread crates/perry-runtime/src/object/global_fetch.rs
Ralph Küpper added 2 commits June 24, 2026 06:07
…lper

The fetch thunk serialized init.headers via the generic js_json_stringify,
which dereferenced a Headers handle (a fetch-band registry id) as a heap
pointer -> EXC_BAD_ACCESS in gc_obj_type. Add js_fetch_headers_to_json that
reads a Headers handle from its registry instead, and route the
fetch(url, { headers }) path through it. Same handle-band family as the
string_from_header / inline-.length guards.

(Advances the bundle's -p path past the json-stringify crash; a further
handle-band deref remains in js_object_has_property on a Headers handle.)
`key in <handle>` where the receiver is a Web Fetch Headers/Request/Response
handle (a fetch-band registry id, e.g. 0x40007) dereferenced the id as a heap
object -> EXC_BAD_ACCESS. Return false for handle-band receivers instead, same
family as the string_from_header / inline-.length / json_stringify guards.
proggeramlug pushed a commit that referenced this pull request Jun 24, 2026
- js_object_has_property: narrow the handle-band crash guard to the fetch/zlib
  bands (>= COMMON_HANDLE_BAND_END). Common/small handles now fall through to
  the registered small-handle property path instead of always returning false.
- headers_init_json_ptr: normalize null/undefined headers to {} in the shared
  helper so the codegen js_fetch_headers_to_json path matches the runtime thunk
  (previously serialized headers:null as "null").
@proggeramlug proggeramlug force-pushed the fix/fetch-headers-handle-stringify branch from 9cbb9a8 to d4ad5ba Compare June 24, 2026 04:07
- js_object_has_property: narrow the handle-band crash guard to the fetch/zlib
  bands (>= COMMON_HANDLE_BAND_END). Common/small handles now fall through to
  the registered small-handle property path instead of always returning false.
- headers_init_json_ptr: normalize null/undefined headers to {} in the shared
  helper so the codegen js_fetch_headers_to_json path matches the runtime thunk
  (previously serialized headers:null as "null").
- Move the headers_fetch_object_json test out of fetch/mod.rs into a sibling
  headers_json_test.rs submodule so fetch/mod.rs is back under the 2000-LOC
  CI limit (2020 -> 1992).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant