llvm / llvm/llvm-project

[clangd] Null-pointer dereference in `FrontendAction::EndSourceFileAction()` when destroying the AST of a preprocessed input (`.i` / `.ii`)

Open
#221,621 2 comments 0 reactions 0 assignees View on GitHub
clangd crash
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.