Conversation
unwindStub, unwindPrologue, unwindEpilogue and unwindCompiled recover a sender pc from a stack slot or the link register. x86_64 subtracted one from it inside the helper; aarch64 did not. Nothing in the signature said which, so walkVM could only guess from the target architecture, which it did through recordUnwoundPc and an #ifdef. The guess was wrong in a way that mattered. On x86_64 the pc reaching the top of the walk loop was already decremented, so isContReturnBarrier, isContEntryReturnPc and isEntryFrame -- all comparing for exact equality against genuine return addresses -- could never match. Continuation boundaries and entry frames simply stopped being detected there, quietly, producing worse unwinding rather than any visible failure. x86_64 was not even self-consistent: unwindPrologue's isFrameComplete arm and unwindCompiled's entry-barrier arm returned the address unadjusted while their siblings subtracted one, so no single rule described what a caller had been handed. All eleven adjustments are gone. Every helper now returns the address exactly as it sat in the slot, and walkVM applies the attribution adjustment itself through the WalkPc it already carries -- so the lookups keep seeing pc - 1 and the exact-address consumers finally see the real return address. aarch64 is unchanged: its helpers already returned raw and walkVM already recorded them as return addresses. The whole behavioural delta is on x86_64. unwindCompiled is reached only from getJavaTraceAsync, so that path now sees raw addresses on x86_64 where it saw adjusted ones before. aarch64 already handed it raw, so this makes the two agree rather than introducing a new split; calling it out because it is a behaviour change on a legacy path that no test covers. unwindHelperContract_ut.cpp pins the contract directly, with no JVM, no VMStructs and no nmethod -- reaching these helpers through walkVM would need a fake JavaThread and CodeHeap, and a fixture that far from a real layout starts passing for the wrong reason. Reintroducing a folded adjustment on either arch fails both tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7c21727a7f
ℹ️ 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".
There was a problem hiding this comment.
|
🔗 Commit SHA: 4ee10c4 | Docs | View more details | Give us feedback! |
CI Test ResultsRun: #36724788720 | Commit:
Status Overview
Legend: ✅ passed | ❌ failed | ⚪ skipped | 🚫 cancelled Failed Jobs
Summary: Total: 32 | Passed: 31 | Failed: 1 Updated: 2026-09-30 14:11:55 UTC |
The unwind helper contract tests reached unwindStub only. unwindPrologue and unwindEpilogue take a VMNMethod*, which left the two helpers that carried most of the folded `- 1` adjustments untested. A 32-byte buffer standing in for an nmethod closes that. Only entry() and frameSize() are read on the branches under test, so the fixture points VMStructs' field offsets at its own layout for the duration of the test and restores them afterwards, via a per-TU VMStructsTestAccessor like the ones in hotspotMethodId_ut.cpp and hotspot_crash_protection_ut.cpp. Three branches are pinned: pc on the method's entry instruction, pc one instruction past the push/stp that saved the caller's fp (the branch where x86_64 read a different stack slot than its sibling), and pc on the `ret`. Each of the three was mutation-checked by reintroducing the adjustment at that site; each failure was caught by exactly one test. The fixture selects entry()'s pre-JDK-23 arm, since that one is a single indirection. What is under test is the addressing convention rather than the field layout, but the tests say so, so nobody reads them as covering both nmethod shapes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… x86_64 The register-less unwindStub/unwindCompiled overloads write the sender pc into the real ucontext and hand it straight to AsyncGetCallTrace, which has no WalkPc to apply the attribution adjustment. Now that the helpers return raw return addresses, subtract one in those overloads on x86_64, as the helpers used to. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Explain why the helpers return raw return addresses (exact-address consumers compare them, symbolizers derive the attribution address from them) instead of describing earlier behavior. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Nice reconciliation! |
kaahos
left a comment
There was a problem hiding this comment.
this looks good to me, thanks for the changes!
Addresses PROF-16033. Split out of PROF-15955; unblocked by PROF-16059.
The defect
unwindStub,unwindPrologue,unwindEpilogueandunwindCompiledrecover a sender pc from a stack slot or the link register. x86_64 subtracted one from it inside the helper; aarch64 did not. Nothing in the signature said which, so walkVM guessed from the target architecture — that is whatrecordUnwoundPcand its#ifdefwere.The guess was wrong in a way that mattered. On x86_64 the pc reaching the top of the walk loop was already decremented, so these three — all comparing for exact equality against genuine return addresses — could never match:
VMStructs::isContReturnBarrierVMStructs::isContEntryReturnPcVMNMethod::isEntryFrameContinuation boundaries and entry frames stopped being detected on x86_64. Silently: worse unwinding, no visible failure.
x86_64 was not even self-consistent.
unwindPrologue'sisFrameCompletearm andunwindCompiled's entry-barrier arm returned the address unadjusted while their siblings subtracted one, so no single rule described what a caller had been handed. That inconsistency is why PROF-15955 could not simply flag these results as return addresses — it would have double-adjusted the majority of branches.The change
All eleven folded adjustments removed. Every helper returns the address exactly as it sat in the slot; walkVM applies the attribution adjustment itself via the
WalkPcit already carries.findLibraryByAddress,findFrameDesc)RA-1✅RA-1❌RA-1✅ (unchanged)RA✅ fixedRA-1✅RA✅So lookup behaviour is identical everywhere, and the only behavioural delta is x86_64's exact-equality comparisons starting to match.
recordUnwoundPcand its#ifdef __aarch64__are deleted — the last place the two architectures disagreed. Net −38 lines of production code.Please push back on this one
unwindCompiledis reached only fromgetJavaTraceAsync(the legacy AsyncGetCallTrace path). That path now receives raw addresses on x86_64 where it previously got adjusted ones. aarch64 already handed it raw, so this makes the two agree rather than creating a new split — but it is a behaviour change on a path no test covers, and it was the open question when PROF-16033 was first split out. I went with convergence for coherence; a reviewer who knows AGCT should decide whether that is right.Testing
unwindHelperContract_ut.cpp(new, 5 tests) drives the helpers directly — a fabricated frame, a few bytes of synthetic code, and a 32-byte buffer standing in for an nmethod. No JVM is started, and no__linux__gate.That is deliberate. Reaching these helpers through walkVM means getting past
vm_thread == NULL(hotspotSupport.cpp:509, which sits above every consumer) and then faking aJavaThreadand a CodeHeap range as well. I mapped that cascade before writing this; the further such a fixture drifts from a real JVM layout, the likelier it is to pass for the wrong reason.UnwindStubAtEntryRecoversTheRawSenderPcunwindStubip == entrysp[0]/ link registerHelperNeverAppliesTheAdjustmentItselfunwindStubUnwindPrologueAtEntryRecoversTheRawSenderPcunwindPrologueip <= entrysp[0]/ link registerUnwindPrologueAfterFrameSetupRecoversTheRawSenderPcunwindProloguepush %rbp/stp x29,x30sp[1], both archesUnwindEpilogueAtReturnRecoversTheRawSenderPcunwindEpilogueretsp[0]/ link registerThe
AfterFrameSetupone is the interesting branch: it is where x86_64 read a different stack slot than its sibling, and both arches happen to source fromsp[1]and pop two, so a single assertion covers both. The adjacent slot holds a distinct sentinel, so an off-by-one-slot fails too, not just an off-by-one-byte.The two nmethod-taking helpers need
entry()andframeSize(), so the fixture points_frame_size_offset/_nmethod_entry_addressat its own layout for the duration of the test and restores them inTearDown, via a per-TUVMStructsTestAccessor— the same idiom ashotspotMethodId_ut.cppandhotspot_crash_protection_ut.cpp.Mutation-checked: each of the five sites was mutated by reintroducing a folded
- 1there. Every mutation was caught, each by exactly one test.What is not covered
gtest-release-amd64and the amd64 sanitizer jobs are what actually test this.isEntryFrame/isContReturnBarrier— still needs the full walkVM cascade above. Worth recording on the ticket rather than building for.unwindPrologue'sisFrameCompletearm is not covered. That one additionally needsframeCompleteOffset()and a plausible instruction stream, i.e. a good deal more fixture for one more branch.unwindCompiledhas no test at all. Found incidentally while mutation-checking: a- 1reintroduced athotspotStackFrame_aarch64.cpp:157— the same branch shape the prologue test covers — is caught by nothing. It is the same signature, so it is reachable with the fixture this PR adds. I left it out becausehotspotStackFrame.h:90marksunwindCompiledfor removal oncevmbecomes the default walking mode, and it is also the helper flagged under Please push back on this one above. Happy to add it if a reviewer would rather have the net there before that removal lands.entry()branches on_nmethod_entry_offset != -1, and the older arm is a single indirection rather than acode_offset/verified_entry_offsetpair. What is under test is the addressing convention, not the field layout, but the tests say so explicitly so nobody reads them as covering both nmethod shapes.Local: full
gtestDebugandgtestReleasesuites green on aarch64.🤖 Generated with Claude Code