[Clangd] Clangd hangs when receiving `prepareCallHierarchy` request, which may involve Macro.
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
`version: 23.0.0, git commit aff5afc48df63615053b2432da198a4932435c3f`
`Environment:Ubuntu-24.04`
A malformed C++ file generated by **fuzzer** plus a single textDocument/prepareCallHierarchy request can trigger a reproducible high-CPU hang in non-ASAN clangd.
It behaves like:
```
PID COMMAND S %CPU TIME+
27338 Worker:main.cpp R 99.9 0:25.21
27324 clangd.main S 0.0 0:00.00
...
```
Here is the request sequence:
```
initialize
initialized
didOpen
prepareCallHierarchy
shutdown
exit
```
And here is the original C++ source code:
```cpp
#include
void f(){
try{
assert(dynamic_cast(_GCC_specific_handler(ms_exc, this_frame, ms_orig_context, ms_disp,__gxx_personality_imp)) == a1.getA1());
"""
```
The prepareCallHierachy request:
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "textDocument/prepareCallHierarchy",
"params": {
"textDocument": {"uri": "REPLACED_AT_RUNTIME"},
"position": {"line": 3, "character": 33},
},
}
```
I also used `perf` to observe, the perf log is affiliated below.
[front_single_enum_compact_lines.dwarf.perf.txt](https://github.com/user-attachments/files/27203474/front_single_enum_compact_lines.dwarf.perf.txt)
**TLDR**:
perf shows the clangd worker thread under:
```
prepareCallHierarchy
-> getDeclAtPositionWithRelations
-> SelectionTree::createEach
-> SelectionVisitor::claimRange
```
## reproducer
usage:
```
python3 reproducer.py \
--clangd /path/to/clangd
```
```python
#!/usr/bin/env python3
import argparse
import json
import queue
import shutil
import subprocess
import sys
import tempfile
import threading
import time
from pathlib import Path
from urllib.parse import quote
SOURCE = """#include
void f(){
try{
assert(dynamic_cast(_GCC_specific_handler(ms_exc, this_frame, ms_orig_context, ms_disp,__gxx_personality_imp)) == a1.getA1());
"""
PREPARE_CALL_HIERARCHY = {
"jsonrpc": "2.0",
"id": 1,
"method": "textDocument/prepareCallHierarchy",
"params": {
"textDocument": {"uri": "REPLACED_AT_RUNTIME"},
"position": {"line": 3, "character": 33},
},
}
def file_uri(path: Path) -> str:
return "file://" + quote(str(path))
def encode_lsp(msg: dict) -> bytes:
body = json.dumps(msg, separators=(",", ":")).encode("utf-8")
header = f"Content-Length: {len(body)}\r\n\r\n".encode("ascii")
return header + body
def read_lsp(stdout, outq: queue.Queue):
try:
while True:
headers = {}
while True:
line = stdout.readline()
if not line:
return
if line in (b"\r\n", b"\n"):
break
text = line.decode("utf-8", "replace").strip()
if ":" in text:
k, v = text.split(":", 1)
headers[k.strip().lower()] = v.strip()
length = int(headers.get("content-length", "0"))
body = stdout.read(length)
if not body:
return
try:
outq.put(json.loads(body.decode("utf-8", "replace")))
except Exception:
outq.put({"_raw": body.decode("utf-8", "replace")})
finally:
outq.put(None)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"--clangd",
default="/home/ubuntu2404/llvm-project/build/bin/clangd",
help="Path to a non-ASAN clangd binary",
)
parser.add_argument("--timeout", type=float, default=30.0)
parser.add_argument(
"--log-dir",
default=None,
help="Optional directory to keep repro files instead of using a temp dir",
)
args = parser.parse_args()
if args.log_dir:
workdir = Path(args.log_dir)
workdir.mkdir(parents=True, exist_ok=True)
cleanup = False
else:
workdir = Path(tempfile.mkdtemp(prefix="clangd-prepareCallHierarchy-hang-"))
cleanup = True
source_path = workdir / "main.cpp"
source_path.write_text(SOURCE, encoding="utf-8")
uri = file_uri(source_path)
cmd = [args.clangd]
print("Running:", " ".join(cmd), file=sys.stderr)
print("Workspace:", workdir, file=sys.stderr)
proc = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=False,
)
q: queue.Queue = queue.Queue()
t = threading.Thread(target=read_lsp, args=(proc.stdout, q), daemon=True)
t.start()
prepare = json.loads(json.dumps(PREPARE_CALL_HIERARCHY))
prepare["params"]["textDocument"]["uri"] = uri
messages = [
{
"jsonrpc": "2.0",
"id": 0,
"method": "initialize",
"params": {
"processId": None,
"rootUri": file_uri(workdir),
"capabilities": {},
"workspaceFolders": [{"uri": file_uri(workdir), "name": workdir.name}],
},
},
{"jsonrpc": "2.0", "method": "initialized", "params": {}},
{
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": uri,
"languageId": "cpp",
"version": 1,
"text": SOURCE,
}
},
},
prepare,
{"jsonrpc": "2.0", "id": 2, "method": "shutdown", "params": None},
{"jsonrpc": "2.0", "method": "exit", "params": None},
]
assert proc.stdin is not None
for msg in messages:
proc.stdin.write(encode_lsp(msg))
proc.stdin.flush()
time.sleep(0.15)
try:
proc.stdin.close()
except Exception:
pass
deadline = time.time() + args.timeout
while time.time() < deadline:
rc = proc.poll()
if rc is not None:
break
time.sleep(0.1)
killed = False
if proc.poll() is None:
killed = True
proc.kill()
stdout_msgs = []
try:
while True:
item = q.get(timeout=0.2)
if item is None:
break
stdout_msgs.append(item)
except queue.Empty:
pass
try:
stderr = proc.stderr.read().decode("utf-8", "replace") if proc.stderr else ""
except Exception:
stderr = ""
rc = proc.wait(timeout=2)
if stderr:
print("=== clangd stderr ===")
print(stderr, end="" if stderr.endswith("\n") else "\n")
summary = {
"exit_code": rc,
"signal": -rc if rc < 0 else None,
"killed_after_timeout": killed,
"stdout_message_count": len(stdout_msgs),
"stdout_messages": stdout_msgs,
}
print(json.dumps(summary, ensure_ascii=False, indent=2))
if cleanup:
shutil.rmtree(workdir, ignore_errors=True)
return 0 if not killed else 124
if __name__ == "__main__":
raise SystemExit(main())
```
Contributor guide
Research direction
Run the provided reproducer.py with a non-ASAN clangd binary and confirm the timeout on the malformed main.cpp input. Trace textDocument/prepareCallHierarchy through getDeclAtPositionWithRelations, SelectionTree::createEach, and SelectionVisitor::claimRange; done means the request no longer hangs or consumes a worker at high CPU for this reproducer.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, python
- Domain
- compilers, devtools
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100