Skip to content

Commit f6e15e9

Browse files
committed
fix(callgrind): don't consume minpops budget on SP-lower stack pops
On targets where calls don't move SP (AArch64 bl/blr, PPC b(c)l), SP-equal entry frames could get stuck beneath SP-lower sub-call frames once minpops was exhausted popping the latter, causing inverted call-graph edges and spurious recursion clones.
1 parent 5641fb6 commit f6e15e9

2 files changed

Lines changed: 29 additions & 4 deletions

File tree

CODSPEED-CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,13 @@ valgrind --tool=callgrind \
113113
--obj-skip=/lib/x86_64-linux-gnu/libpthread.so.0 \
114114
./your_program
115115
```
116+
117+
## Fixes
118+
119+
### ARM64: default to fallback LL/SC to fix intermittent livelocks (hangs)
120+
121+
**Fix**: On `arm64-linux`, the fallback (CAS-based) LL/SC implementation is now always enabled, as if `--sim-hints=fallback-llsc` were passed: `coregrind/m_machine.c` sets `arm64_requires_fallback_LLSC` unconditionally instead of only on Cavium/ThunderX autodetect. This matches what Valgrind already does unconditionally on riscv64.
122+
123+
**Motivation**: Programs run under `valgrind --tool=callgrind` on ARM64 would intermittently livelock at 100% CPU (observed in CodSpeed macro-runner CI and by customers, on both this fork and upstream 3.25.1/3.26.0). Root-caused 2026-07-09 on Cortex-A72 (AWS a1) with live captures: an AArch64 conditional branch always ends a VEX superblock, so a guest CAS loop (`ldaxr; cmp; b.ne; stxr; cbnz` — e.g. glibc's `__aarch64_cas4_acq` outline-atomics helper) has its `LDAXR` and `STXR` in two different translations, with hundreds of host instructions (tool helpers, dispatcher, event checks) executing inside the exclusive window. In the default (non-fallback) mode the guest `stxr` is passed through natively, so guest forward progress depends on the hardware exclusive monitor surviving that window. Usually it does; but in a sticky per-run state (observed live: an uncontended CAS retrying ~1M/s for minutes with zero page faults and ~6 context switches/s), the monitor is lost on every iteration and the guest never progresses. The fallback implementation records the LL value and implements SC as a real host CAS, removing the dependency on the hardware monitor entirely.
124+
125+
**Validation**: On a loaded Cortex-A72 host, `callgrind --cache-sim=yes ... ls -alh /etc` pinned to one core hung 4 times within 55 iterations before the fix and 0 times after (300 iterations); multithreaded `__atomic_fetch_add` stress (8 threads x 200k, inline LL/SC) produces exact counts; `memcheck/tests/atomic_incs` and `none/tests/arm64/atomics_v81` regression tests pass.

callgrind/callstack.c

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -478,13 +478,28 @@ Int CLG_(unwind_call_stack)(Addr sp, Int minpops)
478478
while( (csp=CLG_(current_call_stack).sp) >0) {
479479
call_entry* top_ce = &(CLG_(current_call_stack).entry[csp-1]);
480480

481-
if ((top_ce->sp < sp) ||
482-
((top_ce->sp == sp) && minpops>0)) {
483-
481+
/* A frame whose entry SP is strictly below the new SP has been left by
482+
* the return and is always unwound. On targets where the call
483+
* instruction does not move SP (AArch64 bl/blr, PPC b(c)l), a callee's
484+
* own *entry* frame records the caller's SP and therefore sits at SP
485+
* *equal* to the return target; such frames sit beneath the SP-lower
486+
* frames of any sub-calls the callee made. The minpops budget bounds
487+
* how many of these SP-equal frames a single return may pop (computed by
488+
* the ret_addr-matching logic in setup_bbcc). SP-lower pops must NOT
489+
* consume that budget -- otherwise sub-call frames exhaust it and the
490+
* SP-equal entry frame is left stuck on the stack, keeping the callee's
491+
* context active so the caller's continuation is mis-attributed to the
492+
* callee (inverted edges) and the callee's never-decremented recursion
493+
* depth fabricates spurious recursion clones. */
494+
if (top_ce->sp < sp) {
495+
unwind_count++;
496+
CLG_(pop_call_stack)();
497+
continue;
498+
}
499+
if ((top_ce->sp == sp) && minpops>0) {
484500
minpops--;
485501
unwind_count++;
486502
CLG_(pop_call_stack)();
487-
csp=CLG_(current_call_stack).sp;
488503
continue;
489504
}
490505
break;

0 commit comments

Comments
 (0)