Conversation
This comment has been minimized.
This comment has been minimized.
CI Test ResultsRun: #36020394834 | Commit:
Status Overview
Legend: ✅ passed | ❌ failed | ⚪ skipped | 🚫 cancelled Summary: Total: 32 | Passed: 32 | Failed: 0 Updated: 2026-09-24 15:47:46 UTC |
a78aa75 to
36ff97c
Compare
36ff97c to
09d5f7d
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 63de2f9c71
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (nm->isFrameCompleteAt(walk_pc.raw())) { | ||
| const void* epilogue_pc = walk_pc.raw(); | ||
| if (depth == 1 && frame.unwindEpilogue(nm, (uintptr_t&)epilogue_pc, sp, fp)) { | ||
| walk_pc.setExactAddress(epilogue_pc); |
There was a problem hiding this comment.
Preserve return-address state after arm64 JIT unwinding
On supported arm64, unwindEpilogue, unwindPrologue, and unwindStub assign pc from StackFrame::link(), which is the caller's raw return address; this records it as exact, so the following native resolution and DWARF lookup do not subtract one byte. A sample that leaves a generated frame into a native caller whose call site ends at a symbol/FDE boundary is therefore still attributed to the following symbol/row (the same classification is repeated at lines 676 and 732). Mark these outputs as return addresses, or have the unwind helpers report whether they already adjusted the PC.
AGENTS.md reference: AGENTS.md:L199-L203
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
walkVM fed the raw walking pc to findLibraryByAddress, findFrameDesc and the DW_REG_PLT stub-offset test. Past the leaf that pc is a return address, so a call that is the last instruction of its caller selected the following function's CFA row, derived a sender sp from it, and could miss a MARK_THREAD_ENTRY sitting on the caller -- the same defect already fixed in StackWalker::walkFP/walkDwarf, on the path CSTACK_DEFAULT actually resolves to. Track per-frame whether the pc came out of a return-address slot and route the range-based lookups through attributionPC(). Exact-address consumers (isContReturnBarrier, isContEntryReturnPc, isEntryFrame), the DW_PC_OFFSET arithmetic and the no-progress guard keep the raw pc, and a signal-frame CIE suppresses the adjustment exactly as it does in walkDwarf. resolveNativeFrameForWalkVM was using one address for two jobs. It now takes the attribution address for findLibraryByAddress/binarySearch while the emitted pc_offset keeps deriving from the raw pc, so the remote-symbolication wire value is unchanged and no cross-team contract moves. unwindPrologue/unwindEpilogue/unwindStub are left alone: x86_64 already folds the adjustment into the pc they return and does so inconsistently (the isFrameComplete branch omits it) while aarch64 returns it raw, so their results are flagged as non-return-addresses and cannot be adjusted twice. Unifying that contract also fixes the exact-address comparisons those helpers currently break on x86_64, and is left to its own change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…resolution resolveNativeFrameForWalkVM uses two addresses for two jobs: range lookups key off the attribution address so a call that is the last instruction of its caller still resolves to the caller, while the emitted pc_offset keeps deriving from the raw pc because its meaning is a wire contract rather than a local detail. Neither half was gated by a test. Both are now pinned at a synthetic zero-gap symbol boundary, the shape where the two addresses disagree: flagging the pc as a return address must move the resolved symbol from the following function back to the caller, and must leave the emitted offset untouched. A third case pins that an address well inside a function resolves identically either way, so the adjustment cannot be read as a blanket shift. Both assertions were mutation-checked -- reverting the lookup to the raw pc fails the boundary case alone, and deriving pc_offset from the attribution address fails the wire case alone. The fixtures publish synthetic CodeCaches through a new test-only entry point rather than the test binary's own symbols, which is what makes a controlled zero-gap boundary possible at all and keeps these tests free of the GNU-as/ELF-CFI and updateSymbols() dependencies that confine returnAddressAttribution_ut to Linux. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
walkVM tracked "is this pc a return address?" in a bool sitting beside the pc, so the two could drift: a newly added `pc = ...` inherited whatever the previous frame had set, silently attributing the next lookup to the wrong address. That is the exact hazard WalkPc was introduced for in walkFP/walkDwarf, and walkVM is the default cstack path, so it is the one that most needed it. walkVM now drives a WalkPc. Every pc source states what it produced -- setReturnAddress for a saved-pc slot or link register, setRecoveredPc for a DWARF row, setExactAddress where the arch unwind helpers already folded the adjustment in -- and consumers read raw() or attribution() explicitly. Adding a pc source is now a compile-time decision instead of something a reviewer has to notice. Threading the seed through surfaced a real gap: walkVM is entered either from a ucontext, whose pc is the exact interrupted address, or from callerPC(), which is a genuine return address on every architecture where CALLER_PC_IS_RETURN_ADDRESS holds. Both arrived as a bare pointer and were treated as exact, so the callerPC() entry never got the adjustment it needed. The private overload now takes that distinction from its caller, the same way walkFP and walkDwarf seed themselves. No behaviour change otherwise: the mutator chosen at each site reproduces the flag that site already set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… the call walkVM's public entry point built its two starting states inline, so the pc and the flag describing it were separate arguments chosen a few tokens apart in two near-identical call expressions. Nothing bound them together, which is the same drift the walk itself now avoids by carrying a WalkPc. Both states are one named value instead. walkVMSeed() decides where a walk starts -- register set and the nature of the pc together -- and the entry point delegates without a branch of its own. The frame no longer outlives the decision: its pc/sp/fp are copied out as scalars, which is all the callee ever used, and the ucontext they point into outlives the walk regardless. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three review follow-ups. addLibraryForTest shipped in every build. Every other test hook in this codebase is either behind #ifdef UNIT_TEST or reached through a friend *TestAccessor, so a publicly callable mutator for the process-wide library set was both inconsistent and present where nothing should be able to call it. It is now compiled only into gtest binaries, which is where its one caller lives. prev_native_pc lost its last reader when the walk started carrying the previous frame as a WalkPc; the declaration stayed behind. Unused-variable warnings are not errors here, so nothing caught it. RemoteSymbolication.md still described the two-argument resolveNativeFrameForWalkVM. It now names the third parameter and says what it selects -- lookups move to the attribution address, the emitted pc_offset does not -- since the wire value being unchanged is the part a reader of that document needs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The three arch unwind helpers were all treated as having already applied the attribution adjustment. That holds on x86_64, which subtracts one inside the helper, but not on aarch64: every branch walkVM can reach there assigns the link register or a saved-pc slot, both raw return addresses. The one aarch64 branch that does subtract is guarded by `&pc == &this->pc()`, true only on the AsyncGetCallTrace path where the caller passes the frame's own pc rather than a local, so walkVM never takes it. A sample leaving a generated frame for a native caller whose call is the last instruction before a symbol or FDE boundary was therefore still attributed to whatever follows, on arm64, for exactly the frames this change set out to fix. recordUnwoundPc() names the difference in one place. x86_64 behaviour is unchanged; the distinction disappears once the helpers agree on a contract. Reported by Codex review on #813. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
33f8634 to
77538df
Compare
Addresses PROF-15955.
Summary
HotspotSupport::walkVMfed the raw walking pc tofindLibraryByAddress,findFrameDescand theDW_REG_PLTstub-offset test. Past the leaf that pc is a return address, so a call that is the last instruction of its caller selected the following function's CFA row, derived a sender sp from it, and could miss aMARK_THREAD_ENTRYsitting on the caller. Same defect #786 fixed inStackWalker::walkFP/walkDwarf— except this is the pathCSTACK_DEFAULTactually resolves to whenever the JVM exposes VMStructs on Linux, so it is the common production configuration rather than a fallback.walkVM now carries the pc and whether it came out of a return-address slot as a
single
WalkPcvalue — the type #786 introduced inwalkFP/walkDwarfforexactly this hazard — and routes the range-based lookups through its
attribution(). Every pc source states what it produced (setReturnAddress,setRecoveredPc,setExactAddress,setSeed), so adding one is a compile-timedecision rather than something a reviewer has to catch.
Adopting it surfaced a second defect, now also fixed here: walkVM is entered
either from a ucontext (pc is the exact interrupted address) or from
callerPC()(a genuine return address whereverCALLER_PC_IS_RETURN_ADDRESSholds, i.e. everywhere but aarch64). Both arrived as a bare pointer and were
treated as exact, so the
callerPC()entry never got the adjustment. Theprivate overload now takes that distinction from its caller, the same way
walkFP/walkDwarfseed themselves.Which pcs get adjusted, and which deliberately do not
findLibraryByAddress,findFrameDescDW_REG_PLTstub-offset testisContReturnBarrier,isContEntryReturnPc,isEntryFrameDW_PC_OFFSETarithmeticDW_OP_breg<PC>names the pc register valueinDeadZoneisFrameCompleteAt,findScopeOffset(PcDesc)A signal-frame CIE suppresses the adjustment exactly as it does in
walkDwarf(pc_is_ra = !f.isSignalFrame()).Every site in walkVM that writes
pc— 16 of them — sets the flag. Two were missing from the original enumeration in the ticket: both in-loopanchor->getFrame(pc, sp, fp)calls silently rewritepctolastJavaPC(), which is a return address.The wire format does not move
resolveNativeFrameForWalkVMwas using one address for two jobs: range lookups and the emitted remote-symbolicationpc_offset. It now takes the attribution address forfindLibraryByAddress/binarySearchwhilepc_offsetkeeps deriving from the raw pc, so the emitted wire value is byte-identical.This matters for review: it means this PR does not depend on the unresolved cross-team question about
pc_offsetsemantics raised in #786. It is effectively option (b) from that PR's own list of options, applied locally.Two findings that correct the ticket's premises
The suspected PcDesc double-subtraction bug does not exist. PROF-15955 flagged that x64's ad hoc
-1being re-fed intoisFrameCompleteAt/findScopeOffsetmight be a second latent bug. It is not:findScopeOffset(vmStructs.cpp:798) is a ceiling search — on an exact miss it returns the first PcDesc with_pc >= pc_offset— and HotSpot records PcDescs at return addresses, soRA-1andRAresolve to the same entry.isFrameCompleteAtis a monotone threshold. Both absorb the decrement, so the PcDesc sites are safe on the raw pc.There is a different, real x86_64 bug — filed as PROF-16033, not fixed here.
unwindPrologue/unwindEpilogue/unwindStubfold-1into the pc they return on x86_64 but not on aarch64, so on x86_64 the three exact-equality comparisons above always fail for pcs arriving through those helpers: continuation-boundary and entry-frame detection silently do not fire. The adjustment is also branch-inconsistent (unwindPrologue'sisFrameCompletebranch athotspotStackFrame_x64.cpp:155omits it), which is why a correct flag cannot be threaded without changing those helpers' contract.Those three call sites are therefore flagged
pc_is_ra = falsehere — conservative, cannot double-adjust, and no behaviour change on either arch. The arch files are untouched. Fixing them properly means touching x86_64/aarch64 pattern-matching code shared with the legacygetJavaTraceAsync/AsyncGetCallTrace path, which is a different blast radius and belongs in its own change.Deliberately left untouched
Per the ticket's step 5, calling these out so they do not read as oversights:
CodeHeap::findNMethod,isFrameCompleteAt,findScopeOffset→ PcDesc) stay on the raw pc — correct by HotSpot's own convention, confirmed above.unwindPrologue/unwindEpilogue/unwindStub— PROF-16033.unwindCompiled— same inconsistency, but only reachable from the AGCT path, not walkVM. Covered by PROF-16033.PerfEvents::walkKernel— carries a related defect but is outside PROF-15955's scope; it is a pure consumer of the kernel's ring buffer and would be a much smaller separate change.Test plan
buildDebug/buildRelease— cleangtestDebug— 67 test binaries, 0 failureswalkVmAttribution_ut.cppgates the address split (new, 3 tests)resolveNativeFrameForWalkVMuses two addresses for two jobs, and both halvesare now pinned at a synthetic zero-gap symbol boundary, the shape where the two
disagree:
ReturnAddressAtZeroGapBoundaryResolvesToTheCallerPcOffsetStaysDerivedFromTheRawPcpc_offsetuntouched — the attribution address is a lookup detail and must not reach the wireAddressInsideAFunctionResolvesTheSameEitherWayBoth assertions were mutation-checked against the production code:
pc_offsetderived from the attribution addressEach mutation kills exactly its intended test, so the coverage is targeted
rather than incidental.
The fixtures publish synthetic
CodeCaches via a new test-onlyLibraries::addLibraryForTest()(matching the existing*ForTestidiom inprofiler.h,os.h,callTraceHashTable.h). That is what makes a controlledzero-gap boundary possible; using the test binary's own symbols cannot produce
one. It also keeps these tests free of the GNU-as/ELF-CFI and
updateSymbols()dependencies that confinereturnAddressAttribution_uttoLinux, so they run on macOS dev builds too.
Still ungated: the 21 per-frame
WalkPcmutator calls inside walkVM. TheWalkPcconversion removes the drift hazard (a pc can no longer be assignedwithout restating its nature) but does not prove each individual choice is the
right one.
Reaching them from a test is not viable in-process, which I confirmed rather
than assumed:
walkVMcallsVMThread::current()unconditionally, which asserts_jvm_thread.isKeyValid()— a key only created byJVMThread::initialize(), whichneeds JNI into a live JVM. In a debug gtest that aborts; with the profiler's SIGSEGV
handler installed it becomes an infinite signal loop. Defeating that assert would mean
disabling the very check that says the call is invalid, so I did not.
The coupling is the real problem, and it is now tracked as PROF-16059: walkVM's
dwarf_unwindblock andwalkDwarf's loop body are two copies of one algorithm(22 of
walkDwarf's 33 code lines appear verbatim in walkVM's 56), and extractingthe shared step yields something testable with synthetic
CodeCaches — the mechanismthis PR's new tests already use. That ticket blocks PROF-16033, which has no way to
verify itself for the same reason.
Reviewers: the judgement calls worth pushing back on are (a) the
raw-vs-attribution split in the table above, (b) the new test-only entry point
on
Libraries, (c)setExactAddressas the right choice where the arch unwindhelpers already fold in their own adjustment (see PROF-16033), and (d) whether
the remaining walkVM-level gap should block merge.
🤖 Generated with Claude Code