[clang] Avoid invalidating specialization lookup (#196533)

Fixes a use-after-free in `ASTReader::LoadExternalSpecializationsImpl`
when loading external specializations with `-ftime-trace` enabled.

This resolves the https://github.com/llvm/llvm-project/issues/196482 and
builds upon the https://github.com/llvm/llvm-project/pull/172658

The function kept a pointer into `SpecLookups`:

```cpp
LookupTable = &It->getSecond();
```

Then it constructed a `TimeTraceScope` whose name callback calls
`getNameForDiagnostic`. That call may deserialize additional AST state
and mutate `SpecLookups`, invalidating the saved pointer before it is
later used for:

```cpp
LookupTable->Table.find(HashValue);
```

This is observed as an ASAN `heap-use-after-free` in:

```text
MultiOnDiskHashTable<LazySpecializationInfoLookupTrait>::find
ASTReader::LoadExternalSpecializationsImpl
```

The fix computes the template-argument hash and copies the lazy
specialization lookup result before constructing the time-trace scope.
This avoids retaining a `DenseMap` iterator or pointer into
`SpecLookups` across code that may trigger deserialization.

Validation from a local reproducer:

- Unpatched Clang + PCH + `-ftime-trace`: ASAN heap-use-after-free.
- Unpatched Clang + same PCH without `-ftime-trace`: passes.
- Patched Clang + PCH + `-ftime-trace`: full downstream wasm build
passes.

I cannot attach the original PCH-based reproducer publicly because the
PCH contains private project paths and serialized private headers, but
the reduced failing source no longer contains project logic and the ASAN
trace points directly at invalidation inside
`LoadExternalSpecializationsImpl`.
This commit is contained in:
mzukovec 2026-05-22 09:38:45 +02:00 committed by GitHub
parent 5162d2a47c
commit 08124b704f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 362 additions and 21 deletions

View file

@ -8734,14 +8734,16 @@ bool ASTReader::LoadExternalSpecializationsImpl(
ArrayRef<TemplateArgument> TemplateArgs) {
assert(D);
reader::LazySpecializationInfoLookupTable *LookupTable = nullptr;
if (auto It = SpecLookups.find(D); It != SpecLookups.end())
LookupTable = &It->getSecond();
if (!LookupTable)
auto It = SpecLookups.find(D);
if (It == SpecLookups.end())
return false;
// NOTE: The getNameForDiagnostic usage in the lambda may mutate the
// `SpecLookups` object.
Deserializing LookupResults(this);
auto HashValue = StableHashForTemplateArguments(TemplateArgs);
llvm::SmallVector<serialization::reader::LazySpecializationInfo, 8> Infos =
It->second.Table.find(HashValue);
llvm::TimeTraceScope TimeScope("Load External Specializations for ", [&] {
std::string Name;
llvm::raw_string_ostream OS(Name);
@ -8751,13 +8753,6 @@ bool ASTReader::LoadExternalSpecializationsImpl(
return Name;
});
Deserializing LookupResults(this);
auto HashValue = StableHashForTemplateArguments(TemplateArgs);
// Get Decl may violate the iterator from SpecLookups
llvm::SmallVector<serialization::reader::LazySpecializationInfo, 8> Infos =
LookupTable->Table.find(HashValue);
bool NewSpecsFound = false;
for (auto &Info : Infos) {
if (GetExistingDecl(Info))

View file

@ -0,0 +1,104 @@
// RUN: %clang_cc1 -std=c++20 -emit-pch -o %t %s
// RUN: %clang_cc1 -std=c++20 -include-pch %t -ftime-trace=%t.json -ftime-trace-granularity=0 -fsyntax-only %s
// expected-no-diagnostics
#ifndef HEADER_INCLUDED
#define HEADER_INCLUDED
inline namespace {
// The first declarations give f's body references to many function templates.
#define DECLARE_G(N) template <typename T> T g##N(T v) { return v; }
DECLARE_G(0)
DECLARE_G(1)
DECLARE_G(2)
DECLARE_G(3)
DECLARE_G(4)
DECLARE_G(5)
DECLARE_G(6)
DECLARE_G(7)
DECLARE_G(8)
DECLARE_G(9)
DECLARE_G(10)
DECLARE_G(11)
DECLARE_G(12)
DECLARE_G(13)
DECLARE_G(14)
DECLARE_G(15)
DECLARE_G(16)
DECLARE_G(17)
DECLARE_G(18)
DECLARE_G(19)
DECLARE_G(20)
DECLARE_G(21)
DECLARE_G(22)
DECLARE_G(23)
DECLARE_G(24)
DECLARE_G(25)
DECLARE_G(26)
DECLARE_G(27)
DECLARE_G(28)
DECLARE_G(29)
DECLARE_G(30)
DECLARE_G(31)
#undef DECLARE_G
template <typename T> T f(T v) {
return g0(v) + g1(v) + g2(v) + g3(v) + g4(v) + g5(v) + g6(v) +
g7(v) + g8(v) + g9(v) + g10(v) + g11(v) + g12(v) + g13(v) +
g14(v) + g15(v) + g16(v) + g17(v) + g18(v) + g19(v) + g20(v) +
g21(v) + g22(v) + g23(v) + g24(v) + g25(v) + g26(v) + g27(v) +
g28(v) + g29(v) + g30(v) + g31(v);
}
// These later declarations are deserialized while -ftime-trace prints the
// qualified name of a specialization lookup. Loading enough of them grows the
// ASTReader specialization DenseMap and used to invalidate the active lookup.
#define DECLARE_G(N) template <typename T> T g##N();
DECLARE_G(0)
DECLARE_G(1)
DECLARE_G(2)
DECLARE_G(3)
DECLARE_G(4)
DECLARE_G(5)
DECLARE_G(6)
DECLARE_G(7)
DECLARE_G(8)
DECLARE_G(9)
DECLARE_G(10)
DECLARE_G(11)
DECLARE_G(12)
DECLARE_G(13)
DECLARE_G(14)
DECLARE_G(15)
DECLARE_G(16)
DECLARE_G(17)
DECLARE_G(18)
DECLARE_G(19)
DECLARE_G(20)
DECLARE_G(21)
DECLARE_G(22)
DECLARE_G(23)
DECLARE_G(24)
DECLARE_G(25)
DECLARE_G(26)
DECLARE_G(27)
DECLARE_G(28)
DECLARE_G(29)
DECLARE_G(30)
DECLARE_G(31)
#undef DECLARE_G
} // namespace
#else
int x;
void i() { f(x); }
#endif

View file

@ -6,15 +6,19 @@
//
//===----------------------------------------------------------------------===//
#include "clang/AST/ASTMutationListener.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendActions.h"
#include "clang/Lex/PreprocessorOptions.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/FileUtilities.h"
#include "llvm/Support/JSON.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/TimeProfiler.h"
#include "llvm/Support/VirtualFileSystem.h"
#include "llvm/Support/raw_ostream.h"
#include <stack>
#include "gtest/gtest.h"
@ -41,10 +45,38 @@ std::string teardownProfiler() {
return OS.str().str();
}
class TestASTConsumer : public ASTConsumer {
public:
TestASTConsumer(ASTMutationListener *MutationListener)
: MutationListener(MutationListener) {}
ASTMutationListener *GetASTMutationListener() override {
return MutationListener;
}
private:
ASTMutationListener *MutationListener;
};
class TestFrontendAction : public ASTFrontendAction {
public:
TestFrontendAction(ASTMutationListener *MutationListener)
: MutationListener(MutationListener) {}
private:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
StringRef InFile) override {
return std::make_unique<TestASTConsumer>(MutationListener);
}
ASTMutationListener *MutationListener;
};
// Returns true if code compiles successfully.
// We only parse AST here. This is enough for constexpr evaluation.
bool compileFromString(StringRef Code, StringRef Standard, StringRef File,
llvm::StringMap<std::string> Headers = {}) {
llvm::StringMap<std::string> Headers = {},
ASTMutationListener *MutationListener = nullptr) {
auto FS = llvm::makeIntrusiveRefCnt<llvm::vfs::InMemoryFileSystem>();
FS->addFile(File, 0, MemoryBuffer::getMemBuffer(Code));
@ -65,16 +97,59 @@ bool compileFromString(StringRef Code, StringRef Standard, StringRef File,
Compiler.createDiagnostics();
Compiler.createFileManager();
class TestFrontendAction : public ASTFrontendAction {
private:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
StringRef InFile) override {
return std::make_unique<ASTConsumer>();
}
} Action;
TestFrontendAction Action(MutationListener);
return Compiler.ExecuteAction(Action);
}
bool compileFromArgs(ArrayRef<const char *> Args, FrontendAction &Action) {
IntrusiveRefCntPtr<llvm::vfs::FileSystem> FS = llvm::vfs::getRealFileSystem();
auto Invocation = std::make_shared<CompilerInvocation>();
DiagnosticOptions InvocationDiagOpts;
auto InvocationDiags =
CompilerInstance::createDiagnostics(*FS, InvocationDiagOpts);
if (!CompilerInvocation::CreateFromArgs(*Invocation, Args, *InvocationDiags))
return false;
CompilerInstance Compiler(std::move(Invocation));
Compiler.setVirtualFileSystem(std::move(FS));
Compiler.createDiagnostics();
Compiler.createFileManager();
return Compiler.ExecuteAction(Action);
}
struct SpecializationCounts {
unsigned ClassTemplateSpecializations = 0;
unsigned FunctionTemplateSpecializations = 0;
unsigned VarTemplateSpecializations = 0;
};
class SpecializationCountingListener : public ASTMutationListener {
public:
SpecializationCountingListener(SpecializationCounts &Counts)
: Counts(Counts) {}
void AddedCXXTemplateSpecialization(
const ClassTemplateDecl *TD,
const ClassTemplateSpecializationDecl *D) override {
++Counts.ClassTemplateSpecializations;
}
void AddedCXXTemplateSpecialization(const FunctionTemplateDecl *TD,
const FunctionDecl *D) override {
++Counts.FunctionTemplateSpecializations;
}
void AddedCXXTemplateSpecialization(
const VarTemplateDecl *TD,
const VarTemplateSpecializationDecl *D) override {
++Counts.VarTemplateSpecializations;
}
private:
SpecializationCounts &Counts;
};
std::string GetMetadata(json::Object *Event) {
std::string M;
llvm::raw_string_ostream OS(M);
@ -345,6 +420,173 @@ ExecuteCompiler
buildTraceGraph(Json));
}
static SpecializationCounts
countAddedSpecializationsFromPCH(StringRef SourceFile, StringRef PCHFile,
bool EnableTimeTrace) {
SpecializationCounts Counts;
SpecializationCountingListener Listener(Counts);
TestFrontendAction Action(&Listener);
std::string SourcePath = SourceFile.str();
std::string PCHPath = PCHFile.str();
if (EnableTimeTrace)
setupProfiler();
const char *Args[] = {"-std=c++20", "-include-pch", PCHPath.c_str(),
"-fsyntax-only", SourcePath.c_str()};
EXPECT_TRUE(compileFromArgs(Args, Action));
if (EnableTimeTrace)
(void)teardownProfiler();
return Counts;
}
TEST(TimeProfilerTest, TimeTraceDoesNotChangePCHSpecializationCount) {
StringRef Code = R"(
#ifndef HEADER_INCLUDED
#define HEADER_INCLUDED
inline namespace {
// The first declarations give f's body references to many function templates.
#define DECLARE_G(N) template <typename T> T g##N(T v) { return v; }
DECLARE_G(0)
DECLARE_G(1)
DECLARE_G(2)
DECLARE_G(3)
DECLARE_G(4)
DECLARE_G(5)
DECLARE_G(6)
DECLARE_G(7)
DECLARE_G(8)
DECLARE_G(9)
DECLARE_G(10)
DECLARE_G(11)
DECLARE_G(12)
DECLARE_G(13)
DECLARE_G(14)
DECLARE_G(15)
DECLARE_G(16)
DECLARE_G(17)
DECLARE_G(18)
DECLARE_G(19)
DECLARE_G(20)
DECLARE_G(21)
DECLARE_G(22)
DECLARE_G(23)
DECLARE_G(24)
DECLARE_G(25)
DECLARE_G(26)
DECLARE_G(27)
DECLARE_G(28)
DECLARE_G(29)
DECLARE_G(30)
DECLARE_G(31)
#undef DECLARE_G
template <typename T> T f(T v) {
return g0(v) + g1(v) + g2(v) + g3(v) + g4(v) + g5(v) + g6(v) +
g7(v) + g8(v) + g9(v) + g10(v) + g11(v) + g12(v) + g13(v) +
g14(v) + g15(v) + g16(v) + g17(v) + g18(v) + g19(v) + g20(v) +
g21(v) + g22(v) + g23(v) + g24(v) + g25(v) + g26(v) + g27(v) +
g28(v) + g29(v) + g30(v) + g31(v);
}
// These later declarations are deserialized while -ftime-trace prints the
// qualified name of a specialization lookup. Loading enough of them grows the
// ASTReader specialization DenseMap and used to invalidate the active lookup.
#define DECLARE_G(N) template <typename T> T g##N();
DECLARE_G(0)
DECLARE_G(1)
DECLARE_G(2)
DECLARE_G(3)
DECLARE_G(4)
DECLARE_G(5)
DECLARE_G(6)
DECLARE_G(7)
DECLARE_G(8)
DECLARE_G(9)
DECLARE_G(10)
DECLARE_G(11)
DECLARE_G(12)
DECLARE_G(13)
DECLARE_G(14)
DECLARE_G(15)
DECLARE_G(16)
DECLARE_G(17)
DECLARE_G(18)
DECLARE_G(19)
DECLARE_G(20)
DECLARE_G(21)
DECLARE_G(22)
DECLARE_G(23)
DECLARE_G(24)
DECLARE_G(25)
DECLARE_G(26)
DECLARE_G(27)
DECLARE_G(28)
DECLARE_G(29)
DECLARE_G(30)
DECLARE_G(31)
#undef DECLARE_G
} // namespace
#else
int x;
void i() { f(x); }
#endif
)";
int SourceFD;
SmallString<256> SourceFileName;
ASSERT_FALSE(llvm::sys::fs::createTemporaryFile(
"ftime-trace-specialization-lookup", "cpp", SourceFD, SourceFileName));
llvm::FileRemover SourceFileRemover(SourceFileName);
{
raw_fd_ostream SourceOS(SourceFD, /*shouldClose=*/true);
SourceOS << Code;
SourceOS.flush();
ASSERT_FALSE(SourceOS.error());
}
int PCHFD;
SmallString<256> PCHFileName;
ASSERT_FALSE(llvm::sys::fs::createTemporaryFile(
"ftime-trace-specialization-lookup", "pch", PCHFD, PCHFileName));
llvm::FileRemover PCHFileRemover(PCHFileName);
{
raw_fd_ostream PCHOS(PCHFD, /*shouldClose=*/true);
PCHOS.flush();
ASSERT_FALSE(PCHOS.error());
}
GeneratePCHAction GeneratePCH;
const char *PCHArgs[] = {"-std=c++20", "-emit-pch", "-o", PCHFileName.c_str(),
SourceFileName.c_str()};
ASSERT_TRUE(compileFromArgs(PCHArgs, GeneratePCH));
SpecializationCounts WithoutTimeTrace = countAddedSpecializationsFromPCH(
SourceFileName, PCHFileName, /*EnableTimeTrace=*/false);
SpecializationCounts WithTimeTrace = countAddedSpecializationsFromPCH(
SourceFileName, PCHFileName, /*EnableTimeTrace=*/true);
EXPECT_GT(WithoutTimeTrace.FunctionTemplateSpecializations, 0u);
EXPECT_EQ(WithoutTimeTrace.ClassTemplateSpecializations,
WithTimeTrace.ClassTemplateSpecializations);
EXPECT_EQ(WithoutTimeTrace.FunctionTemplateSpecializations,
WithTimeTrace.FunctionTemplateSpecializations);
EXPECT_EQ(WithoutTimeTrace.VarTemplateSpecializations,
WithTimeTrace.VarTemplateSpecializations);
}
TEST(TimeProfilerTest, ConstantEvaluationC99) {
std::string Code = R"(
struct {