Attaching to a JVM leaves the session reporting paused while anchored to a thread that has no stack frames. The session is inspectable only via an undocumented continue → pause dance, and the guidance the server offers cannot be followed with the tools that exist.
Repro
$ java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5016 -cp examples/java PauseTest &
PauseTest.main is sitting in Thread.sleep() — it has frames. The anchor just isn't on it.
Why an agent gets stuck
- The note's advice is not actionable. It says "use list_threads to inspect other threads" — but
list_threads returns only {id, name}, and get_stack_trace accepts no threadId. There is no tool that returns frames for a chosen thread. The suggested escape hatch doesn't exist.
pause_execution { threadId: 1 } does not re-anchor — returns {"message": "Already paused"} and the stack stays empty.
get_local_variables contradicts session state — says "The debugger may not be paused" while list_debug_sessions reports state: "paused". An agent reading that reasonably concludes the attach failed.
Recovery (works, but nothing suggests it)
So the attach is sound and the JVM is fully inspectable — the session just starts anchored to the wrong thread and offers no discoverable way back.
Notes
- Launch-mode Java is unaffected: the pause workflow anchors to
PauseTest.main correctly and returns frames + locals first try. This is attach-specific.
- The same shape shows up on C/C++ attach:
attach_to_process { processId, stopOnEntry: true } anchored to NtWaitForWorkViaWorkerFactory (a Windows thread-pool worker) rather than main, out of 5 threads. Frames were returned there, so it degrades more gracefully, but the thread choice is equally arbitrary.
Suggested fixes (any one helps; the first is probably the real fix)
- On attach, adopt a frame-bearing thread.
get_stack_trace already has exactly this fallback for empty results — scan other stopped threads, adopt the first with frames, annotate via note. Running that at attach time would fix this outright. Preferring a thread with user (non-internal) frames would also fix the C/C++ case.
- Add
threadId to get_stack_trace so the note's advice becomes followable, and so multi-threaded attach targets are inspectable at all.
- Make the note name the actual recovery (
continue_execution then pause_execution) instead of pointing at a tool that can't do the job.
- Fix the
get_local_variables message — "The debugger may not be paused" is wrong when the session is paused; the real condition is "the anchored thread has no frames".
Found during a full /testdebugger sweep (9 servers × 3 backends); reproduced on local/SSE and local/HTTP.
Attaching to a JVM leaves the session reporting
pausedwhile anchored to a thread that has no stack frames. The session is inspectable only via an undocumentedcontinue→pausedance, and the guidance the server offers cannot be followed with the tools that exist.Repro
$ java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5016 -cp examples/java PauseTest &attach_to_process { port: 5016 } → { "success": true, "state": "paused" } get_stack_trace { } → { "stackFrames": [], "count": 0, "note": "The stopped thread reported no stack frames within 3000ms; the target may be paused in native code. Retry get_stack_trace, or use list_threads to inspect other threads." } get_local_variables { } → { "variables": [], "message": "No stack frames available. The debugger may not be paused." } list_threads { } → [ {id:1,name:"main"}, {id:2,"Reference Handler"}, … 7 threads ] // main is right therePauseTest.mainis sitting inThread.sleep()— it has frames. The anchor just isn't on it.Why an agent gets stuck
list_threadsreturns only{id, name}, andget_stack_traceaccepts nothreadId. There is no tool that returns frames for a chosen thread. The suggested escape hatch doesn't exist.pause_execution { threadId: 1 }does not re-anchor — returns{"message": "Already paused"}and the stack stays empty.get_local_variablescontradicts session state — says "The debugger may not be paused" whilelist_debug_sessionsreportsstate: "paused". An agent reading that reasonably concludes the attach failed.Recovery (works, but nothing suggests it)
continue_execution { } pause_execution { } → { "stopReason": "pause", "location": { "file": "PauseTest.java", "line": 7 } } get_stack_trace → [ { "name": "PauseTest.main", "line": 7 } ] get_local_variables → [ { "args": "java.lang.String[][0]" }, { "counter": "208" } ]So the attach is sound and the JVM is fully inspectable — the session just starts anchored to the wrong thread and offers no discoverable way back.
Notes
PauseTest.maincorrectly and returns frames + locals first try. This is attach-specific.attach_to_process { processId, stopOnEntry: true }anchored toNtWaitForWorkViaWorkerFactory(a Windows thread-pool worker) rather thanmain, out of 5 threads. Frames were returned there, so it degrades more gracefully, but the thread choice is equally arbitrary.Suggested fixes (any one helps; the first is probably the real fix)
get_stack_tracealready has exactly this fallback for empty results — scan other stopped threads, adopt the first with frames, annotate vianote. Running that at attach time would fix this outright. Preferring a thread with user (non-internal) frames would also fix the C/C++ case.threadIdtoget_stack_traceso the note's advice becomes followable, and so multi-threaded attach targets are inspectable at all.continue_executionthenpause_execution) instead of pointing at a tool that can't do the job.get_local_variablesmessage — "The debugger may not be paused" is wrong when the session is paused; the real condition is "the anchored thread has no frames".Found during a full
/testdebuggersweep (9 servers × 3 backends); reproduced on local/SSE and local/HTTP.