fix: Buffer own-property reads via dynamic key (#6412) and shadowed folded read intrinsic (#6405) - #6550
Merged
proggeramlug merged 3 commits intoJul 18, 2026
Conversation
…, not undefined (PerryTS#6412) A Buffer is an ordinary Uint8Array, so `buf[k]` with a string-valued `k` reads an own property (else the shadowed prototype method), not a byte. Storing through a computed key that is a string at runtime but `any` statically (`const k: any = keys[0]; (buf as any)[k] = "D"`) landed the own property via `js_object_set_index_polymorphic` -> `buffer_set_own_prop`, but reading it back returned `undefined`: the read folds to `Uint8ArrayGet` -> `js_typed_array_index_get_dynamic` -> (for a BufferHeader) `js_dyn_index_get`, whose buffer arm returned `undefined` for every non-numeric key and never consulted the own-prop table. Route a string key through `js_object_get_field_by_name_f64` — the same own-prop/method-aware getter (`buffer_own_prop_or_method`) the dotted `buf.k` and static-string `buf["k"]` paths already use, and the mirror of the already-correct `js_object_get_index_polymorphic` sibling. The numeric byte fast path is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rinsic (PerryTS#6405) An own property on a Buffer shadows the same-named `Buffer.prototype` method (a Buffer is an ordinary Uint8Array). Every dynamic dispatch path honors this (`dispatch_buffer_method` checks own props first), but a statically provable `buf.readUInt8(0)` folds to `try_emit_buffer_read_intrinsic` — an inline byte-load that reads the bytes directly and never consults the property table, so `b.readUInt8 = fn; b.readUInt8(0)` returned the raw byte instead of the override. Add a whole-module pre-codegen scan (`module_shadows_buffer_read_method`): when the module assigns any Buffer numeric read-method name as a property (`buf.readUInt8 = fn`, `buf["readInt32BE"] = fn`), `try_emit_buffer_read_intrinsic` bails so the call routes through the own-prop-aware `js_native_call_method` -> `dispatch_buffer_method`. This is the issue's whole-program compile-time-flag option: zero runtime cost for the common program that never shadows a Buffer method — the inline fast path is untouched, so the native-region-proof gate (incl. `width_aware_buffer_kernels`'s folded reads) stays green. A per-module scan suffices because the intrinsic only fires on a buffer that `lower_buffer_access_proof` proves non-escaping — a closure-captured or cross-module-shared/exported buffer is stamped hazardous and never folds (verified: an exported buffer shadowed in another module already dispatches correctly). Only literal property names are matched; a dynamic `buf[computedName] = fn` is out of scope (that shape already defeats the static buffer proof). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughChangesBuffer dispatch correctness
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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: 2
🤖 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-codegen/src/lower_call/buffer_intrinsic.rs`:
- Around line 297-319: Update module_shadows_buffer_read_method’s class
traversal to check constructor, method, getter, setter, and computed-member
names with is_buffer_numeric_read_method, setting found when any match. Also
scan class.fields, class.static_fields, extends_expr, and each computed member’s
key_expr and function body via scan_body so assignments in initializers and
class expressions are detected.
In `@crates/perry-runtime/src/value/dyn_index.rs`:
- Around line 180-190: Update the string-key handling in the dynamic index
lookup to detect canonical numeric strings before calling
js_object_get_field_by_name_f64. Route those keys through the existing Buffer
indexed-access path, such as js_buffer_get, so buf[k] with a string key like "2"
returns the byte value; preserve name lookup for non-numeric strings and the
existing undefined fallback.
🪄 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: 652e74c2-5958-4d3e-836c-90dc93634e90
📒 Files selected for processing (12)
crates/perry-codegen/src/codegen/closure.rscrates/perry-codegen/src/codegen/entry.rscrates/perry-codegen/src/codegen/function.rscrates/perry-codegen/src/codegen/method.rscrates/perry-codegen/src/codegen/mod.rscrates/perry-codegen/src/codegen/opts.rscrates/perry-codegen/src/expr/mod.rscrates/perry-codegen/src/lower_call/buffer_intrinsic.rscrates/perry-codegen/src/lower_call/mod.rscrates/perry-runtime/src/value/dyn_index.rstest-files/test_gap_buffer_dyn_key_own_prop_6412.tstest-files/test_gap_buffer_own_prop_shadow_intrinsic_6405.ts
…lete class scan PerryTS#6412: a canonical numeric-index string on a Buffer (`buf["2"]`) now reads the byte at that index before the named-property fallback (out-of-range → undefined), matching Node's IntegerIndexedExotic [[Get]]. It previously fell through to the by-name getter and returned undefined. PerryTS#6405: the module shadow-scan now walks class field / static-field initializers (which live in `fields[*].init`, emitted via `apply_field_initializers_recursive` separately from the constructor body), computed field keys, computed-member key expressions, and the dynamic `extends` expression — a shadow assignment hidden in any of those escaped the scan. Class member NAMES are intentionally NOT matched: a class method/field named `readUInt8` is a user-class member, not an own-prop assignment on a `Buffer.alloc` local, so it never folds to the intrinsic (which only fires on a proven buffer view, never a class instance). Adds gap-test cases (`dyn-str-index`, `dyn-str-oob`) and two scan unit tests (`detects_shadow_in_class_field_initializer`, `plain_class_field_does_not_shadow`). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related Buffer own-property correctness bugs where a folded byte-access path failed to honor that a Node
Bufferis an ordinaryUint8Array— user code can store own properties on one, and an own key shadows a same-named prototype method. Both bugs' write/dynamic halves already worked; these fix the folded read halves.#6412 — a dynamic (any-typed) string key silently read a byte instead of the own property
The write stored the own property fine (
js_object_set_index_polymorphic→buffer_set_own_prop), but the read folds toUint8ArrayGet→js_typed_array_index_get_dynamic→ (for aBufferHeader)js_dyn_index_get, whose buffer arm returnedundefinedfor any non-numeric key — it never consulted the own-prop table.Fix (
crates/perry-runtime/src/value/dyn_index.rs): the buffer arm now routes a string key tojs_object_get_field_by_name_f64— the same own-prop/method-aware getter (buffer_own_prop_or_method) the dottedbuf.kand static-stringbuf["k"]paths already use. The numeric byte fast path is unchanged. This brings the arm in line with its already-correct siblingjs_object_get_index_polymorphic.#6405 — an own property did not shadow a prototype method when the call folded to the inline byte intrinsic
A statically-provable
buf.readUInt8(0)folds totry_emit_buffer_read_intrinsic(an inline LLVM load) that never consults the property table. Every dynamic dispatch path already honors the shadow (dispatch_buffer_methodchecks own props first); only the static fold did not.Fix (codegen): a whole-module pre-codegen scan (
module_shadows_buffer_read_method) records whether the module assigns any Buffer numeric read-method name as a property. When it does,try_emit_buffer_read_intrinsicbails so the call routes through the own-prop-awarejs_native_call_method→dispatch_buffer_method. This is the issue's "whole-program compile-time flag" option: zero runtime cost for the overwhelmingly common program that never shadows a Buffer method — the inline fast path is untouched there, so thenative-region-proofcompiler-output gate stays green.A per-module scan is sufficient: the intrinsic only fires on a buffer that
lower_buffer_access_proofproves non-escaping — a closure-captured or cross-module-shared/exported buffer is stamped hazardous and never folds (verified — an exported buffer shadowed in another module already dispatches correctly). Only literal property names are matched; a dynamicbuf[computedName] = fnis out of scope (that shape already defeats the static buffer proof in practice).Verification
node --experimental-strip-types.native-region-proofcompiler-output gate passes — the buffer fast path (incl.width_aware_buffer_kernels's foldedreadInt16BE/readUInt32BE/… loops) is unchanged for non-shadowing programs.test_gap_buffer_dyn_key_own_prop_6412.ts,test_gap_buffer_own_prop_shadow_intrinsic_6405.ts.No version bump or changelog entry (left for the maintainer to fold in at merge).
Fixes #6412. Fixes #6405.
🤖 Generated with Claude Code
Summary by CodeRabbit
Buffer/Uint8Arraydynamic access for string keys so canonical numeric index strings behave like byte indices, while non-canonical keys resolve as own properties.Bufferinstance defines own numeric read-method properties, ensuring shadowed prototype methods use the correct runtime dispatch.Bufferread methods across dot, literal bracket, and dynamic-key access.undefinedon out-of-range reads.