fix(runtime): give Buffer instances own properties, method values, and in#6406
fix(runtime): give Buffer instances own properties, method values, and in#6406proggeramlug wants to merge 2 commits into
in#6406Conversation
…d `in`
A Buffer is an ordinary `Uint8Array` in Node: user code can hang own properties
on one, read `Buffer.prototype` methods as VALUES, and shadow them. Perry stored
buffers outside the object model — a raw `BufferHeader` with no property table —
so all three were missing:
const b = Buffer.alloc(8);
b.myFlag = 42;
b.myFlag; // node: 42 perry: undefined (write dropped)
typeof b.readUInt8; // node: "function" perry: "undefined" (dispatched on CALL only)
"myFlag" in b; // node: true perry: false
mysql2 sizes every outgoing packet with exactly that idiom (`MockBuffer`): it
walks the prototype, replaces the matching write methods on a ZERO-LENGTH Buffer
with a no-op, serializes once to MEASURE, then allocates for real. With the
`typeof mock[k] === "function"` probe reading `undefined`, nothing was replaced,
the measuring pass wrote into the empty Buffer, and the MySQL handshake died with
`RangeError [ERR_OUT_OF_RANGE]`.
This adds an address-keyed own-property table for buffers, wired into the
read/write/call/`in`/computed-index paths and scanned by the GC (both the owner
address and the NaN-boxed values, in every phase). `register_buffer` clears the
entry first, so a recycled address cannot inherit a dead buffer's properties.
Numeric keys remain byte access and never become own props. The full
`Buffer.prototype` method list is now generated from `is_buffer_method_name`
(115 names; node enumerates 93) instead of an 11-name stub, so the `for (const k
in Buffer.prototype)` walk sees them.
Known limitation, filed as #6405: an own property does NOT shadow a prototype
method when the call folds to the inline byte-load intrinsic — i.e. when codegen
can statically prove the receiver is a buffer AND the method name is a literal
(`b.readUInt8(0)`, `b["readUInt8"](0)`). Every dynamic path is correct, which is
why the mysql2 idiom works in practice (the proof fails on minified receivers).
Fixing it costs a guard on the hottest byte-access path, so it wants a deliberate
decision rather than a drive-by.
Found while compiling a real Next.js + MySQL app. Covered by
`test_gap_buffer_own_props` — own data props, method-value reads, `in`,
value-call `this` binding, dynamic-key shadowing, byte indices, a recycled
address, and own props surviving a write. Byte-identical to node.
📝 WalkthroughWalkthroughBuffer and Uint8Array lowering now distinguishes numeric and string keys. Runtime support stores Buffer own properties, integrates them with GC, resolves them during reads, writes, membership checks, and method dispatch, and adds coverage for shadowing and address reuse. ChangesBuffer own-property semantics
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant BufferAccess
participant PolymorphicIndex
participant BufferOwnProps
participant BufferDispatch
BufferAccess->>PolymorphicIndex: read or write computed string key
PolymorphicIndex->>BufferOwnProps: get or set Buffer own property
PolymorphicIndex->>BufferDispatch: resolve Buffer method when no own property exists
BufferDispatch-->>BufferAccess: bound method or dispatched result
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 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-hir/src/lower/expr_assign.rs`:
- Around line 1074-1091: Use a provably numeric-key check instead of the current
!key_is_string heuristic in the Uint8ArraySet path at
crates/perry-hir/src/lower/expr_assign.rs:1074-1091, defining or reusing
key_is_numeric so dynamic and any-typed keys fall through to generic property
handling; apply the same condition before Uint8ArrayGet in
crates/perry-hir/src/lower/expr_member/member_tail.rs:752-763.
In `@crates/perry-runtime/src/buffer/own_props.rs`:
- Around line 34-37: Replace the global OnceLock<Mutex<BufferProps>> returned by
buffer_props with thread-local storage using thread_local! and RefCell, so each
isolate thread owns a separate own-properties map. Update callers, especially
scan_buffer_own_props_roots_mut, to access the RefCell through with/borrow_mut
and iterate directly without mutex locking, preserving existing property
behavior.
In `@crates/perry-runtime/src/object/buffer_dispatch.rs`:
- Around line 368-379: Update the own-property handling in the buffer method
dispatch to treat any present property as authoritative, not only closure
pointers. When buffer_get_own_prop finds a non-callable own value, throw the
standard non-callable TypeError instead of falling through to the native method;
preserve the existing js_native_call_value path for callable own properties.
In `@crates/perry-runtime/src/object/field_get_set/has_property.rs`:
- Around line 231-249: Remove the early registered-buffer return block from the
HasProperty path and merge its own-property, numeric-index, and Buffer-method
checks into the existing complete Buffer handling branch. Ensure that symbols
such as “length”, “map”, own view slots, inherited typed-array members, and
in-range byte indices continue through the established Buffer logic instead of
being returned false prematurely.
In `@crates/perry-runtime/src/object/field_set_by_name.rs`:
- Around line 293-306: The Buffer branch must never fall through to the
ObjectHeader path. In the buffer handling logic, recognize only canonical
numeric index strings (not values such as "00") for index writes; store every
other name with buffer_set_own_prop, then return unconditionally, including
invalid UTF-8 cases as appropriate for the existing behavior.
In `@crates/perry-runtime/src/object/polymorphic_index.rs`:
- Around line 115-121: Ensure Buffer method names passed to js_class_method_bind
remain valid after binding: in
crates/perry-runtime/src/object/polymorphic_index.rs:115-121, copy name into
stable closure-owned storage before binding; apply the same fix to the
GC-managed key bytes in
crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs:276-282.
Preserve the existing Buffer method dispatch behavior while ensuring both bound
pointers outlive their closures.
🪄 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: 9b876601-2aa9-458f-b6d5-d69048ebb6ea
📒 Files selected for processing (13)
crates/perry-codegen/src/expr/arrays_finds.rscrates/perry-hir/src/lower/expr_assign.rscrates/perry-hir/src/lower/expr_member/member_tail.rscrates/perry-runtime/src/buffer/header.rscrates/perry-runtime/src/buffer/mod.rscrates/perry-runtime/src/buffer/own_props.rscrates/perry-runtime/src/gc/mod.rscrates/perry-runtime/src/object/buffer_dispatch.rscrates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rscrates/perry-runtime/src/object/field_get_set/has_property.rscrates/perry-runtime/src/object/field_set_by_name.rscrates/perry-runtime/src/object/polymorphic_index.rstest-files/test_gap_buffer_own_props.ts
| // Numeric keys only — a STRING key stores an own | ||
| // property on the Buffer (Node's Buffer is an | ||
| // ordinary Uint8Array object), and an own key | ||
| // shadows the same-named prototype method. Folding | ||
| // it to the byte-write path silently dropped the | ||
| // store (see the mirrored comment in IndexGet). | ||
| let key_is_string = matches!(index.as_ref(), Expr::String(_)) | ||
| || matches!( | ||
| index.as_ref(), | ||
| Expr::LocalGet(kid) if ctx | ||
| .locals | ||
| .iter() | ||
| .find(|(_, lid, _)| lid == kid) | ||
| .is_some_and(|(_, _, kty)| matches!(kty, Type::String)) | ||
| ); | ||
| if !key_is_string | ||
| && matches!(ty, Type::Named(n) if n == "Uint8Array" || n == "Buffer") | ||
| { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Dynamic or any-typed string keys fall through to the byte-access path.
Both lowering paths currently default to the specialized Uint8ArraySet / Uint8ArrayGet byte paths when the key is not provably a string (!key_is_string). However, if the computed key is dynamic (e.g. Type::Any or the result of a function call) and evaluates to a string at runtime, it will incorrectly hit the numeric byte-access path, bypassing the own-property logic entirely.
Consider inverting this heuristic to only take the fast byte-access path when the key is provably numeric (e.g., key_is_numeric), allowing dynamic keys to safely fall through to the generic polymorphic path which checks the type at runtime.
crates/perry-hir/src/lower/expr_assign.rs#L1074-L1091: Adjust the condition to usekey_is_numericbefore emittingExpr::Uint8ArraySet.crates/perry-hir/src/lower/expr_member/member_tail.rs#L752-L763: Adjust the condition similarly before emittingExpr::Uint8ArrayGet.
📍 Affects 2 files
crates/perry-hir/src/lower/expr_assign.rs#L1074-L1091(this comment)crates/perry-hir/src/lower/expr_member/member_tail.rs#L752-L763
🤖 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-hir/src/lower/expr_assign.rs` around lines 1074 - 1091, Use a
provably numeric-key check instead of the current !key_is_string heuristic in
the Uint8ArraySet path at crates/perry-hir/src/lower/expr_assign.rs:1074-1091,
defining or reusing key_is_numeric so dynamic and any-typed keys fall through to
generic property handling; apply the same condition before Uint8ArrayGet in
crates/perry-hir/src/lower/expr_member/member_tail.rs:752-763.
| fn buffer_props() -> &'static Mutex<BufferProps> { | ||
| static PROPS: OnceLock<Mutex<BufferProps>> = OnceLock::new(); | ||
| PROPS.get_or_init(|| Mutex::new(HashMap::new())) | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Global Mutex shares isolate-specific GC state across threads.
BufferProps is defined as a global static OnceLock<Mutex<...>>. Perry runs JS inside thread-local arenas (isolates), which means this side table will wrongly share JS values (the NaN-boxed f64s) across all threads. When one thread runs GC and calls scan_buffer_own_props_roots_mut, it will extract all owners from the global map and trace them using its thread-local visitor, leading to severe memory corruption or crashes.
Switch to a thread_local! RefCell to ensure own-properties remain safely isolated per thread. This also eliminates the locking overhead and greatly simplifies the GC scanner's loop. As per path instructions, thread-local arenas make cross-thread JS values invalid.
🛠️ Proposed `thread_local!` refactor
-use std::sync::{Mutex, OnceLock};
+use std::cell::RefCell;
type BufferProps = HashMap<usize, HashMap<String, u64>>;
-fn buffer_props() -> &'static Mutex<BufferProps> {
- static PROPS: OnceLock<Mutex<BufferProps>> = OnceLock::new();
- PROPS.get_or_init(|| Mutex::new(HashMap::new()))
-}
+thread_local! {
+ static PROPS: RefCell<BufferProps> = RefCell::new(HashMap::new());
+}
/// Store `buf.<prop> = value`. Only reached for a registered buffer address.
pub fn buffer_set_own_prop(addr: usize, prop: &str, value: f64) {
if addr == 0 {
return;
}
- if let Ok(mut props) = buffer_props().lock() {
- props
- .entry(addr)
- .or_default()
- .insert(prop.to_string(), value.to_bits());
- }
+ PROPS.with(|p| {
+ p.borrow_mut()
+ .entry(addr)
+ .or_default()
+ .insert(prop.to_string(), value.to_bits());
+ });
}
/// Read an own dynamic prop, or `None` when the buffer has no such key.
pub fn buffer_get_own_prop(addr: usize, prop: &str) -> Option<f64> {
if addr == 0 {
return None;
}
- buffer_props()
- .lock()
- .ok()
- .and_then(|props| props.get(&addr).and_then(|m| m.get(prop)).copied())
+ PROPS.with(|p| p.borrow().get(&addr).and_then(|m| m.get(prop)).copied())
.map(f64::from_bits)
}
/// Whether the buffer carries any own dynamic prop under `prop`.
pub fn buffer_has_own_prop(addr: usize, prop: &str) -> bool {
buffer_get_own_prop(addr, prop).is_some()
}
/// GC: trace stored values in every phase (a stored closure is reachable ONLY
/// through this table) and rewrite the owner address when the buffer moves.
pub fn scan_buffer_own_props_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) {
- let owners = buffer_props()
- .lock()
- .ok()
- .map(|props| props.keys().copied().collect::<Vec<_>>())
- .unwrap_or_default();
- for owner in owners {
- let Some(mut entries) = buffer_props()
- .lock()
- .ok()
- .and_then(|mut props| props.remove(&owner))
- else {
- continue;
- };
- let mut new_owner = owner;
- visitor.visit_metadata_usize_slot(&mut new_owner);
- for bits in entries.values_mut() {
- let mut v = f64::from_bits(*bits);
- visitor.visit_nanbox_f64_slot(&mut v);
- *bits = v.to_bits();
- }
- if let Ok(mut props) = buffer_props().lock() {
- props.insert(new_owner, entries);
- }
- }
+ PROPS.with(|p| {
+ let mut props = p.borrow_mut();
+ let mut new_props = HashMap::with_capacity(props.len());
+ for (owner, mut entries) in props.drain() {
+ let mut new_owner = owner;
+ visitor.visit_metadata_usize_slot(&mut new_owner);
+ if new_owner == 0 {
+ continue; // Prune dead addresses
+ }
+ for bits in entries.values_mut() {
+ let mut v = f64::from_bits(*bits);
+ visitor.visit_nanbox_f64_slot(&mut v);
+ *bits = v.to_bits();
+ }
+ new_props.insert(new_owner, entries);
+ }
+ *props = new_props;
+ });
}
/// Drop every own prop recorded for `addr`. Called when a buffer is registered
/// (freed storage is recycled, and the table is address-keyed).
pub fn clear_buffer_own_props(addr: usize) {
if addr == 0 {
return;
}
- if let Ok(mut props) = buffer_props().lock() {
- props.remove(&addr);
- }
+ PROPS.with(|p| {
+ p.borrow_mut().remove(&addr);
+ });
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fn buffer_props() -> &'static Mutex<BufferProps> { | |
| static PROPS: OnceLock<Mutex<BufferProps>> = OnceLock::new(); | |
| PROPS.get_or_init(|| Mutex::new(HashMap::new())) | |
| } | |
| use std::cell::RefCell; | |
| type BufferProps = HashMap<usize, HashMap<String, u64>>; | |
| thread_local! { | |
| static PROPS: RefCell<BufferProps> = RefCell::new(HashMap::new()); | |
| } |
🤖 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/buffer/own_props.rs` around lines 34 - 37, Replace
the global OnceLock<Mutex<BufferProps>> returned by buffer_props with
thread-local storage using thread_local! and RefCell, so each isolate thread
owns a separate own-properties map. Update callers, especially
scan_buffer_own_props_roots_mut, to access the RefCell through with/borrow_mut
and iterate directly without mutex locking, preserving existing property
behavior.
Source: Path instructions
| if let Some(own) = crate::buffer::buffer_get_own_prop(addr, method_name) { | ||
| let jv = JSValue::from_bits(own.to_bits()); | ||
| if jv.is_pointer() { | ||
| let ptr = jv.as_pointer::<u8>() as usize; | ||
| if crate::closure::is_closure_ptr(ptr) { | ||
| let prev_this = crate::object::js_implicit_this_set(buf_f64); | ||
| let r = crate::closure::js_native_call_value(own, args_ptr, args_len); | ||
| crate::object::js_implicit_this_set(prev_this); | ||
| return r; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not fall through when a non-callable own property shadows the method.
An own value such as buf.writeUInt8 = undefined still shadows Buffer.prototype.writeUInt8; calling it must throw a non-callable TypeError. This currently invokes the native method instead.
🤖 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/buffer_dispatch.rs` around lines 368 - 379,
Update the own-property handling in the buffer method dispatch to treat any
present property as authoritative, not only closure pointers. When
buffer_get_own_prop finds a non-callable own value, throw the standard
non-callable TypeError instead of falling through to the native method; preserve
the existing js_native_call_value path for callable own properties.
| if crate::buffer::is_registered_buffer(addr) { | ||
| if let Some(name) = unsafe { crate::object::metadata_key_to_string(key) } { | ||
| if crate::buffer::buffer_get_own_prop(addr, &name).is_some() | ||
| || crate::object::buffer_dispatch::is_buffer_method_name(&name) | ||
| { | ||
| return nanbox_true; | ||
| } | ||
| // A numeric key is an index into the bytes: present iff in range. | ||
| if let Ok(idx) = name.parse::<usize>() { | ||
| let len = unsafe { (*(addr as *const crate::buffer::BufferHeader)).length }; | ||
| return if idx < len as usize { | ||
| nanbox_true | ||
| } else { | ||
| nanbox_false | ||
| }; | ||
| } | ||
| return nanbox_false; | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not bypass the complete Buffer HasProperty path.
This early branch returns false before the existing Buffer logic can recognize own view slots and inherited typed-array members. For example, "length" in buf and "map" in buf now become false. Merge own-property and Buffer-method checks into the later branch instead.
🤖 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/field_get_set/has_property.rs` around lines
231 - 249, Remove the early registered-buffer return block from the HasProperty
path and merge its own-property, numeric-index, and Buffer-method checks into
the existing complete Buffer handling branch. Ensure that symbols such as
“length”, “map”, own view slots, inherited typed-array members, and in-range
byte indices continue through the established Buffer logic instead of being
returned false prematurely.
| if !key.is_null() && crate::buffer::is_registered_buffer(obj as usize) { | ||
| unsafe { | ||
| let key_ptr = (key as *const u8).add(std::mem::size_of::<crate::StringHeader>()); | ||
| let key_len = (*key).byte_len as usize; | ||
| if let Ok(name) = std::str::from_utf8(std::slice::from_raw_parts(key_ptr, key_len)) { | ||
| // Numeric keys are element writes (`buf[0] = 1`) — leave those | ||
| // to the index path; only NAMED props become expandos. | ||
| if name.parse::<u32>().is_err() { | ||
| crate::buffer::buffer_set_own_prop(obj as usize, name, value); | ||
| return; | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Return from the Buffer branch and distinguish canonical indices.
parse::<u32>() misclassifies ordinary keys such as "00" as numeric. They then fall through toward the ObjectHeader path even though the receiver is a raw BufferHeader, risking an invalid GC-header read. Handle canonical numeric-index strings explicitly; store all other names as own properties, then always return.
🤖 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/field_set_by_name.rs` around lines 293 - 306,
The Buffer branch must never fall through to the ObjectHeader path. In the
buffer handling logic, recognize only canonical numeric index strings (not
values such as "00") for index writes; store every other name with
buffer_set_own_prop, then return unconditionally, including invalid UTF-8 cases
as appropriate for the existing behavior.
| if crate::object::buffer_dispatch::is_buffer_method_name(&name) { | ||
| let bytes = name.as_bytes(); | ||
| return crate::object::js_class_method_bind( | ||
| crate::value::js_nanbox_pointer(raw as i64), | ||
| bytes.as_ptr(), | ||
| bytes.len(), | ||
| ); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Ensure bound Buffer method names outlive their closures.
Both paths pass transient name storage to js_class_method_bind, which retains the pointer.
crates/perry-runtime/src/object/polymorphic_index.rs#L115-L121: copynameinto stable closure-owned storage before binding.crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs#L276-L282: copy the GC-managed key bytes before binding.
📍 Affects 2 files
crates/perry-runtime/src/object/polymorphic_index.rs#L115-L121(this comment)crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs#L276-L282
🤖 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/polymorphic_index.rs` around lines 115 - 121,
Ensure Buffer method names passed to js_class_method_bind remain valid after
binding: in crates/perry-runtime/src/object/polymorphic_index.rs:115-121, copy
name into stable closure-owned storage before binding; apply the same fix to the
GC-managed key bytes in
crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs:276-282.
Preserve the existing Buffer method dispatch behavior while ensuring both bound
pointers outlive their closures.
…_name_tail get_field_by_name_tail.rs is a single very large function and was 11 lines under the 2000-line cap; the Buffer own-property/method read pushed it to 2016. Move that block to a `buffer_own_prop` sibling module. Pure relocation.
|
CodeRabbit's key-guard finding is real — confirmed against node and fixed in The dynamic-key hole. The guard tested const buf = Buffer.alloc(4);
const keys: any[] = ["dyn"];
const k: any = keys[0]; // a string at runtime
(buf as any)[k] = "D";
console.log((buf as any)[k]); // node: "D" this PR (before): undefinedThe store went to a byte index and the read came back out-of-range. That's the same defect this PR fixes for static keys, left open for dynamic ones — and dynamic is the common shape (a key out of Fix: invert to The thing worth checking with that inversion is that dynamic numeric indices still reach the bytes, since they now route through the generic path. They do. Verified byte-identical to node:
Also Separately — |
e466bfa to
151cc2d
Compare
|
Correction — I was wrong, and I've reverted it. The What I got wrongCodeRabbit's finding is real — an const k: any = keys[0]; // "dyn" at runtime
(buf as any)[k] = "D";
(buf as any)[k] // node: "D" perry: undefinedBut my fix assumed a "provably numeric" key could be recognised statically. It can't. Printing the HIR for an ordinary buffer kernel shows why: So a loop counter and a string-holding
Trading a buffer-kernel perf regression for an edge-case correctness fix is a bad deal, especially since the dynamic-key bug is pre-existing on What actually needs to happen (not in this PR)The guard can't separate these two cases while loop counters are
Filing this separately with the repro and the HIR evidence rather than jamming it in here. The file-size split ( |
The bug
A Buffer is an ordinary
Uint8Arrayin Node: user code can hang own properties on one, readBuffer.prototypemethods as values, and shadow them. Perry stored buffers outside the object model — a rawBufferHeaderwith no property table — so all three were missing:Why it matters
mysql2 sizes every outgoing packet with exactly that idiom (
MockBuffer): it walks the prototype, replaces the matching write methods on a zero-length Buffer with a no-op, serializes once to measure, then allocates for real:With the
typeof mock[k] === "function"probe readingundefined, nothing was replaced, the measuring pass wrote into the empty Buffer, and the MySQL handshake died withRangeError [ERR_OUT_OF_RANGE].The fix
An address-keyed own-property table for buffers, wired into the read / write / call /
in/ computed-index paths and scanned by the GC (both the owner address and the NaN-boxed values, in every phase).register_bufferclears the entry first, so a recycled address cannot inherit a dead buffer's properties. Numeric keys remain byte access and never become own props.The
Buffer.prototypemethod list is now generated fromis_buffer_method_name(115 names; node enumerates 93) instead of an 11-name stub, so thefor (const k in Buffer.prototype)walk actually sees them.Known limitation — #6405
An own property does not shadow a prototype method when the call folds to the inline byte-load intrinsic: when codegen can statically prove the receiver is a buffer and the method name is a literal (
b.readUInt8(0),b["readUInt8"](0)). Every dynamic path is correct — which is why the mysql2 idiom works in practice, since the proof fails on minified receivers. Fixing it costs a guard on Perry's hottest byte-access path (a runtime flag + branch, or a whole-program pre-codegen scan), so it deserves a deliberate decision rather than a drive-by. Filed as #6405.Test
test_gap_buffer_own_props— own data props, method-value reads,in, value-callthisbinding, dynamic-key shadowing, byte indices, a recycled address, and own props surviving a write through the buffer's own methods. Byte-identical to node.Found while compiling a real Next.js + MySQL app.
Summary by CodeRabbit
New Features
Bug Fixes
inand field/property dispatch for registered Buffers to correctly reflect own properties and bounds.Tests