Skip to content

fix(object): a Buffer/DataView receiver must not reach the ordinary object walk (#8117) - #8141

Merged
proggeramlug merged 2 commits into
mainfrom
fix/8117-buffer-receiver-own-key
Aug 15, 2026
Merged

fix(object): a Buffer/DataView receiver must not reach the ordinary object walk (#8117)#8141
proggeramlug merged 2 commits into
mainfrom
fix/8117-buffer-receiver-own-key

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Closes the two pass -> crash entries of #8117: test_gap_buffer_own_props and test_gap_6386_dataview_concat_regex_fastpaths. Both are SIGSEGV, both reproduce standalone on Linux, and both are this one missing receiver arm.

The bug

obj_value_has_own_key (crates/perry-runtime/src/object/reflect_support.rs) has arms for a registry typed array, a GC_TYPE_ARRAY/GC_TYPE_LAZY_ARRAY, a closure, and a native-module namespace. It had none for a Buffer / ArrayBuffer / DataView, so one fell through to the ordinary ObjectHeader arm.

A buffer is a BufferHeader: no class_id, no keys_array. The walk read (*obj).keys_array out of the bytes that follow a buffer header and handed that to js_array_length, whose lazy-array probe dereferences addr - 8. The only thing between the two was a < 0x10000 magnitude floor, which arbitrary payload bytes clear routinely.

Four lines reproduce it — 10/10 on Linux:

const b: any = Buffer.alloc(8);
b.readUInt8 = function () { return "shadowed"; };
const k = "readUInt8";
b[k](0);
#0  js_array_length                                        <- SIGSEGV, x0 = 0x12b00003aa1f03e2
#1  perry_runtime::object::reflect_support::obj_value_has_own_key
#2  perry_runtime::proxy::own_set_descriptor
#3  perry_runtime::proxy::ordinary_set_with_receiver
#4  js_put_value_set
#5  js_put_value_set_dyn_ic_miss
#6  main

x0 is payload bytes, not an address. The faulting instruction is ldurb w9, [x20, #-8] followed by cmp w9, #0x9 and a compare against 0x4C5A5841GC_TYPE_LAZY_ARRAY and LAZY_ARRAY_MAGIC.

This is the same "ask the receiver question before the generic path claims it" shape as #8090 / #8109 / #8119 / #8120, on the has-own-key / [[Set]] path instead of the element path.

The fix

A registered-buffer arm, placed beside the typed-array arm and for the same stated reason (it must precede the GC-header read, because small slab buffers carry no GcHeader at all).

A buffer's own string keys are exactly its expando table (#6406). Prototype methods are inherited, not own — that is what lets buf.readUInt8 = fn install a shadowing own property instead of being treated as the redefinition of an existing one. Canonical integer indices are deliberately not folded in: the byte-index [[Set]] is routed upstream of this call, and answering "own" for one would divert it into the ordinary data-property store.

Second, smaller change: the keys_array guard becomes addr_class::is_plausible_heap_addr instead of the bare < 0x10000 floor — defence in depth for the class this fix closes by routing. A receiver kind with no arm here should produce a wrong answer, not a SIGSEGV.

Why it read as twelve days old and macOS-only

The garbage keys_array has to clear the floor and land unmapped. macOS's 2 TB heap floor means it usually reads as null, so the same call silently answered "no own key" for a property the buffer really owns — wrong, but not fatal. That is why 30 runs each on macOS/arm64 were clean under every GC instrument (PERRY_GC_ZEAL, PERRY_GC_ZEAL_ALLOC_KB=0, forced evacuation, from-space protect + scan, PERRY_GC_SCHEDULE_RATE=1, PERRY_GEN_GC=0) while CI segfaulted.

It is also why the CI-log bisect landed on #7314 (2026-08-03): that commit added initialize_stack_maps() to js_gc_init, whose Linux arm std::fs::reads the whole executable at startup — a Linux-only multi-megabyte alloc/free before any user code, which moved the heap enough to make the pre-existing garbage read fatal. The defect is older than #7314; #7314 only made it land. Details in #8117 (comment).

The new test asserts the ANSWER, not "did not crash", so it fails on macOS too.

Testing

object::tests::buffer_own_key_comes_from_the_expando_table_not_the_object_walk — watched fail with the buffer arm removed:

panicked at crates/perry-runtime/src/object/tests.rs:1618:
a buffer's own expando property must be reported as an own key

It also asserts a Buffer.prototype method and an unknown key are not own, so the arm cannot pass by answering true unconditionally.

  • cargo test -p perry-runtime --lib2390 passed, 0 failed, 4 ignored (repo baseline 2389 + this test), exit 0. grep -c "Compiling perry-runtime v" on the restore build = 1.

  • End-to-end on Linux (ubuntu 24.04 aarch64 container, --release, PERRY_NO_AUTO_OPTIMIZE=1, PERRY_RUNTIME_DIR pinned), before → after:

    fixture before after
    the 4-line repro above 10/10 SIGSEGV 20/20 exit 0
    test_gap_6386_dataview_concat_regex_fastpaths 25/25 SIGSEGV 20/20 exit 0
    test_gap_buffer_own_props SIGSEGV (auto-optimize) 20/20 exit 0

    Both gap fixtures are byte-identical to node v26.5.1 after the fix (diff clean; node --version checked against .node-version).

  • The x86-64 side is confirmed independently: on ubuntu-latest at base fa83ecab2, test_gap_buffer_own_props segfaults standalone, outside the gap harness — so this is not a test-isolation or ordering artefact.

  • rustfmt, scripts/check_file_size.sh, and all sixteen lint gate scripts run individually, all exit 0. raw_handle_debt included: the new arm carries its address across the GC-capable key coercion with across_mut rather than a bare handle read, so the file stays at its ceiling of 3.

Not in this PR

Summary by CodeRabbit

  • Bug Fixes

    • Fixed a Linux crash that could occur when dynamically checking properties on Buffer objects.
    • Corrected own-property checks for Buffer, ArrayBuffer, and DataView objects.
    • Ensured inherited methods and indexed properties are not incorrectly reported as own properties.
    • Added safeguards for invalid object references during property checks.
  • Tests

    • Added regression coverage for Buffer expando properties, inherited methods, and unknown keys across supported platforms.

…bject walk (#8117)

`obj_value_has_own_key` has arms for a registry typed array, a
GC_TYPE_ARRAY/LAZY_ARRAY, a closure, and a native-module namespace. It had none
for a Buffer / ArrayBuffer / DataView, so one fell through to the ordinary
`ObjectHeader` arm — and a buffer is a `BufferHeader`: no `class_id`, no
`keys_array`. The walk read `(*obj).keys_array` out of the bytes that follow a
buffer header and handed that to `js_array_length`, whose lazy-array probe
dereferences `addr - 8`. The only thing in between was a `< 0x10000` magnitude
floor, which arbitrary payload bytes clear routinely.

Four lines reproduce it, and it is the two `pass -> crash` entries of #8117:

    const b: any = Buffer.alloc(8);
    b.readUInt8 = function () { return "shadowed"; };
    const k = "readUInt8";
    b[k](0);

    #0  js_array_length                                        <- SIGSEGV
    #1  perry_runtime::object::reflect_support::obj_value_has_own_key
    #2  perry_runtime::proxy::own_set_descriptor
    #3  perry_runtime::proxy::ordinary_set_with_receiver
    #4  js_put_value_set
    #5  js_put_value_set_dyn_ic_miss

with `x0 = 0x12b00003aa1f03e2` — payload bytes, not an address. It is the same
"ask the receiver question before the generic path claims it" shape as
#8090/#8109/#8119/#8120, on the has-own-key / `[[Set]]` path.

A buffer's own string keys are exactly its expando table (#6406). Prototype
methods are inherited, not own, which is what lets `buf.readUInt8 = fn` install
a shadowing own property rather than be treated as a redefinition. Canonical
integer indices are deliberately not folded in: the byte-index `[[Set]]` is
routed upstream of this call, and answering "own" for one would divert it into
the ordinary data-property store.

Second, smaller change: the `keys_array` guard becomes
`addr_class::is_plausible_heap_addr` instead of the bare `< 0x10000` floor. That
is defence in depth for the class this fix closes by routing — a receiver kind
with no arm here should get a wrong answer, not a SIGSEGV.

Why it was invisible on macOS, and why it looked twelve days old: the garbage
`keys_array` has to clear the floor AND land unmapped. macOS's 2 TB heap floor
means it usually reads as null, so the same call silently answered "no own key"
for a property the buffer really owns. That is what the new test asserts, so it
fails on both platforms.

Testing
- `object::tests::buffer_own_key_comes_from_the_expando_table_not_the_object_walk`,
  watched fail with the buffer arm removed: "a buffer's own expando property
  must be reported as an own key". Also asserts a prototype method and an
  unknown key are NOT own, so the arm cannot pass by answering true.
- `cargo test -p perry-runtime --lib`: 2390 passed, 0 failed, 4 ignored
  (baseline 2389 + this test), exit 0; `Compiling perry-runtime v` = 1.
- End-to-end on Linux (ubuntu 24.04 aarch64 container, release,
  `PERRY_NO_AUTO_OPTIMIZE=1`, `PERRY_RUNTIME_DIR` pinned), before -> after:
    mini repro above                              10/10 SIGSEGV -> 20/20 exit 0
    test_gap_6386_dataview_concat_regex_fastpaths 25/25 SIGSEGV -> 20/20 exit 0
    test_gap_buffer_own_props                     SIGSEGV       -> 20/20 exit 0
  Both gap fixtures are byte-identical to node v26.5.1 after the fix.
- The x86-64 side is confirmed independently: on ubuntu-latest,
  `test_gap_buffer_own_props` segfaults standalone at base fa83eca.
- rustfmt, `scripts/check_file_size.sh` and all sixteen `lint` gate scripts
  clean (`raw_handle_debt` included — the new arm carries its address across the
  GC-capable coercion with `across_mut`, not a bare handle read).

Claude-Session: https://claude.ai/code/session_01MsfDzkTEnuS2nh7ygsYkoi
@coderabbitai

coderabbitai Bot commented Aug 15, 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: b8d1d789-b47e-473f-b43b-127e22998b0a

📥 Commits

Reviewing files that changed from the base of the PR and between fa83eca and 3574ce0.

📒 Files selected for processing (3)
  • changelog.d/8141-buffer-receiver-own-key.md
  • crates/perry-runtime/src/object/reflect_support.rs
  • crates/perry-runtime/src/object/tests.rs

📝 Walkthrough

Walkthrough

obj_value_has_own_key now handles Buffer-like receivers through their expando tables, preserves the receiver during key coercion, rejects invalid key-array addresses, and includes regression coverage and changelog documentation.

Changes

Buffer own-key handling

Layer / File(s) Summary
Runtime own-key lookup
crates/perry-runtime/src/object/reflect_support.rs
Registered Buffer, ArrayBuffer, and DataView receivers use expando properties for own-key checks. Generic key-array access validates canonical heap addresses before dereferencing.
Regression coverage and changelog
crates/perry-runtime/src/object/tests.rs, changelog.d/8141-buffer-receiver-own-key.md
The regression test checks expando, inherited, and missing Buffer properties. The changelog documents the crash fix and verification results.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 3574c

This localized change fixes Buffer and DataView own-property handling with targeted regression coverage; no actionable merge-blocking risk remains after normal checks and review.

Possibly related PRs

  • PerryTS/perry#6941: Both harden own-property checks against garbage collection during key coercion.
  • PerryTS/perry#6713: Both address unsafe dereferencing of invalid values in runtime object operations.
  • PerryTS/perry#7603: Both add validation to prevent object-backed values from being treated as raw runtime headers.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main fix: preventing Buffer/DataView receivers from entering the ordinary object walk.
Description check ✅ Passed The description clearly explains the bug, fix, related issue, regression test, and extensive validation, although it does not follow the template headings exactly.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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/8117-buffer-receiver-own-key

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Control run, to keep the two halves of this PR honestly labelled.

Rebuilt in the Linux container with only the buffer arm — the keys_array guard reverted to the original < 0x10000 floor — grep -c "Compiling perry-runtime v" = 1:

ARMONLY mini_buf exits: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
ARMONLY 6386     exits: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0   lines=28

Both clean, 15/15. So the arm alone fixes the crash and the is_plausible_heap_addr change is genuinely defence in depth for the next missing arm, not part of the fix — which is what the description claims. Happy to drop it into a separate PR if you'd rather keep this one to the single arm.

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