common/race: fix darwin -race crashes from file mmaps in the TSAN heap window - #21611
Merged
Merged
Conversation
…m unshadowed file mmaps go test -race on macOS flakily died with 'fatal error: runtime: split stack overflow' (a SEGV inside __tsan_read mangled by the runtime) or 'too many address space collisions for -race mode' in mdbx-heavy packages. Each in-mem test env reserved 16GiB of VA; with dozens of parallel testers the kernel places mdbx maps between Go heap arenas, inside TSAN's heap window [0x00c0..., 0x00e0...). racecalladdr accepts those addresses (it checks one coarse [racearenastart, racearenaend) interval) but shadow is only mapped per arena, so the first instrumented read of such a map faults. The runtime's own __tsan_map_shadow cannot repair this: Go-mode MapShadow tracks a monotonic mapped-shadow interval and silently skips requests inside it. Fix: common/race (compiled only under race && darwin, linked via blank imports from db/kv/mdbx and common/mmap) fills every unmapped gap in the heap window's shadow at init with zeroed MAP_FIXED anonymous mmaps, leaving existing arena shadow untouched, with the layout self-checked against a live heap allocation. One-time cost; covers any mapping the kernel ever places in the window. A per-env-open region walk was tried first and rejected: it serialized an O(VM-regions) scan into thousands of env opens and hung execution/tests. Also shrink InMem geometry to 1GiB for tests to remove the VA squatting that causes the arena-reservation collisions. Verified: EXEC3_PARALLEL=true go test -race ./execution/execmodule ./execution/state went from 6/6 fatal on main to 0/8 with the fix; execution/tests under race stays at ~9min.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR addresses flaky/crashing go test -race runs on macOS (darwin, Apple Silicon) caused by large file mmaps landing inside Go/TSAN’s heap window without corresponding mapped shadow memory. It adds a darwin+race-only initializer to pre-map missing TSAN shadow ranges and reduces MDBX in-mem test VA reservations to avoid exacerbating address-space collisions.
Changes:
- Add
common/racedarwin+race implementation that fills unmapped TSAN shadow gaps for the Go heap window, plus a targeted unit test. - Ensure the
common/raceside-effect init runs for relevant mappings by blank-importing it from MDBX and unix mmap codepaths. - Reduce default
InMem(tb!=nil)MDBX map size upper bound from 16GiB to 1GiB to mitigate macOS VA pressure during parallel tests.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
db/kv/mdbx/kv_mdbx.go |
Blank-import common/race; reduce default in-mem test map size to lower VA pressure under parallel test runs. |
common/race/shadow_fallback.go |
No-op stub for non-race && darwin builds to keep imports safe across platforms. |
common/race/shadow_darwin.go |
darwin+race TSAN-shadow hole-filling implementation via mach_vm_region + mmap(MAP_FIXED, ...), with runtime layout self-check. |
common/race/shadow_darwin_test.go |
darwin+race unit test asserting the layout self-check passes and shadow is mapped across the heap window. |
common/mmap/mmap_unix.go |
Blank-import common/race so mmap users also trigger shadow pre-mapping on darwin race builds. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The 1GB InMem cap (added to relieve parallel-test VA pressure under -race) overflowed BenchmarkPruneSmallBatches with MDBX_MAP_FULL. Benchmarks run sequentially, so they don't contribute to the parallel-env VA squat the cap targets; exempt *testing.B and keep the 16GB map for them.
…adow formula The mechanism analysis now lives in golang/go#80292 (filed with a deterministic reproducer), so the package comment shrinks to the invariant plus the issue link. map_shadow_holes takes shadow-space bounds computed by Go's mem2shadow, removing the C-side copy of the formula.
The txpool DB opens with a hard-coded 1TB geometry upper bound (txnprovider/txpool/assemble.go) unless cfg.MdbxDBSizeLimit overrides it, and the tester left that at zero. A 1TB VA reservation cannot fit below the Go race-mode heap window [0x00c0..., 0x00e0...) on darwin, so the kernel places it across the window - vmmap of a crashing run shows two 1TB txpool mdbx.dat maps starting 320MB above the window base. Each such mapping burns the runtime's race-mode arena hints (they are discarded permanently on collision), and a later arena request dies with "fatal error: too many address space collisions for -race mode". EXEC3_PARALLEL=true go test -race ./execution/engineapi: 6/6 fatal before this change, 6/6 green after. Linux is unaffected because its top-down mmap places the reservation near 0x7f..., far above the window. Flow the tester's existing MdbxDBSizeLimit (1GB default, the same value already applied to chaindata) into the txpool config.
new([16]byte) whose address only flows into a uintptr may be stack-allocated by escape analysis. The self-check also held for stack addresses (goroutine stacks live in the same race-mode arenas with mapped shadow), but a package-level holder makes the probe unconditionally a heap address instead of relying on that subtlety.
JkLondon
approved these changes
Jul 9, 2026
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.
Problem
go test -raceon darwin (Apple Silicon) flakily dies in mdbx-heavy packages — reproduced at 6/6 onmainwithEXEC3_PARALLEL=true go test -race ./execution/execmodule ./execution/stateon an M-series Mac — with either of:The same packages pass the Linux race CI legs, which long disguised this as an environment flake. It isn't — both fatals share one root cause.
Root cause (caught live in lldb)
Catching the original fault under lldb (before the Go runtime mangles it into "split stack overflow") shows:
[0x00c0…, 0x00e0…); shadow (shadow = app*2 + 0x2000_0000_0000) is mapped per heap arena.InMemgeometry upper bound); with dozens of parallel testers the kernel's bottom-up placement exhausts low VA and drops mdbx maps between Go heap arenas.racecalladdrvalidity filter checks one coarse interval[racearenastart, racearenaend)(min/max over arenas) — a sandwiched map passes the check with no shadow → the first instrumented read (txpoolfromDB) faults inside__tsan_read. The SEGV lands whileracecallis on the g0 stack, so the runtime dies with the misleading split-stack throw. The same squatting also makes heap-arena reservation collide repeatedly → the "too many address space collisions" fatal.Linux is unaffected because
mmap(NULL)there places file maps near0x7f…, far from the heap window, so they always fail theracecalladdrfilter and are simply (silently) invisible to TSAN.Two repair strategies were tried and rejected with evidence before the final one:
__tsan_map_shadowper mapping verifiably does nothing here: compiler-rt's Go-modeMapShadowtracks a monotonicctx->mapped_shadow_*interval and silentlyreturns for requests inside it — interior holes (precisely this case) are skipped. Confirmed in compiler-rt source and empirically (crash inside a region the call had "covered").unix.Mmap, locating regions viamach_vm_region+proc_regionfilename, fixed the crash but serialized an O(all-VM-regions) walk into every env open —execution/tests(thousands of env opens) went from 9 minutes to a 1h timeout.Fix
common/race(linked via blank imports fromdb/kv/mdbxandcommon/mmap; everything is compiled out unlessrace && darwin):[app*2+0x2000…, …)with zeroedMAP_FIXED|MAP_ANON|MAP_NORESERVEmappings, leaving existing arena shadow untouched. One-time cost of a few mmaps; zero per-open cost; covers every file mapping the kernel ever places in the window — current or future, mdbx or otherwise. Zero shadow is valid "no prior access" TSAN state, so races on such mappings also become detectable where they were previously fatal — for plain reads/writes; Go atomics on such mappings would still fault, since meta shadow is not pre-mapped.Plus:
InMemtest geometry 16GiB → 1GiB upper bound (only when atesting.TBis supplied, and not for benchmarks, which run sequentially and can need the full map). Unit tests never approach 1GiB per env; this removes the TB-scale VA squatting that pushes file maps into the heap window in the first place and is what triggers the "address space collisions" fatal.Verification
EXEC3_PARALLEL=true go test -race ./execution/execmodule ./execution/state(sequential, quiet M-series machine):main6/6 fatal → this branch 0/8.EXEC3_PARALLEL=true go test -race ./execution/testscompletes in normal time (the rejected per-open design hung it — kept as a regression gate).go test -race ./common/race: asserts the layout self-check and that shadow is actually mapped across the whole window.EXEC3_PARALLEL=true go test -race ./execution/...green — re-verified after merging main (2026-07-08,-count=1: 52 ok / 0 fail);make lintclean;make erigon integrationbuilds; stubs cross-compile (linux/windows)../execution/engineapidying 6/6 with the "address space collisions" fatal — a sibling mechanism, not a regression of this fix: the txpool DB's hard-coded 1TB geometry (txnprovider/txpool/assemble.go) cannot fit below the 768GiB heap-window base on darwin, so each tester node's txpool map spanned the window (caught live in vmmap: two 1TBtxpool/mdbx.datmaps starting 320MB above the window base) and burned the runtime's 32 race-mode arena hints, which are discarded permanently on collision; the fatal then hits a later innocent allocation. Fixed by flowing the tester's existing 1GBMdbxDBSizeLimitinto the txpool config: 6/6 fatal → 6/6 green. Linux is unaffected (top-down mmap places the reservation near0x7f…).TDD note: natural occurrence depends on kernel VM placement (hence the flakiness), but the crash is deterministically reproducible — golang/go#80292 carries a standalone ~50-line reproducer (mmap with an address hint into the heap window + heap ballast + one instrumented read, 100% fatal). In-repo, a deterministic crash test would need a separate crashing subprocess (the fault kills the whole test binary), so the unit test pins the fix's load-bearing properties (layout constants, window coverage) and the repetition harness above is the end-to-end gate; each design iteration was validated live in lldb.
This is arguably a Go runtime/TSAN deficiency (coarse interval in
racecalladdr, interior-skip in Go-modeMapShadow) — reported upstream as golang/go#80292 with a deterministic standalone reproducer; this PR makes erigon's macOS race runs work today.Forensics trail: first reported as a suspected environment flake on #21605 (#21605 (comment)).