| Issue |
202956
|
| Summary |
[clang] Crash instantiating member variable template partial specialization from PCH-deserialized class template
|
| Labels |
|
| Assignees |
|
| Reporter |
RexTechnology1
|
## Summary
Clang crashes while instantiating a member variable template partial specialization when the enclosing class template has been deserialized from a PCH/preamble that was built **with compiler errors** (`-fallow-pch-with-compiler-errors`, which clangd uses for preambles).
The crash happens in:
```cpp
clang::TemplateDeclInstantiator::VisitVarTemplatePartialSpecializationDecl
```
That code assumes the primary member variable template has already been instantiated into the current instantiation `Owner` before one of its partial specializations is instantiated.
This is true for a normally parsed class template: the primary member variable template is declared before its partial specializations, so it is visited first while the enclosing class template is instantiated.
However, when the enclosing class template pattern is deserialized from a preamble PCH that contains errors, the primary member variable template may not have been materialized into `Owner` by the time the partial specialization is visited. The lookup for the instantiated primary then returns empty.
In an assertions build, this trips:
```cpp
assert(!Found.empty() && "Instantiation found nothing?");
```
In a release build, the following `Found.front()` dereference crashes with a SIGSEGV.
In practice this makes clangd crash-loop on code using `std::expected<T, E>` from libstdc++ 15.x whenever the preamble has an unresolved `#include`, a routine state in interactive editing, for example generated headers that have not been built yet.
## Crash
Assertion failure:
```text
clang/lib/Sema/SemaTemplateInstantiateDecl.cpp:2561:
Decl *clang::TemplateDeclInstantiator::VisitVarTemplatePartialSpecializationDecl(
VarTemplatePartialSpecializationDecl *):
Assertion `!Found.empty() && "Instantiation found nothing?"' failed.
```
Relevant stack from trunk with assertions enabled, abridged:
```text
#11 clang::TemplateDeclInstantiator::VisitVarTemplatePartialSpecializationDecl
#12 clang::Sema::InstantiateClassImpl
#13 clang::Sema::InstantiateClassTemplateSpecialization
#16 clang::Sema::RequireCompleteTypeImpl
#19 clang::Sema::ActOnStartOfFunctionDef
#21 clang::Parser::ParseFunctionDefinition
...
#29 clang::clangd::ParsedAST::build
```
The crash occurs while completing the return type of an out-of-line function definition. `#15 clang::StackExhaustionHandler::runWithSufficientStackSpace`, omitted above, is the standard wrapper around `RequireCompleteType` here, not a stack-exhaustion frame.
Clang reports the instantiation context as:
```text
.../c++/15.2.0/expected:335:11: instantiating class definition
'std::expected<..., ...>'
```
## Trigger
libstdc++ 15.x implements part of `std::expected` using a member variable template and a partial specialization. Reduced excerpt:
```cpp
template<typename _Tp, typename _Er>
class expected {
template<typename _Up, typename _Gr, typename _Unex = unexpected<_Er>,
typename = remove_cv_t<_Tp>>
static constexpr bool __cons_from_expected = /* ... */;
template<typename _Up, typename _Gr, typename _Unex>
static constexpr bool __cons_from_expected<_Up, _Gr, _Unex, bool> = /* ... */;
// ...
};
```
When `std::expected<T, E>` is completed from a class-template pattern deserialized from an errorful preamble PCH, Clang attempts to instantiate the partial specialization without the primary member variable template having been instantiated into `Owner` first, so `Owner->lookup(...)` returns an empty result.
## Reproduction
Minimal reproducer, 3-line header plus 2-line main file, via clangd against libstdc++ 15.x:
`repro.h`:
```cpp
#include "totally_nonexistent_header.h" // unresolved include: the key ingredient
#include <expected>
std::expected<int, int> g(); // specialization formed in the preamble
```
`repro.cpp`:
```cpp
#include "repro.h"
std::expected<int, int> g() { return 1; } // completes it -> crash
```
Open `repro.cpp` in clangd configured to use a GCC 15.x libstdc++, in my case via `--query-driver` for a GCC 15.2 cross toolchain. Any libstdc++ 15.x setup is expected to behave the same. clangd builds a preamble of `repro.h` with the unresolved-include error, under its always-on `-fallow-pch-with-compiler-errors`, and then crashes while building the AST for `repro.cpp`.
This is a minimal pair: removing only the unresolved-include line, with the files otherwise identical, makes the crash disappear.
The conditions, isolated by bisection, each variant verified:
1. **An unresolved `#include` in the preamble.** Notably, a *different* kind of error, an unknown type used inside a struct without any missing include, does **not** trigger it.
2. **libstdc++ 15.x `<expected>`.** GCC 16 rewrote this part of `<expected>` and no longer triggers. A hand-written mock of the member-variable-template-partial-specialization shape (with the same unresolved include present) does not trigger either, so the exact libstdc++ 15 structure matters.
3. **clangd's preamble path.** The specialization is formed incompletely in the preamble and completed in the main file.
`T`'s triviality does not matter: `std::expected<int, int>` crashes, as do non-trivial payloads.
What does **not** reproduce:
```text
one-shot clang -fsyntax-only (no PCH)
clang -x c++-header -Xclang -fallow-pch-with-compiler-errors -emit-pch
+ -include-pch (separate-header form)
the same, additionally with -Xclang -preamble-bytes=N,1 to mimic clangd's
main-file-preamble mechanism
C++20 modules
GCC 16 libstdc++
a self-contained mock of the <expected> structure
```
So the crash is currently only reachable through clangd's in-process `PrecompiledPreamble`, but the reproducer above is tiny and fires deterministically there. Help turning it into a hermetic lit test would be appreciated; it still needs real libstdc++ 15 headers. A clangd `TestTU`-style unittest may be the most direct option if there is precedent for an errorful-preamble fixture.
## Affected versions
I reproduced the crash on:
```text
esp-clang 20.1.1
LLVM 21.1.8
LLVM 22.1.6
LLVM trunk 23.0.0git, commit 146abed5c
```
The relevant code is unchanged at `main` `53ae585a9` on 2026-06-10, byte-identical to the `146abed5c` build I crash-tested, so it reproduces on current main.
## Expected behavior
Clang should not crash when instantiating a member variable template partial specialization from a deserialized enclosing class template.
If the primary member variable template has not yet been instantiated into the current instantiation, Clang should either instantiate it on demand or fail gracefully without dereferencing an empty lookup result.
## Actual behavior
`VisitVarTemplatePartialSpecializationDecl` looks up the instantiated primary member variable template in `Owner`. The lookup result is empty when the primary has not been materialized from the errorful preamble. Assertions builds fail the `!Found.empty()` assertion; release builds crash dereferencing the empty lookup result.
## Proposed fix
In `TemplateDeclInstantiator::VisitVarTemplatePartialSpecializationDecl`, when lookup of the primary member variable template in `Owner` returns empty, instantiate the primary member variable template on demand and retry the lookup.
The existing invariant assertions are retained. They then hold, since the on-demand instantiation populates the lookup, with a null return only as a release-safe guard if the primary still cannot be found or instantiated.
The normal non-deserialized path is unchanged. Instrumentation confirms the on-demand instantiation succeeds, is taken exactly once per affected instantiation, and creates no duplicate declarations; the member-instantiation loop does not visit the primary again.
I have a patch for this and will open a PR. The patch has zero regressions across the following test suites:
```text
SemaTemplate
PCH
Modules
CXX/temp
```
## Environment
Found while using clangd with a GCC cross-toolchain: `xtensa-esp-elf-g++ 15.2.0`, ESP-IDF, libstdc++ 15.2.0 headers. The C++ standard does not matter; gnu++23 and gnu++26 both crash. The clangd version does not matter; I reproduced the crash with clangd/LLVM 20, 21, 22, and trunk.
In the original real-world hit, the unresolved include was a generated protobuf header that had not been built yet — an everyday situation for code with generated headers, which is why clangd crash-looped continuously on this codebase.
_______________________________________________
llvm-bugs mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-bugs