Skip to content

Bytecode cache: return null from Encoder::release when the payload allocation fails - #512

Open
robobun wants to merge 1 commit into
mainfrom
farm/36729027/bytecode-encoder-release-try-malloc
Open

Bytecode cache: return null from Encoder::release when the payload allocation fails#512
robobun wants to merge 1 commit into
mainfrom
farm/36729027/bytecode-encoder-release-try-malloc

Conversation

@robobun

@robobun robobun commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • Encoder::release allocates the final contiguous payload with MallocSpan<uint8_t, VMMalloc>::malloc(size) and copies every page into it (runtime/CachedTypes.cpp). VMMalloc::malloc is crash-on-failure by contract, so the result is never checked.
  • On Windows release builds the contract does not hold. bmalloc's BCRASH() compiles as a call through a null function pointer, clang treats it as unreachable and deletes the null check in bmalloc::api::malloc, so fastMalloc returns null on OOM (BAssert.h: use __builtin_trap for BCRASH on clang-cl #316 has the disassembly). The encoder then does memcpy into a null buffer. Sentry BUN-4RGJ: bun 1.4.0 Windows x64, JSC::Encoder::release -> WTF::memcpySpan -> memcpy, fault address 0x20, reached from the embedder's compile-cache thread.

Fix

  • Allocate the payload with tryMalloc. If it fails, set error = BytecodeCacheError::StandardError(ENOMEM) and return null.
  • Correct because release already returns null in two cases (no page, failed mapped write), and every caller handles it: encodeCodeBlock, encodeFunctionCodeBlock, encodeBuiltinFunction return the RefPtr as is, and jsc.cpp plus the embedder check for null. A module that cannot be cached is skipped. The payload is the largest single allocation the encoder makes, so it is the one most likely to fail under memory pressure. The page allocations keep the crash-on-failure malloc.
  • Verified: CachedTypes.cpp compiles with the change (clang++ -fsyntax-only -std=c++23 against the JSC headers). An OOM path cannot be exercised by a unit test here.

Background

  • The bytecode cache serializes unlinked code blocks into one buffer that a later run maps or reads instead of parsing the source again. Encoder writes into page-sized chunks and release concatenates them.
  • VMMalloc is FastMalloc. FastMalloc::malloc crashes on failure, FastMalloc::tryMalloc returns null. MallocSpan::tryMalloc wraps the latter and yields an empty span on failure.
  • BytecodeCacheError::StandardError(int errno) is the existing way release reports an OS failure to the caller.
  • BAssert.h: use __builtin_trap for BCRASH on clang-cl #316 fixes the BCRASH() miscompile itself. With both changes, a failed payload allocation skips the cache, and any other OOM inside JSC on Windows crashes at the allocator (0xbbadbeef) instead of at a random null write.

…location fails

Encoder::release allocated the final contiguous payload with
MallocSpan<uint8_t, VMMalloc>::malloc, which is crash-on-failure by
contract. On Windows release builds that contract does not hold:
bmalloc's BCRASH() is compiled as a call through a null function
pointer, clang treats that as unreachable and deletes the null check in
bmalloc::api::malloc, so fastMalloc returns null on OOM. The encoder
then memcpy'd every page into a null buffer (bun 1.4.0 Windows x64,
Sentry BUN-4RGJ, fault address 0x20 inside memcpy).

The payload is the largest single allocation the encoder makes, and a
bytecode cache is optional. Allocate it with tryMalloc and, if that
fails, set BytecodeCacheError::StandardError(ENOMEM) and return null.
Every encodeCodeBlock, encodeFunctionCodeBlock and encodeBuiltinFunction
caller already handles a null result (release already returns null for
a missing page and for a failed mapped write), so an embedder skips the
cache for that module instead of crashing.

The BCRASH() miscompile itself is fixed separately (#316).
@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 27 days. After that, they cost $0.25 per reviewed file.

Or wait 6 minutes for your next included review.

View limit details

Limit details: You’ve used all 5 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cb5f9f1e-a205-48c8-9656-85a099b06f38

📥 Commits

Reviewing files that changed from the base of the PR and between c148a12 and c217c57.

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

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.

LGTM — small defensive change that swaps a crash-on-fail allocation for a fallible one and reports ENOMEM through the existing error path.

What was reviewed:

  • MallocSpan::tryMalloc returns an empty span on failure and AllocSpanMixin::operator! tests data(), so if (!buffer) is correct.
  • release() already returns nullptr for the no-page and releaseMapped failure cases; all three call sites (encodeCodeBlock, encodeFunctionCodeBlock, encodeBuiltinFunction) propagate a null RefPtr safely.
  • BytecodeCacheError::StandardError(int) matches the pattern already used a few lines below in releaseMapped.
Extended reasoning...

Overview

The PR changes one allocation in Encoder::release (Source/JavaScriptCore/runtime/CachedTypes.cpp) from MallocSpan<uint8_t, VMMalloc>::malloc(size) to tryMalloc(size), and adds a null check that sets error = BytecodeCacheError::StandardError(ENOMEM) and returns nullptr. Net diff is +6/-1 lines including a comment.

Security risks

None. This is a defensive OOM guard on the bytecode cache's final payload buffer. The bytecode cache is an optional optimization; skipping it on allocation failure has no correctness or security impact. No user-controlled data flows change.

Level of scrutiny

Low. The change is mechanical and mirrors the existing failure path in the same function: releaseMapped at line ~742 already does error = BytecodeCacheError::StandardError(errno); return nullptr;. I verified in MallocSpan.h that tryMalloc returns a default-constructed span on failure, and in AllocSpanMixin.h that operator! checks m_span.data(), so the if (!buffer) test is sound. BytecodeCacheError has an operator=(const StandardError&) overload, and errno.h constants are already in scope (used a few lines below).

Other factors

All three callers of encoder.release(error)encodeCodeBlock, encodeFunctionCodeBlock, encodeBuiltinFunction — return the RefPtr<CachedBytecode> directly or explicitly null-check it (line 4197), so no caller will dereference the new null return. The motivating Sentry crash (Windows OOM → memcpy into null) is plausibly addressed by bailing out before the copy loop. No outstanding reviewer comments; the only prior timeline entry is a rate-limited CodeRabbit notice.

@github-actions

Copy link
Copy Markdown

Preview Builds

Commit Release Date
c217c57b autobuild-preview-pr-512-c217c57b 2026-08-24 18:08:46 UTC

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.

2 participants