Repository navigation
fix(lldb): hide the Windows CRT, NT and winpthreads frames the stack filter never knew (#824) - #894
Merged
Merged
Conversation
…filter never knew (#824) On Windows the LLDB display filter hid nothing: it knew glibc's start-up names and the POSIX syscall wrappers, but not `__tmainCRTStartup`, `mainCRTStartup`, `BaseThreadInitThunk`, `RtlUserThreadStart`, `NtWaitForSingleObject`, `WaitForSingleObjectEx` or winpthreads — and the source rule did not recognise CodeLLDB's path-less `@symbol` label, which the anchor resolver carries into `file`, so even a known name would not have been hidden. The filter now treats an `@symbol` file as "no source", knows the Windows CRT start-up, the kernel32/ntdll thread, wait and break-in plumbing and `pthread_*`, and recognises MSYS2's include dirs and the MSVC vctools tree as system paths — all under the existing name + no-user-source rule, so a user `Sleep` with a workspace path stays. Measured on this box (pause_test.cpp paused in its sleep loop, CodeLLDB 1.11.8, MSYS2 g++ 15.2.0): the program thread's default stack went from 12 frames, none hidden, to `sleep_for` + `main` with hiddenFrames: 10. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…es and the MinGW-w64 sysroot, and say where inspection anchors (#824) Review of #894: the docs promised a pausedFrame disclosure on every Windows pause and an anchor on "the first program frame"; the disclosure fires only when the stop's thread is the anchored thread, and the anchor for a program asleep in std::this_thread::sleep_for is that header frame. The cpp guide, the changelog and the filtering overview now say so. The bare '/lib/' path pattern, extended to Windows by the backslash normalisation, would have hidden a user poll() under a project's lib\ folder — the roots are anchored now; the Windows patterns match case-insensitively and include the MinGW-w64 sysroot layout and the MSVC STL; /^_*pthread_/ reaches glibc's __pthread_ and ___pthread_ names; and the #816 fixture pins hiddenFrameCount 3 and kept: false. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
debugmcpdev
added a commit
that referenced
this pull request
Oct 12, 2026
…the break-in thread shows the program thread (#904, #898) (#912) * feat(session): selectStopAnchor — one bounded thread-rescue verdict (#904) A debugger names the thread it stopped on, but that thread is not always the program's: CodeLLDB reports a Windows attach or pause on the injected break-in thread (#761/#817/#898), a .NET stop can land on a frameless thread (#496), and js-debug's idle pause lands on a sourceless frame (#900). The attach controller and the frame-anchor resolver each carried their own copy of the rescue, with different predicates (path-based vs policy-based) and different bounds (16 threads / 3 s / 10 s vs none). selectStopAnchor is the one verdict both will share: candidates ordered stopped thread, then threads named main, then listed order; a thread reaches the program when a frame both names a source (not a '@symbol' or '<placeholder>' label) and is one the policy calls the program's; bounds injected by the caller. It decides and does not act — the callers move the anchor and word the disclosure. policyUserFrames reads the per-frame classifier before the filter because Java's filter hands the whole stack back when nothing survives. toStackFrame is the resolver's mapping, extracted so the scan classifies the frames the display sees. threadStacksHandler is the test double both proxy fakes can take. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PKWRJTTqYdYrhDn2z9Et5P * refactor(attach): anchor through selectStopAnchor (#904) The attach controller's own thread rescue (reachesSource, threadReachingSource, the three scan bounds) becomes a call to the shared verdict under the same bounds: the reported stop thread first, then a thread named main, then listed order; 16 unwinds, each with its own 3 s timeout, all within 10 s or the verify window. The fallback chain, the rule strings and the log lines are unchanged, and a target with no observed stop is still never unwound. One deliberate change in the predicate: a frame whose source has a name but no path (a Java class without debug info; a source the adapter could not map) now counts as reaching the program when the policy does not call it internal. '@symbol' and '<placeholder>' labels still never do. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PKWRJTTqYdYrhDn2z9Et5P * fix(session): a pause reported on an internal-only thread adopts the program thread and says so (#898) On Windows, CodeLLDB reports a pause on the thread the OS injects for the break-in: an ntdll-only stack, every frame internal after #894, while the program's own thread sits in main one list_threads away. The resolver's sibling scan only ever ran for an EMPTY stack; an all-internal one took the #346 path (show the top internal frame) and stopped there. The resolver now routes both scans through selectStopAnchor. A program-neutral stop (pause after normalisation) reported on a thread with no program frame — judged by the per-frame classifier, so it holds for Java, whose filter hands the whole stack back — adopts the first sibling whose stack reaches the program, moves the anchor, and discloses the switch in note. A breakpoint, step, exception or entry stop belongs to its thread and never moves; an explicit threadId never moves; a stop that lands during the scan drops the stale verdict (on the frameless path too). The verdict is kept per stop so later locals and evaluate reads do not scan again, and the readiness scan gains the per-unwind timeout it never had. Any implicit read anchored off the reported thread says so in one sentence. StackTraceResult gains noProgramFrame for the case where no sibling reaches the program either, which PR B words in the pause answer. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PKWRJTTqYdYrhDn2z9Et5P * docs: the stop-anchor verdict and the internal-only pause adoption (#904, #898) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PKWRJTTqYdYrhDn2z9Et5P * fix(session): the resolver's plain stack read stays a two-argument request The per-unwind timeout is passed only when asked for; an explicit undefined third argument broke the tests that pin the plain read's call shape. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PKWRJTTqYdYrhDn2z9Et5P * fix(session): key the internal-only adoption on the readiness option, not on a missing threadId The server facade names the current thread when it reads the default stack, and marks the read implicit with ensureStackReady alone; a caller's explicit threadId is read without the option. The adoption gate and the divergence note now follow that contract — measured live on a Windows pause of examples/cpp/pause_test.cpp, where the first cut never fired through get_stack_trace. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PKWRJTTqYdYrhDn2z9Et5P * fix(session): review follow-ups for the stop-anchor verdict (#904) - get_stack_trace forwards noProgramFrame, as the docs and fragment said it did; handler test. - The #600 resolver test names its program thread something other than main, so the user-over-runtime preference, not name ordering, decides it. - The readiness scan's budget is floored at one poll interval, so a sibling that answers in a few ms is not "could not be unwound" with a few ms left in the window. - A verdict with no stopped thread says 'none'; the memo keeps negative verdicts by design and says so; ensureStackReady is documented where it is declared and where the facade reads it. - The fragment discloses both directions of the attach predicate change and the Go pause consequence; CLAUDE.md loses a stray list marker. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PKWRJTTqYdYrhDn2z9Et5P --------- Co-authored-by: JF <john.franklin@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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.
Summary
Fixes #824.
On Windows the LLDB display filter hid nothing. Two causes, both in
packages/shared/src/interfaces/lldb-policy-shared.ts:@symbollabel. A frame without debug info arrives assource: {name: '@NtWaitForSingleObject', sourceReference: N}with no path, andFrameAnchorResolvercarries that label intofile.isSystemOrMissingSource('@NtWaitForSingleObject')returnedfalse(not empty, not<unknown_source>, no POSIX path), so even a name the filter knew would not have been hidden — adding Windows names alone would have changed nothing.__libc_start_main,_startand the POSIX syscall wrappers, not__tmainCRTStartup,mainCRTStartup,BaseThreadInitThunk,RtlUserThreadStart,NtWaitForSingleObject,WaitForSingleObjectEx,DbgBreakPoint/DbgUiRemoteBreakin(the break-in thread a pause lands on) or winpthreads.Change
isSystemOrMissingSource: an@-prefixed file is "no source";\is normalised to/before path matching; MSYS2'smingw64|mingw32|ucrt64|clang64|clangarm64/include/, the MSVCvctoolstree andWindows Kitsare system paths.WINDOWS_RUNTIME_NAME_PATTERNS: the CRT start-up ([w]mainCRTStartup,[w]WinMainCRTStartup,__tmainCRTStartup,__scrt_common_main[_seh],invoke_main,__acrt_*), the kernel32/ntdll plumbing (BaseThreadInitThunk,RtlUserThreadStart,Dbg{BreakPoint,UiRemoteBreakin},Nt*/Zw*,WaitFor{Single,Multiple}Object[s][Ex],Sleep[Ex],SleepConditionVariable{CS,SRW},WaitOnAddress) and_?pthread_*— all under the existing name + no-user-source rule, so a user function namedSleepwith a workspace path is kept.FrameAnchorResolver: the [QUESTION] Go stack filtering can return an empty stack — should it keep the first frame like JS? #346 guarantee (never an empty stack) and the js: when the paused frame is inside node_modules, get_stack_trace hides it and get_local_variables/evaluate_expression silently anchor on an async ancestor frame #672 paused-frame disclosure apply as before.docs/stack-trace-filtering.md(the Windows rule, the@symbolshape, COBOL composes the filter too),docs/cpp/README.mdstep 6;changelog.d/824.fixed.md(it states the Linux side effect:pthread_*now also hides glibc's nptl frames, whose./nptl/path the filter already classified as system).Measured on this box (Windows 11, CodeLLDB 1.11.8, MSYS2 g++ 15.2.0,
examples/cpp/pause_test.cpppaused in its sleep loop, through the dev server)Program thread, before → after:
hiddenFramesNtWaitForSingleObject…main…RtlUserThreadStart), none hiddenstd::this_thread::sleep_for<…>(MinGWthis_thread_sleep.h),main(pause_test.cpp:20)includeInternals: trueshows the full 12 with the 9@symbolframes flaggedunresolvedSource: trueand the #816 note, as before.The break-in thread the pause itself lands on (
DbgBreakPoint→RtlUserThreadStart, 4 frames) now reads "All 4 frames are internal/runtime frames; showing the top internal frame …" withhiddenFrames: 3— the #346 shape. That a launched program'spause_executionanchorsget_stack_traceon this injected thread rather than the program's thread is a separate finding, filed on its own.Tests
adapter-policy-cpp.test.ts: 17 new rows in the real Windows shape (name: 'NtWaitForSingleObject', file: '@NtWaitForSingleObject'…), including the keep cases (userSleepwith a workspace path,main,sleep_forfrom the MinGW C++ header, a stripped user frame whose@labelmatches no name), plusfilterStackFrames reduces the measured Windows pause stack to the programover the issue's 12-frame stack →[sleep_for, main].adapter-policy-rust.test.ts: 11 new rows (Windows names, MSVCinvoke_mainundervctools, glibc nptlpthread_cond_waitunder./nptl/→ hidden, userpthread_helper→ kept, a/rustc/std frame with a non-wrapper name → kept).adapter-policy-cobol.test.ts: three Windows rows (COBOL wraps the shared filter).session-manager-dap.test.ts(get_stack_trace gives the js-debug sourceMaps hint for native frames on a C/C++ (CodeLLDB) session #816 fixture): the default view now hides the@symbolframes (hiddenFrameCount ≥ 2,pausedFramenamesNtWaitForSingleObject) andincludeInternals: truestill carries theunresolvedSourceflags.tests/e2e/mcp-server-smoke-cpp-attach.test.ts: on win32 assertshiddenFrames > 0and ahiddennote instead of the conditionalunresolvedSourcecheck that would otherwise silently stop running. Run locally: 2/2 pass.Verification:
npm run typecheck:all(ratchet 0/0),npm run lint,node scripts/check-docs.mjs,pnpm changelog:check; targeted unit run 379/379; pre-push hook (unit + integration) passed.🤖 Generated with Claude Code