[Bug] DistributedMutex flat combining crashes 100% under multi-threaded workloads on aarch64
Summary
DistributedMutex::lock_combine() crashes deterministically (100% reproduction rate) under multi-threaded workloads on aarch64 (ARM64) platforms. The same workload runs correctly on x86_64.
The crash occurs in the flat combining / CoalescedTask code path in DistributedMutex-inl.h. Replacing DistributedMutex with std::mutex eliminates the crash entirely.
The similar issue also found in CacheLib: https://github.com/facebook/CacheLib/issues/361
Environment
- Folly version: v2024.06.24.00
- Platform: aarch64 (AWS Graviton4 / ARM Neoverse V2)
Crash Symptoms
Two crash modes observed, both originating from the flat combining path:
Crash A: SIGILL (Illegal Instruction)
call_ in the Waiter struct's InlineFunctionRef jumps to a corrupted address. The callInline function pointer in storage_ was overwritten by a concurrent combiner thread writing to the overlapping metadata_ union fields. On aarch64, the corrupted address (0x47xxxx~0x6fxxxx) lands on the stack → illegal opcode → SIGILL.
Crash B: SIGSEGV (Invalid Pointer)
storage_ (first 16 bytes of Waiter) is corrupted by concurrent writes to waiters_/sleeper_ in the overlapping WakerMetadata. The this pointer captured in the lambda resolves to garbage → isValidSlab() fails → SIGSEGV.
Root Cause Analysis
The flat combining path in DistributedMutex-inl.h relies on non-atomic multi-byte reads/writes combined with timestamp-based spin heuristics to maintain mutual exclusion over a multi-purpose union. The Waiter struct (L260-310) reuses the same 24-byte region for three purposes:
metadata_(WakerMetadata:waker_,waiters_,sleeper_) — written by the waking/combiner threadfunction_/storage_viaInlineFunctionRef— the callable stored by the waiting thread- Return value — coalesced result written back by
TaskWithCoalesce::operator()
The invariant is: only one of these three uses is "active" at any given time. This invariant holds on x86 but breaks on aarch64.
Why it works on x86 but fails on aarch64
| Factor | x86_64 | aarch64 |
|---|---|---|
| Memory model | TSO — stores are globally visible in program order | Weak — stores can remain in store buffer, loads can be reordered |
48-byte function_ read |
Follows futex_.load(acquire), effectively ordered by TSO |
Compiles to multiple ldp instructions; acquire on futex_ does NOT fence subsequent non-atomic loads |
kScheduledAwaySpinThreshold = 200 (L155) |
rdtsc @ 2.55GHz → ~78ns → tight spin window |
cntvct_el0 @ ~1GHz → ~200ns → 2.5× wider window, changes preempted() hit rate |
| Race window | Narrow, masked by hardware ordering | Wide, amplified by both weak ordering and timer drift |
The code itself acknowledges the platform gap
At line 248:
// on x86, this gets optimized away to just a regular store, it might be
// needed on platforms where explicit acquire-release barriers are
// required for synchronization
//
// note that we release here at the end of the constructor because
// construction is complete here, any thread that acquires this release
// will see a well constructed wait node
futex_.store(futex, std::memory_order_release);
This comment shows the author was aware that non-x86 platforms need explicit barriers, but the subsequent code paths — particularly the TaskWithCoalesce union reuse and the lock_combine() return-value retrieval — do not add the necessary fences.
The critical crash path: lock_combine()
The entry point is lock_combine() (L1042-1078):
auto lock = dm.lock_combine([&] { /* user func */ return result; });
When the user's function is combined (executed by a remote combiner thread), the flow is:
- Waiting thread stores its callable into
Waiter::storage_(viaInlineFunctionRef) - Combiner thread reads
storage_, executes the callable, writes the return value back into the same union region viaTaskWithCoalesce::operator()(L479-486) - Waiting thread wakes and retrieves the return value via
std::move(task).get()(L1077)
On aarch64, step 2's writes may not be visible to step 3's reads due to missing acquire-release synchronization on the union memory itself.
All problematic locations
| Location | Code | Issue |
|---|---|---|
| L248-257 | futex_.store(futex, memory_order_release) |
Author-acknowledged: "might be needed on platforms where explicit acquire-release barriers are required" |
| L479-486 | TaskWithCoalesce::operator() |
Writes coalesced return value into union — no release barrier for the waiting thread to acquire |
| L972 | waiter->metadata_.sleeper_.exchange(kSleeping, acq_rel) |
The atomic exchange is properly ordered, but adjacent non-atomic union fields (waker_, waiters_) are not |
| L1042-1078 | lock_combine() |
Caller retrieves return value via task.get() without acquire fence after being woken |
| L1249-1273 | state.metadata_.waker_ / waiters_ |
Non-atomic reads race with combiner thread writes |
| L1387-1398 | Placement new Metadata{waker, ...} |
Overwrites union without ensuring prior function_/storage_ reads are complete |
Current Workaround
Replace DistributedMutex with std::mutex. This disables flat combining but eliminates the crash. The trade-off is reduced throughput under high lock contention (flat combining can provide 2-5× throughput improvement in contended scenarios).
Suggested Fix
- Add acquire-release barriers around the non-atomic union field accesses that participate in cross-thread communication (especially the
TaskWithCoalescewrite-back and thelock_combine()return-value read) - Make critical union fields atomic with appropriate memory ordering
- **Normalize **
kScheduledAwaySpinThresholdto wall-clock time instead of raw tick counts, to avoid behavioral drift across architectures - Conservative option:
#ifdefto disable flat combining on non-TSO architectures until the barriers are audited
Status
- Affected version: v2024.06.24.00 (confirmed)
- Latest main branch: same code, unchanged as of 2026-09-14
- Not fixed upstream
References
- Source file:
folly/synchronization/DistributedMutex-inl.h(1745 lines) - Flat Combining paper: Hendler, Incze, Shavit, Tzafrir. "Flat Combining and the Synchronization-Parallelism Tradeoff." SPAA 2010.
Source: facebook/folly