[clangd] Null-pointer dereference in `FrontendAction::EndSourceFileAction()` when destroying the AST of a preprocessed input (`.i` / `.ii`)
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
## Summary
`clangd` crashes with `SIGSEGV` whenever it destroys a `ParsedAST` whose input was
classified as *already preprocessed* (`.i`, `.ii`, `.mi`, `.mii`, or explicit
`-x cpp-output`).
`ParsedAST::~ParsedAST()` intentionally detaches the `Preprocessor` from the
`CompilerInstance` before calling `FrontendAction::EndSourceFile()`. Since
[#137665](https://github.com/llvm/llvm-project/pull/137665),
`FrontendAction::EndSourceFileAction()` unconditionally dereferences that
now-null `Preprocessor` for preprocessed inputs.
## Root cause
clangd deliberately nulls out the preprocessor before ending the source file —
`clang-tools-extra/clangd/ParsedAST.cpp`:
```cpp
ParsedAST::~ParsedAST() {
if (Action) {
// We already notified the PP of end-of-file earlier, so detach it first.
// We must keep it alive until after EndSourceFile(), Sema relies on this.
auto PP = Clang->getPreprocessorPtr(); // Keep PP alive for now.
Clang->setPreprocessor(nullptr); // Detach so we don't send EOF again.
Action->EndSourceFile(); // Destroy ASTContext and Sema.
}
}
```
`FrontendAction::EndSourceFile()` guards its own preprocessor use, but the
`EndSourceFileAction()` it calls a few lines later does not —
`clang/lib/Frontend/FrontendAction.cpp`:
```cpp
void FrontendAction::EndSourceFile() {
CompilerInstance &CI = getCompilerInstance();
if (CI.hasPreprocessor()) // <-- guarded here
CI.getPreprocessor().EndSourceFile();
CI.getDiagnosticClient().EndSourceFile();
EndSourceFileAction(); // <-- but not inside here
...
}
void FrontendAction::EndSourceFileAction() {
if (CurrentInput.isPreprocessed())
// Reset the preprocessor macro expansion to the default.
getCompilerInstance().getPreprocessor().SetEnableMacroExpansion();
// ^^^^^^^^^^^^^^^^ asserts in +Asserts builds,
// dereferences null in release builds
}
```
## Steps to reproduce
Minimal, self-contained; no MySQL checkout needed.
```bash
mkdir /tmp/cd && cd /tmp/cd
printf 'int main(){ return 0; }\n' > a.ii
cat > compile_commands.json <<'EOF'
[{"directory":"/tmp/cd",
"command":"/usr/bin/g++ -std=c++17 -c /tmp/cd/a.ii -o a.o",
"file":"/tmp/cd/a.ii"}]
EOF
```
Start `clangd --compile-commands-dir=/tmp/cd` and send `initialize`,
`initialized`, `textDocument/didOpen` for `file:///tmp/cd/a.ii` (any LSP client,
or just open the file in your editor).
**Expected:** diagnostics for `a.ii`.
**Actual:** `clangd` dies with `SIGSEGV` (exit code `-11`).
The same happens with a `.i` *header* that is only ever `#include`d and never
appears in `compile_commands.json` (the InnoDB `univ.i` case):
```bash
mkdir /tmp/cd3 && cd /tmp/cd3
printf '#pragma once\nint univ_fn(int x);\n' > univ.i
printf '#include "univ.i"\nint main(){ return univ_fn(0); }\n' > main.cc
cat > compile_commands.json <<'EOF'
[{"directory":"/tmp/cd3",
"command":"/usr/bin/g++ -std=gnu++17 -c /tmp/cd3/main.cc -o main.o",
"file":"/tmp/cd3/main.cc"}]
EOF
# open file:///tmp/cd3/univ.i -> SIGSEGV
```
Backtrace (minimal repro)
```
PLEASE submit a bug report to https://github.com/llvm/llvm-project/issues/ ...
#0 0x0000000000545bc7 llvm::sys::PrintStackTrace(llvm::raw_ostream&, int)
#1 0x0000000000542b65 SignalHandler(int, siginfo_t*, void*) Signals.cpp:0:0
#2 0x00007fdf12e4a990 __restore_rt
#3 0x000000000286268e clang::FrontendAction::EndSourceFileAction()
#4 0x0000000002864c8c clang::FrontendAction::EndSourceFile()
#5 0x000000000195a11b clang::clangd::ParsedAST::~ParsedAST()
#6 0x00000000019f3266 std::_Sp_counted_ptr::_M_dispose()
#7 0x0000000000516707 std::_Sp_counted_base<...>::_M_release()
#8 0x0000000001b9c42c llvm::thread::ThreadProxy<...>(void*)
```
Contributor guide
Research direction
Start with clang-tools-extra/clangd/ParsedAST.cpp and clang/lib/Frontend/FrontendAction.cpp, then reproduce the crash using the minimal .ii compile_commands.json setup. Trace the preprocessor lifetime through ParsedAST::~ParsedAST() and EndSourceFileAction(), and add regression coverage showing that opening preprocessed inputs produces diagnostics without clangd crashing.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers, devtools
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100