Skip to content

Java attach anchors to a frameless thread — session reports paused but is uninspectable, and the suggested recovery isn't possible with existing tools #465

Description

@debugmcpdev

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 continuepause 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 &
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 there

PauseTest.main is sitting in Thread.sleep() — it has frames. The anchor just isn't on it.

Why an agent gets stuck

  1. 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.
  2. pause_execution { threadId: 1 } does not re-anchor — returns {"message": "Already paused"} and the stack stays empty.
  3. 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)

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

  • 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)

  1. 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.
  2. Add threadId to get_stack_trace so the note's advice becomes followable, and so multi-threaded attach targets are inspectable at all.
  3. Make the note name the actual recovery (continue_execution then pause_execution) instead of pointing at a tool that can't do the job.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions