Skip to content

CachedTypes: reject out-of-range offsets instead of reading past the buffer - #368

Open
robobun wants to merge 1 commit into
mainfrom
farm/cbc4ae8e/bytecode-decoder-bounds
Open

CachedTypes: reject out-of-range offsets instead of reading past the buffer#368
robobun wants to merge 1 commit into
mainfrom
farm/cbc4ae8e/bytecode-decoder-bounds

Conversation

@robobun

@robobun robobun commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Bun's node:vm exposes decodeCodeBlock() to user-provided bytes via the cachedData option. The decoder derives pointers from self-relative offsets stored in the payload and bounds-checks them only via debug ASSERTs, so a truncated or corrupted buffer reads wild heap memory in release (SIGSEGV at a wild address) and trips Decoder::offsetOf in debug:

ASSERTION FAILED: addr >= cachedBytecodeSpan.data() && addr < std::to_address(cachedBytecodeSpan.end())
vendor/WebKit/Source/JavaScriptCore/runtime/CachedTypes.cpp(314) : ptrdiff_t JSC::Decoder::offsetOf(const void *)

Under USE(BUN_JSC_ADDITIONS):

  • Decoder gains a latched m_failed flag.
  • offsetOf() and ptrForOffsetFromBase() bounds-check against the span and set m_failed instead of asserting.
  • CachedPtr::decode() bails out once the decoder has failed instead of dereferencing an out-of-range buffer().
  • GenericCacheEntry::decode() returns false for an unknown or eval tag rather than RELEASE_ASSERT_NOT_REACHED.
  • decodeCodeBlockImpl / decodeSourceCodeKey / isCachedBytecodeStillValid reject spans shorter than a CacheEntry before the bit_cast, and check decoder->failed() before trusting the result.

This is defense in depth behind the envelope Bun wraps around node:vm cachedData (oven-sh/bun#32839). It does not attempt to harden every decode path against arbitrary corruption; the remaining VariableLengthObject::buffer() callers that read payload bytes directly are still only reached with a hash-verified payload on the Bun side. It also tightens the short-buffer behavior for the other callers of these entry points (bun build --bytecode cache files).

…buffer

Bun's node:vm exposes decodeCodeBlock() to user-provided bytes via the
cachedData option. The decoder derives pointers from self-relative
offsets stored in the payload and bounds-checks them only via debug
ASSERTs, so a truncated or corrupted buffer reads wild heap memory in
release (SIGSEGV) and trips Decoder::offsetOf in debug.

Under USE(BUN_JSC_ADDITIONS):

- Decoder gains a latched m_failed flag.
- offsetOf() and ptrForOffsetFromBase() bounds-check against the span
  and set m_failed instead of asserting.
- CachedPtr::decode() bails out once the decoder has failed instead of
  dereferencing an out-of-range buffer().
- GenericCacheEntry::decode() returns false for an unknown/eval tag
  rather than RELEASE_ASSERT_NOT_REACHED.
- decodeCodeBlockImpl / decodeSourceCodeKey / isCachedBytecodeStillValid
  reject spans shorter than a CacheEntry before the bit_cast, and check
  decoder->failed() before trusting the result.

This is defense in depth behind the envelope Bun wraps around node:vm
cachedData; it does not attempt to harden every decode path against
arbitrary corruption.
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 6 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ee866627-a8b5-4bca-a97c-2a44894f03e4

📥 Commits

Reviewing files that changed from the base of the PR and between e6063b0 and 1372e68.

📒 Files selected for processing (2)
  • Source/JavaScriptCore/runtime/CachedTypes.cpp
  • Source/JavaScriptCore/runtime/CachedTypes.h

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

@claude claude 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.

I didn't find any bugs, but this is security-sensitive hardening of the bytecode decoder against untrusted node:vm cachedData input, and the PR explicitly scopes which paths are hardened vs. left to the Bun-side hash envelope — a human should sign off on that boundary.

Checked: the new bounds checks in offsetOf/ptrForOffsetFromBase mirror the existing ASSERT conditions and return in-span sentinel values; CachedPtr::decode bails before dereferencing get() when failed() latches; the three entry points reject short spans before the bit_cast and re-check failed() after decode. All changes are gated behind USE(BUN_JSC_ADDITIONS).

Extended reasoning...

Overview

The PR adds defensive bounds-checking to JSC's CachedTypes bytecode decoder under USE(BUN_JSC_ADDITIONS). It introduces a latched m_failed flag on Decoder, converts debug-only ASSERTs in offsetOf() and ptrForOffsetFromBase() into runtime checks that set the flag and return safe sentinel values, adds a bail-out in CachedPtr::decode(), replaces two RELEASE_ASSERT_NOT_REACHED() sites in GenericCacheEntry::decode() with return false, and adds minimum-size checks plus post-decode failed() checks to the three top-level entry points (decodeCodeBlockImpl, decodeSourceCodeKey, isCachedBytecodeStillValid).

Security risks

This is explicitly security-relevant: it hardens a code path that Bun exposes to user-provided bytes via node:vm's cachedData option, where the current release build reads wild heap memory on a corrupted/truncated buffer. The PR is a partial hardening — the description notes that other VariableLengthObject::buffer() callers still rely on Bun's hash-verified envelope. Whether the chosen boundary (harden CachedPtr::decode and the entry points, leave CachedArray and direct buffer() reads alone) is the right defense-in-depth line is a design judgment that should be confirmed by a maintainer familiar with the Bun-side envelope in oven-sh/bun#32839.

Level of scrutiny

High. This touches JavaScriptCore's bytecode cache decoder — memory-safety code on a path reachable from untrusted input. Even though every change is behind #if USE(BUN_JSC_ADDITIONS) and the individual edits are small and mechanical (mirror the existing assert condition, latch a flag, return early), the correctness of the sentinel returns (e.g. ptrForOffsetFromBase returning span().data() on failure, offsetOf returning 0) depends on every caller either checking failed() before using the result or being unreachable with untrusted data. That's the kind of invariant a human reviewer should confirm.

Other factors

No prior human reviews on the PR (only a rate-limited CodeRabbit stub). The bug-hunting system found no issues. The changes look internally consistent — minimumCacheEntrySize uses std::max over the two concrete CacheEntry<...> sizes, the failed() checks are placed after each decode call, and the non-Bun build is unaffected. But given the security surface and the explicit "does not attempt to harden every decode path" caveat, deferring to a human is the right call.

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