Skip to content

fix(runtime): guard js_error_get_errors against non-error receivers (.errors corrupted regular objects) - #5543

Merged
proggeramlug merged 1 commit into
mainfrom
fix/errors-accessor-guard
Jun 22, 2026
Merged

fix(runtime): guard js_error_get_errors against non-error receivers (.errors corrupted regular objects)#5543
proggeramlug merged 1 commit into
mainfrom
fix/errors-accessor-guard

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Problem

for (const x of obj.errors) (and any obj.errors read) corrupted its value when obj is a regular object, not a native error — surfacing downstream as TypeError: Iterator result is not an object.

Root cause (pinned at the instruction level)

  • Codegen lowers every obj.errors property read (receiver typically any) to the native js_error_get_errors accessor, then unconditionally OR-s POINTER_TAG onto the result (for the AggregateError.errors array fast path).
  • js_error_get_errors (crates/perry-runtime/src/error.rs) blindly read (*ErrorHeader).errors at the fixed byte offset +48 — only valid for a genuine native error.
  • For a regular object, +48 holds NaN-boxed undefined (0x7FFC_0000_0000_0001). OR-ing POINTER_TAG yields 0x7FFD_0000_0000_0001 — a pointer-tagged value with payload 1. js_get_iterator then treats raw 1 as a handle-band iterator; .next() finds no dispatcher → returns undefined → "Iterator result is not an object".

