-
-
Notifications
You must be signed in to change notification settings - Fork 145
fix(runtime): give Buffer instances own properties, method values, and in
#6406
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
Changes from all commits
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 | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,107 @@ | ||||||||||||||||||||||||
| //! Own (dynamic) properties assigned onto a `Buffer` / typed-array value. | ||||||||||||||||||||||||
| //! | ||||||||||||||||||||||||
| //! Perry allocates buffers as raw `BufferHeader`s outside the object model, so | ||||||||||||||||||||||||
| //! a plain `buf.foo = v` had nowhere to go: the set was dropped and the read | ||||||||||||||||||||||||
| //! returned `undefined`. Node's Buffer is a `Uint8Array` — an ordinary object — | ||||||||||||||||||||||||
| //! so user code freely stores properties on one, and it also *shadows* the | ||||||||||||||||||||||||
| //! prototype's methods when the key collides. | ||||||||||||||||||||||||
| //! | ||||||||||||||||||||||||
| //! mysql2 sizes every outgoing packet with exactly that idiom | ||||||||||||||||||||||||
| //! (`packets/packet.js` → `MockBuffer`): | ||||||||||||||||||||||||
| //! | ||||||||||||||||||||||||
| //! ```js | ||||||||||||||||||||||||
| //! const noop = function () {}; | ||||||||||||||||||||||||
| //! const mock = Buffer.alloc(0); | ||||||||||||||||||||||||
| //! for (const k in Packet.prototype) | ||||||||||||||||||||||||
| //! if (typeof mock[k] === "function") mock[k] = noop; // neutralize writes | ||||||||||||||||||||||||
| //! // …serialize once against `mock` to MEASURE, then for real against | ||||||||||||||||||||||||
| //! // Buffer.alloc(mock.offset) | ||||||||||||||||||||||||
| //! ``` | ||||||||||||||||||||||||
| //! | ||||||||||||||||||||||||
| //! Without own-prop storage the no-ops never landed, the measuring pass wrote | ||||||||||||||||||||||||
| //! into the zero-length Buffer, and the MySQL handshake died with | ||||||||||||||||||||||||
| //! `RangeError [ERR_OUT_OF_RANGE]`. | ||||||||||||||||||||||||
| //! | ||||||||||||||||||||||||
| //! Mirrors `closure::dynamic_props` (same locked side table + GC root scanner | ||||||||||||||||||||||||
| //! contract): values are traced in EVERY phase so a stored closure/array stays | ||||||||||||||||||||||||
| //! reachable, and the owner key is rewritten on evacuation. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| use std::collections::HashMap; | ||||||||||||||||||||||||
| use std::sync::{Mutex, OnceLock}; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| 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())) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
Comment on lines
+34
to
+37
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 | 🔴 Critical | ⚡ Quick win Global
Switch to a 🛠️ 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
Suggested change
🤖 Prompt for AI AgentsSource: Path instructions |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /// 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()); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /// 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()) | ||||||||||||||||||||||||
| .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); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /// 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); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -360,6 +360,23 @@ pub unsafe fn dispatch_buffer_method( | |
| } else { | ||
| &[] | ||
| }; | ||
| // An OWN property shadows the prototype method of the same name (Node's | ||
| // Buffer is an ordinary Uint8Array). mysql2's `MockBuffer` overwrites the | ||
| // write methods of a zero-length Buffer with a no-op to MEASURE a packet | ||
| // before allocating it; dispatching the native method regardless would | ||
| // write into the empty buffer and throw RangeError [ERR_OUT_OF_RANGE]. | ||
| 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; | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+368
to
+379
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 | 🟠 Major | ⚡ Quick win Do not fall through when a non-callable own property shadows the method. An own value such as 🤖 Prompt for AI Agents |
||
| let arg_i32 = |i: usize| -> i32 { | ||
| if i < args.len() { | ||
| args[i] as i32 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| //! Buffer own-property / method-value reads for the object-deref tail. | ||
| //! | ||
| //! Split out of `get_field_by_name_tail.rs` to keep that file under the | ||
| //! 2000-line budget (it is a single very large function). | ||
|
|
||
| use super::*; | ||
|
|
||
| /// Node's Buffer IS an object (a Uint8Array), so user code can store properties | ||
| /// on one — and an own key SHADOWS the prototype method of the same name. Perry | ||
| /// keeps buffers outside the object model, so both halves were missing: | ||
| /// `buf.foo = v` was dropped and `typeof buf.writeInt8` read `undefined` | ||
| /// (methods dispatched on CALL only). | ||
| /// | ||
| /// mysql2 sizes every outgoing packet with exactly that idiom (`MockBuffer`): | ||
| /// it walks `Packet.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 false, nothing was | ||
| /// replaced, the measuring pass wrote into the empty Buffer, and the MySQL | ||
| /// handshake died with RangeError [ERR_OUT_OF_RANGE]. | ||
| /// | ||
| /// Returns `None` when the key names neither an own property nor a Buffer | ||
| /// method, so the caller falls through to the rest of the property walk. | ||
| pub(super) fn buffer_own_prop_or_method( | ||
| obj: *const ObjectHeader, | ||
| key_bytes: &[u8], | ||
| key_ptr: *const u8, | ||
| key_len: usize, | ||
| ) -> Option<JSValue> { | ||
| let name = std::str::from_utf8(key_bytes).ok()?; | ||
| if let Some(v) = crate::buffer::buffer_get_own_prop(obj as usize, name) { | ||
| return Some(JSValue::from_bits(v.to_bits())); | ||
| } | ||
| if crate::object::buffer_dispatch::is_buffer_method_name(name) { | ||
| let bound = crate::object::js_class_method_bind( | ||
| crate::value::js_nanbox_pointer(obj as i64), | ||
| key_ptr, | ||
| key_len, | ||
| ); | ||
| return Some(JSValue::from_bits(bound.to_bits())); | ||
| } | ||
| None | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -223,6 +223,30 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 { | |
| // already does further down this function. | ||
| if obj_val.is_pointer() { | ||
| let addr = (obj_val.bits() & crate::value::POINTER_MASK) as usize; | ||
| // A Buffer is an ordinary Uint8Array in Node, so `"k" in buf` must see both | ||
| // the own properties user code hangs on it and the inherited | ||
| // `Buffer.prototype` methods. Perry keeps buffers outside the object model | ||
| // (a raw `BufferHeader`), so the generic pointer path below reads the header | ||
| // as an `ObjectHeader` and reported `false` for both. | ||
| 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; | ||
| } | ||
| } | ||
|
Comment on lines
+231
to
+249
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 | 🟠 Major | ⚡ Quick win Do not bypass the complete Buffer This early branch returns 🤖 Prompt for AI Agents |
||
| if addr >= crate::value::addr_class::COMMON_HANDLE_BAND_END | ||
| && crate::value::addr_class::is_handle_band(addr) | ||
| { | ||
|
|
||
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.
🎯 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/Uint8ArrayGetbyte paths when the key is not provably a string (!key_is_string). However, if the computed key is dynamic (e.g.Type::Anyor 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