llvm / llvm/llvm-project

[clangd] clangd crashes when CWD is a deleted directory due to unchecked ErrorOr in OverlayFileSystem::pushOverlay

Open
#210,481 1 comment 0 reactions 0 assignees View on GitHub
clangd crash generated by fuzzer
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

This is generated by a fuzzer.
When a clangd operation that explicitly uses `DraftStoreFS::view()` (e.g. `textDocument/rename`, `textDocument/codeAction`) is processed while the
process's current working directory is a deleted directory ("zombie CWD"),
clangd asserts:

ErrorOr.h:236: Assertion `!HasError && "Cannot get value when an error exists!"' failed.

The root cause is an unchecked `ErrorOr::get()` call in `OverlayFileSystem::pushOverlay`.
The crash only occurs for LSP methods that call `DirtyFS->view(std::nullopt)` directly — `rename`, `codeAction`, and potentially others. It does **not** occur on
`textDocument/didOpen` alone, because the AST worker uses `TFS` (the base
filesystem) via `getHeaderFS()` (`UseDirtyHeaders` defaults to `false`).

I traced the history of this issue, and found a larger refactor of OverlayFileSystem was proposed in [D121423]. Among other changes, it would have fixed this bug as a side effect by keeping the CWD in the overlay itself rather than syncing it across contained filesystems. However, after 4 revisions and review discussion about CWD semantics with heterogeneous filesystems, the patch was marked "Changes Planned" (2022-03-17) and later abandoned.

reproducer:
```py
#!/usr/bin/env python3
"""Minimal repro: pushOverlay crashes clangd when CWD doesn't exist.

Usage:
python3 repro_min.py # auto-detect clangd from PATH
python3 repro_min.py /path/to/clangd # use specific binary
"""

import subprocess, os, json, sys, time, shutil

CLANGD = sys.argv[1] if len(sys.argv) > 1 else shutil.which("clangd")
if not CLANGD:
print("Usage: repro_min.py [/path/to/clangd]", file=sys.stderr)
print(" clangd not found in PATH, provide the binary path", file=sys.stderr)
sys.exit(1)

def send(proc, req):
data = json.dumps(req).encode()
proc.stdin.write(f"Content-Length: {len(data)}\r\n\r\n".encode() + data)
proc.stdin.flush()

# 1. zombie CWD
zombie = os.path.join("/tmp", f"repro-zombie-{os.getpid()}")
os.mkdir(zombie)
os.chdir(zombie)
os.rmdir(zombie)

# 2. source file + compile_commands.json (not under zombie CWD)
src = "/tmp/repro.cpp"
with open(src, "w") as f:
f.write("int x;\n")
with open("/tmp/compile_commands.json", "w") as f:
json.dump([{"command": "clang++ -fsyntax-only repro.cpp",
"directory": "/tmp", "file": src}], f)

# 3. launch clangd (TMPDIR=/tmp so crash handler can write temp files;
# LLVM_DISABLE_SYMBOLIZATION=1 prints raw stack addresses directly to stderr)
env = os.environ.copy()
env["TMPDIR"] = "/tmp" # zombie CWD breaks temp file creation, redirect it
proc = subprocess.Popen(
[CLANGD],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
env=env)

# 4. initialize → didOpen (wait for async parse) → rename
send(proc, {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},"processId":None}})
send(proc, {"jsonrpc":"2.0","method":"initialized","params":{}})
send(proc, {"jsonrpc":"2.0","method":"textDocument/didOpen",
"params":{"textDocument":{"uri":"file:///tmp/repro.cpp","languageId":"cpp","version":1,"text":"int x;\n"}}})
time.sleep(3)
send(proc, {"jsonrpc":"2.0","id":2,"method":"textDocument/rename",
"params":{"textDocument":{"uri":"file:///tmp/repro.cpp"},"position":{"line":0,"character":4},"newName":"y"}})

proc.stdin.close()
proc.stdout.close()
try:
proc.wait(timeout=8)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()

err = proc.stderr.read().decode(errors="replace")
print(err)

```

```
clangd: /home/ubuntu2404/lsp_fuzz_latest/tmp/llvm-project-main-20260622-latest/llvm/include/llvm/Support/ErrorOr.h:236: storage_type *llvm::ErrorOr>::getStorage() [T = std::basic_string]: Assertion `!HasError && "Cannot get value when an error exists!"' failed.
PLEASE submit a bug report to https://github.com/llvm/llvm-project/issues/ and include the crash backtrace and instructions to reproduce the bug.
Stack dump without symbol names (ensure you have llvm-symbolizer in your PATH or set the environment var `LLVM_SYMBOLIZER_PATH` to point to it):
0 clangd 0x00005c0549e32c1a __interceptor_backtrace+0x4a ___interceptor_backtrace + 74
1 clangd 0x00005c054a4f3aa6 llvm::sys::PrintStackTrace(llvm::raw_ostream&, int)+0x146
2 clangd 0x00005c054a4e8358 llvm::sys::RunSignalHandlers()+0x1c8
3 clangd 0x00005c054a4f8138 SignalHandler(int, siginfo_t*, void*)+0x4f8
4 libc.so.6 0x000077593d445330
5 libc.so.6 0x000077593d49eb2c pthread_kill + 284
6 libc.so.6 0x000077593d44527e gsignal + 30
7 libc.so.6 0x000077593d4288ff abort + 223
8 libc.so.6 0x000077593d42881b
9 libc.so.6 0x000077593d43b517
10 clangd 0x00005c054a3cfddf llvm::vfs::OverlayFileSystem::pushOverlay(llvm::IntrusiveRefCntPtr)+0x66f
11 clangd 0x00005c054ff8c619 clang::clangd::(anonymous namespace)::DraftStoreFS::viewImpl() const+0x3a9
12 clangd 0x00005c054ffb8746 void llvm::detail::UniqueFunctionBase >::CallImpl)>)::$_0>(void*, llvm::Expected&)+0x406
13 clangd 0x00005c0550a09a5a void llvm::detail::UniqueFunctionBase::CallImpl)>, clang::clangd::TUScheduler::ASTActionInvalidation)::$_0>(void*)+0x151a
14 clangd 0x00005c05509e8915 clang::clangd::(anonymous namespace)::ASTWorker::runTask(llvm::StringRef, llvm::function_ref)+0x7b5
15 clangd 0x00005c05509e5fa7 void llvm::detail::UniqueFunctionBase::CallImpl(void*)+0x7797
16 clangd 0x00005c0550fd1867 void* llvm::thread::ThreadProxy)::$_1> >(void*)+0x1c7
17 clangd 0x00005c0549e86c2d asan_thread_start(void*)+0x6d
18 libc.so.6 0x000077593d49caa4
19 libc.so.6 0x000077593d529c6c
```

Contributor guide

Open the contributing guide

Research direction

Start by running the supplied repro_min.py with clangd, then inspect OverlayFileSystem::pushOverlay and DraftStoreFS::viewImpl, where the stack trace places the failure. Compare the behavior of rename and codeAction with didOpen under a deleted current directory. Done means these operations no longer assert when the current directory is unavailable, with regression coverage for the reproducer scenario.

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
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.