(Confirmed in lldb: the corrupt receiver entered js_get_iterator straight from the bl js_error_get_errorsorr POINTER_TAG site; the receiver's object_type was OBJECT_TYPE_REGULAR, and +48 held 0x7FFC…0001.)

Fix

js_error_get_errors now validates the receiver is a heap-plausible native error (object_type == OBJECT_TYPE_ERROR) before reading the +48 slot. For any other receiver it resolves errors as an ordinary own property and returns its real value (a clean array pointer, or null when absent → for…of over null reports "not iterable", as a generic read would). Native AggregateError still uses the fixed slot.

Tests

cargo test -p perry-runtime --lib: 1071 passed (+2 new: a regular object resolves its real .errors array / null when absent / rejects a handle-band pointer; native AggregateError unchanged). Verified against a real large bundle: subcommands that previously crashed here now advance past it.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed incorrect memory access when reading error properties from non-native Error objects, which could cause crashes or undefined behavior. Enhanced validation now safely handles various object types while preserving full compatibility with native Error objects.

… of a non-error object

Codegen lowers EVERY `obj.errors` property read (receiver frequently `any`)
to the native `js_error_get_errors` accessor and then unconditionally OR-s
POINTER_TAG onto the result. The accessor blindly read `ErrorHeader.errors`
at byte offset +48, which is only valid for a genuine native error.

Applied to a regular user object, +48 is an unrelated property slot. Observed
in a deep async-dispatched config-resolution `for…of` over `obj.errors`: the
slot held NaN-boxed `undefined` (0x7FFC_0000_0000_0001); codegen OR-ed
POINTER_TAG to produce 0x7FFD_0000_0000_0001 — a handle-band id (raw=1), not a
heap array. `js_get_iterator` then treated it as an iterator whose `.next()`
routed to the small-handle dispatcher (no `next`) → undefined →
'TypeError: Iterator result is not an object'.

Fix: validate the receiver is a native error (object_type == OBJECT_TYPE_ERROR,
heap-plausible address) before reading the fixed slot. For any other receiver,
resolve `errors` as an ordinary own property and hand back its clean pointer
(or null, which the caller's re-tag turns into a not-iterable null receiver) —
matching what a generic dynamic property read would have produced.

Instruction-level diagnosis (lldb, debug bundle): the corrupt
0x7FFD_0000_0000_0001 entered js_get_iterator from cli_ts__li5 at the
`bl js_error_get_errors` → `orr POINTER_TAG` site; the receiver object had
object_type=0x1 (OBJECT_TYPE_REGULAR) with +48 = 0x7FFC_0000_0000_0001.
Verified end-to-end: relinking the bundle with the fixed runtime advances
`agents` past this wall (to an unrelated downstream error).

Tests: 2 new unit tests (regular object resolves real .errors property / null
when absent / rejects handle-band ptr; native AggregateError still uses the
fixed slot). perry-runtime lib: 1071 passed.
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

js_error_get_errors in perry-runtime is hardened against unsafe dereferences when the receiver is not a native Error object. The function now guards with null checks, heap-address plausibility tests, and object_type discrimination before accessing the fixed ErrorHeader.errors slot, falling back to generic property lookup otherwise. Two tests validate both paths.

Changes

js_error_get_errors Safety Fix

Layer / File(s) Summary
Guarded dispatch and tests
crates/perry-runtime/src/error.rs
js_error_get_errors replaces the unconditional ErrorHeader.errors read with null-pointer rejection, small/tagged-value plausibility checks, object_type == OBJECT_TYPE_ERROR gating for the fixed slot, and a generic property-lookup fallback that returns the cleaned pointer or null. Tests assert a plain object returns its real .errors property pointer and a native AggregateError still reads from the fixed slot.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A rabbit once leapt to a pointer too fast,
And landed on memory that shouldn't be passed.
Now null checks and type gates line the trail,
So only true Errors may walk past the veil.
🐇 Hop safely, dear pointer — no wild derefs today!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main fix: guarding js_error_get_errors against non-error receivers to prevent .errors field corruption in regular objects.
Description check ✅ Passed The description comprehensively covers the problem, root cause, fix, and tests. All major template sections are addressed with detailed technical context and verification results.
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/errors-accessor-guard

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

@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: 1

🧹 Nitpick comments (2)
crates/perry-runtime/src/error.rs (2)

1374-1378: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider interning the "errors" key string.

js_string_from_bytes allocates a fresh string on every non-error call. Since this is a hot path when iterating .errors on user objects, you could intern or cache the key to avoid repeated allocations. That said, this is a micro-optimization and the current approach is functionally correct.

🤖 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/error.rs` around lines 1374 - 1378, The
`js_string_from_bytes` call for the "errors" key allocates a fresh string on
every invocation in this hot path, which is inefficient. To fix this, intern or
cache the "errors" key string at module level or as a static/lazy-static
variable so that the same allocated string is reused across multiple calls to
js_object_get_field_by_name. Replace the inline js_string_from_bytes call with a
reference to the cached/interned key to eliminate repeated allocations.

1619-1655: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Tests placed in tostring_tests module.

These tests cover js_error_get_errors behavior, not to_string functionality. Consider moving them to a dedicated get_errors_tests module or renaming the containing module to something more general like error_accessor_tests. This is a minor organizational nit.

🤖 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/error.rs` around lines 1619 - 1655, The test
function get_errors_on_regular_object_reads_real_property_not_fixed_slot is
placed in the tostring_tests module, but it tests js_error_get_errors
functionality rather than to_string behavior, causing organizational
misalignment. Either move this test to a dedicated get_errors_tests module or
rename the containing tostring_tests module to something more general like
error_accessor_tests that accurately reflects its broader test coverage.
🤖 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/error.rs`:
- Around line 1360-1366: Update the comment near the is_plausible_heap_addr call
to accurately reflect that the validation logic differs from js_error_is_error,
not matches it. The comment should note that while js_error_is_error uses only
is_valid_obj_ptr for validation, this code uses the stricter
is_plausible_heap_addr which includes an additional is_above_handle_band check,
making it more defensive. Clarify in the comment that this is intentional and
more restrictive validation than what is_valid_obj_ptr alone provides.

---

Nitpick comments:
In `@crates/perry-runtime/src/error.rs`:
- Around line 1374-1378: The `js_string_from_bytes` call for the "errors" key
allocates a fresh string on every invocation in this hot path, which is
inefficient. To fix this, intern or cache the "errors" key string at module
level or as a static/lazy-static variable so that the same allocated string is
reused across multiple calls to js_object_get_field_by_name. Replace the inline
js_string_from_bytes call with a reference to the cached/interned key to
eliminate repeated allocations.
- Around line 1619-1655: The test function
get_errors_on_regular_object_reads_real_property_not_fixed_slot is placed in the
tostring_tests module, but it tests js_error_get_errors functionality rather
than to_string behavior, causing organizational misalignment. Either move this
test to a dedicated get_errors_tests module or rename the containing
tostring_tests module to something more general like error_accessor_tests that
accurately reflects its broader test coverage.
🪄 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: ebf815a4-519d-484a-bf01-ca32f26cd2d3

📥 Commits

Reviewing files that changed from the base of the PR and between 7d3f2d2 and ff74391.

📒 Files selected for processing (1)
  • crates/perry-runtime/src/error.rs

Comment on lines +1360 to +1366
if !crate::value::addr_class::is_plausible_heap_addr(addr) {
return std::ptr::null_mut();
}
// Native error objects carry `object_type == OBJECT_TYPE_ERROR` in
// their first u32; only those have the `errors` field at a fixed
// offset. (Matches the validation in `js_error_is_error`.)
let object_type = std::ptr::read(error as *const u32);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if is_plausible_heap_addr and is_valid_obj_ptr have the same semantics
# or if one is a superset of the other

# Find definitions of both functions
echo "=== is_plausible_heap_addr definition ==="
ast-grep --pattern 'fn is_plausible_heap_addr($$$) { $$$ }'

echo ""
echo "=== is_valid_obj_ptr definition ==="
ast-grep --pattern 'fn is_valid_obj_ptr($$$) { $$$ }'

echo ""
echo "=== All usages of is_plausible_heap_addr ==="
rg -n 'is_plausible_heap_addr' --type rust

echo ""
echo "=== All usages of is_valid_obj_ptr ==="
rg -n 'is_valid_obj_ptr' --type rust

Repository: PerryTS/perry

Length of output: 26574


🏁 Script executed:

# Get the function definitions for is_plausible_heap_addr and is_valid_obj_ptr
sed -n '140,200p' crates/perry-runtime/src/value/addr_class.rs

Repository: PerryTS/perry

Length of output: 2576


🏁 Script executed:

# Check js_error_is_error implementation
sed -n '729,750p' crates/perry-runtime/src/error.rs

Repository: PerryTS/perry

Length of output: 881


🏁 Script executed:

# Check is_above_handle_band definition and js_error_get_errors signature
rg -n 'fn is_above_handle_band' crates/perry-runtime/src/value/addr_class.rs -A 5

Repository: PerryTS/perry

Length of output: 312


🏁 Script executed:

# Get HANDLE_BAND_MAX value to understand the range
rg -n 'HANDLE_BAND_MAX' crates/perry-runtime/src/value/addr_class.rs -B 2

Repository: PerryTS/perry

Length of output: 1999


🏁 Script executed:

# Get full js_error_get_errors function to see complete context
sed -n '1332,1385p' crates/perry-runtime/src/error.rs

Repository: PerryTS/perry

Length of output: 2904


Update comment at line 1365: validation functions differ, not equivalent.

The comment claims this code "Matches the validation in js_error_is_error", but js_error_is_error (line 736) uses only is_valid_obj_ptr, while this code uses is_plausible_heap_addr. These have different semantics: is_plausible_heap_addr adds is_above_handle_band(addr) (addr >= 0x100000) on top of the heap-range check, making it stricter. Addresses in the handle band ([0x1000, 0x100000) on Linux) would pass is_valid_obj_ptr but fail is_plausible_heap_addr. The more defensive check here is sound, but the comment should reflect that the validations differ, not match.

🤖 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/error.rs` around lines 1360 - 1366, Update the
comment near the is_plausible_heap_addr call to accurately reflect that the
validation logic differs from js_error_is_error, not matches it. The comment
should note that while js_error_is_error uses only is_valid_obj_ptr for
validation, this code uses the stricter is_plausible_heap_addr which includes an
additional is_above_handle_band check, making it more defensive. Clarify in the
comment that this is intentional and more restrictive validation than what
is_valid_obj_ptr alone provides.

@proggeramlug
proggeramlug merged commit 59ed84d into main Jun 22, 2026
15 checks passed
@proggeramlug
proggeramlug deleted the fix/errors-accessor-guard branch June 22, 2026 11:50
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