Repository navigation
Conversation
SmallHeapBlockT::GetRealAddressFromInterior narrowed the byte offset of an interior pointer within its block to ushort before shifting it down to an object index. A medium block is eight pages. With 4K pages that is 32K and every offset fits. On Apple arm64 the page is 16K, the block is 128K, and any offset past 64K lost its top bit and resolved to the object 64K lower in the same block. The marker reaches objects on the native stack through this function. HeapBlockMap32::MarkInterior sets the candidate's own mark bit first, then resolves and queues the object it believes the pointer belongs to. For a medium object in the upper half of its block that queued the wrong object, and the real one kept its mark bit without ever being scanned. When the same object was later reached through an exact heap pointer it was skipped as already marked, so everything referenced only from it was swept while still live. Async function and generator frames live on the heap as medium objects and the interpreter executes inside them, so a collection during an async function left its locals, including the await object, pointing at freed memory after the next resume. An embedder saw this as crashes in SetInlineSlot, StrictEqual, instanceof and JSON building on native Apple Silicon only; x64 and Rosetta use 4K pages and never truncate. The sibling GetAddressBitIndex was already fixed for this in the Apple Silicon page size change (015b05d) by shifting before narrowing. Do the same here. Co-Authored-By: Claude Fable 5.1 <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
SmallHeapBlockT::GetRealAddressFromInteriornarrows the byte offset of an interior pointer within its heap block toushortbefore shifting it down to an object index. A medium block isPageCount = 8pages, so the largest offset is 32767 with 4K pages but 131071 with the 16K pages used on Apple Silicon (AutoSystemInfo::PageSize = 16384since 015b05d). Any offset past 64K silently loses its top bit and the function returns the object 64K lower in the same block.This is reached from conservative stack scanning:
HeapBlockMap32::MarkInteriorsets the candidate's own mark bit first, then resolves the pointer and queues the resolved object. For a medium object in the upper half of its block the wrong object gets queued, and the real one keeps its mark bit without ever being scanned. When the marker later reaches it through an exact heap pointer,MarkInternalreports it as already marked, so nothing it references gets marked. The sweep then frees live objects.Async function and generator frames live on the heap as medium objects, and the interpreter executes inside them, so a collection during an async function leaves its locals, including the function's await object, pointing at freed memory after the next resume. In an embedding this showed up on native Apple Silicon as
DynamicObject::SetInlineSlotwriting through a null type,StrictEqualandinstanceofon zeroed objects, andJSONStringBuilderlength mismatches. The same binary under Rosetta, and x64 Linux and Windows, never hit it because their blocks are 32K.The sibling
GetAddressBitIndexwas already fixed for the same overflow in 015b05d by shifting before narrowing. This change does the same here: keep the byte offset asuint, shift, then take theushortindex.Reproduction
On an arm64 macOS build of
ch(./build.sh --no-jit --test-build), an async function with enough locals to put its frame in a medium block, called enough times that some frames land in the upper half of their block, with allocation pressure before the firstawait:medium_frame.js
Before the fix, every run reports a local whose object was freed and reused, for example
FAIL corrupt v0 in call 138: "4132", or crashes under-RecyclerStress. After the fix it printsok 600, including under-RecyclerStressand-RecyclerVerifyMark.Testing
test/runtests.pyon arm64 macOS (Test build,disable_jitvariant): 1927 passed, 29 failed. The 29 are the Intl, Unicode and timezone tests that need ICU, which these macOS builds do not embed, and the failure set is identical with and without this change.-RecyclerStressand-RecyclerVerifyMark.🤖 Generated with Claude Code