[C++20][Modules] Prevent premature calls to PassInterestingDeclsToConsumer() within FinishedDeserializing(). (#129982)

`ASTReader::FinishedDeserializing` uses `NumCurrentElementsDeserializing` to keep track of nested `Deserializing` RAII actions. The `FinishedDeserializing` only performs actions if it is the top-level `Deserializing` layer. This works fine in general, but there is a problematic edge case.

If a call to `redecls()` in `FinishedDeserializing` performs deserialization, we re-enter `FinishedDeserializing` while in the middle of the previous `FinishedDeserializing` call.

The known problematic part of this is that this inner `FinishedDeserializing` can go all the way to `PassInterestingDeclsToConsumer`, which operates on `PotentiallyInterestingDecls` data structure which contain decls that should be handled by the previous `FinishedDeserializing` stage.

The other shared data structures are also somewhat concerning at a high-level in that the inner `FinishedDeserializing` would be handling pending actions that are not "within its scope", but this part is not known to be problematic.

We already have a guard within `PassInterestingDeclsToConsumer` because we can end up with recursive deserialization within `PassInterestingDeclsToConsumer`. The implemented solution is to apply this guard to the portion of `FinishedDeserializing` that performs further deserialization as well. This ensures that recursive deserialization does not trigger `PassInterestingDeclsToConsumer` which may operate on entries that are not ready to be passed.
This commit is contained in:
Michael Park 2025-03-15 23:03:20 -07:00 committed by GitHub
parent 508db53d1a
commit 0689d23ab3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 296 additions and 56 deletions

View File

@ -278,6 +278,9 @@ Bug Fixes in This Version
considered an error in C23 mode and are allowed as an extension in earlier language modes.
- Remove the ``static`` specifier for the value of ``_FUNCTION_`` for static functions, in MSVC compatibility mode.
- Fixed a modules crash where exception specifications were not propagated properly (#GH121245, relanded in #GH129982)
- Fixed a problematic case with recursive deserialization within ``FinishedDeserializing()`` where
``PassInterestingDeclsToConsumer()`` was called before the declarations were safe to be passed. (#GH129982)
Bug Fixes to Compiler Builtins
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -1176,11 +1176,11 @@ private:
/// Number of Decl/types that are currently deserializing.
unsigned NumCurrentElementsDeserializing = 0;
/// Set true while we are in the process of passing deserialized
/// "interesting" decls to consumer inside FinishedDeserializing().
/// This is used as a guard to avoid recursively repeating the process of
/// Set false while we are in a state where we cannot safely pass deserialized
/// "interesting" decls to the consumer inside FinishedDeserializing().
/// This is used as a guard to avoid recursively entering the process of
/// passing decls to consumer.
bool PassingDeclsToConsumer = false;
bool CanPassDeclsToConsumer = true;
/// The set of identifiers that were read while the AST reader was
/// (recursively) loading declarations.

View File

@ -10182,12 +10182,12 @@ void ASTReader::visitTopLevelModuleMaps(
}
void ASTReader::finishPendingActions() {
while (
!PendingIdentifierInfos.empty() || !PendingDeducedFunctionTypes.empty() ||
!PendingDeducedVarTypes.empty() || !PendingIncompleteDeclChains.empty() ||
!PendingDeclChains.empty() || !PendingMacroIDs.empty() ||
!PendingDeclContextInfos.empty() || !PendingUpdateRecords.empty() ||
!PendingObjCExtensionIvarRedeclarations.empty()) {
while (!PendingIdentifierInfos.empty() ||
!PendingDeducedFunctionTypes.empty() ||
!PendingDeducedVarTypes.empty() || !PendingDeclChains.empty() ||
!PendingMacroIDs.empty() || !PendingDeclContextInfos.empty() ||
!PendingUpdateRecords.empty() ||
!PendingObjCExtensionIvarRedeclarations.empty()) {
// If any identifiers with corresponding top-level declarations have
// been loaded, load those declarations now.
using TopLevelDeclsMap =
@ -10235,13 +10235,6 @@ void ASTReader::finishPendingActions() {
}
PendingDeducedVarTypes.clear();
// For each decl chain that we wanted to complete while deserializing, mark
// it as "still needs to be completed".
for (unsigned I = 0; I != PendingIncompleteDeclChains.size(); ++I) {
markIncompleteDeclChain(PendingIncompleteDeclChains[I]);
}
PendingIncompleteDeclChains.clear();
// Load pending declaration chains.
for (unsigned I = 0; I != PendingDeclChains.size(); ++I)
loadPendingDeclChain(PendingDeclChains[I].first,
@ -10479,6 +10472,43 @@ void ASTReader::finishPendingActions() {
for (auto *ND : PendingMergedDefinitionsToDeduplicate)
getContext().deduplicateMergedDefinitonsFor(ND);
PendingMergedDefinitionsToDeduplicate.clear();
// For each decl chain that we wanted to complete while deserializing, mark
// it as "still needs to be completed".
for (Decl *D : PendingIncompleteDeclChains)
markIncompleteDeclChain(D);
PendingIncompleteDeclChains.clear();
assert(PendingIdentifierInfos.empty() &&
"Should be empty at the end of finishPendingActions");
assert(PendingDeducedFunctionTypes.empty() &&
"Should be empty at the end of finishPendingActions");
assert(PendingDeducedVarTypes.empty() &&
"Should be empty at the end of finishPendingActions");
assert(PendingDeclChains.empty() &&
"Should be empty at the end of finishPendingActions");
assert(PendingMacroIDs.empty() &&
"Should be empty at the end of finishPendingActions");
assert(PendingDeclContextInfos.empty() &&
"Should be empty at the end of finishPendingActions");
assert(PendingUpdateRecords.empty() &&
"Should be empty at the end of finishPendingActions");
assert(PendingObjCExtensionIvarRedeclarations.empty() &&
"Should be empty at the end of finishPendingActions");
assert(PendingFakeDefinitionData.empty() &&
"Should be empty at the end of finishPendingActions");
assert(PendingDefinitions.empty() &&
"Should be empty at the end of finishPendingActions");
assert(PendingWarningForDuplicatedDefsInModuleUnits.empty() &&
"Should be empty at the end of finishPendingActions");
assert(PendingBodies.empty() &&
"Should be empty at the end of finishPendingActions");
assert(PendingAddedClassMembers.empty() &&
"Should be empty at the end of finishPendingActions");
assert(PendingMergedDefinitionsToDeduplicate.empty() &&
"Should be empty at the end of finishPendingActions");
assert(PendingIncompleteDeclChains.empty() &&
"Should be empty at the end of finishPendingActions");
}
void ASTReader::diagnoseOdrViolations() {
@ -10792,48 +10822,55 @@ void ASTReader::FinishedDeserializing() {
--NumCurrentElementsDeserializing;
if (NumCurrentElementsDeserializing == 0) {
// Propagate exception specification and deduced type updates along
// redeclaration chains.
//
// We do this now rather than in finishPendingActions because we want to
// be able to walk the complete redeclaration chains of the updated decls.
while (!PendingExceptionSpecUpdates.empty() ||
!PendingDeducedTypeUpdates.empty() ||
!PendingUndeducedFunctionDecls.empty()) {
auto ESUpdates = std::move(PendingExceptionSpecUpdates);
PendingExceptionSpecUpdates.clear();
for (auto Update : ESUpdates) {
ProcessingUpdatesRAIIObj ProcessingUpdates(*this);
auto *FPT = Update.second->getType()->castAs<FunctionProtoType>();
auto ESI = FPT->getExtProtoInfo().ExceptionSpec;
if (auto *Listener = getContext().getASTMutationListener())
Listener->ResolvedExceptionSpec(cast<FunctionDecl>(Update.second));
for (auto *Redecl : Update.second->redecls())
getContext().adjustExceptionSpec(cast<FunctionDecl>(Redecl), ESI);
{
// Guard variable to avoid recursively entering the process of passing
// decls to consumer.
SaveAndRestore GuardPassingDeclsToConsumer(CanPassDeclsToConsumer, false);
// Propagate exception specification and deduced type updates along
// redeclaration chains.
//
// We do this now rather than in finishPendingActions because we want to
// be able to walk the complete redeclaration chains of the updated decls.
while (!PendingExceptionSpecUpdates.empty() ||
!PendingDeducedTypeUpdates.empty() ||
!PendingUndeducedFunctionDecls.empty()) {
auto ESUpdates = std::move(PendingExceptionSpecUpdates);
PendingExceptionSpecUpdates.clear();
for (auto Update : ESUpdates) {
ProcessingUpdatesRAIIObj ProcessingUpdates(*this);
auto *FPT = Update.second->getType()->castAs<FunctionProtoType>();
auto ESI = FPT->getExtProtoInfo().ExceptionSpec;
if (auto *Listener = getContext().getASTMutationListener())
Listener->ResolvedExceptionSpec(cast<FunctionDecl>(Update.second));
for (auto *Redecl : Update.second->redecls())
getContext().adjustExceptionSpec(cast<FunctionDecl>(Redecl), ESI);
}
auto DTUpdates = std::move(PendingDeducedTypeUpdates);
PendingDeducedTypeUpdates.clear();
for (auto Update : DTUpdates) {
ProcessingUpdatesRAIIObj ProcessingUpdates(*this);
// FIXME: If the return type is already deduced, check that it
// matches.
getContext().adjustDeducedFunctionResultType(Update.first,
Update.second);
}
auto UDTUpdates = std::move(PendingUndeducedFunctionDecls);
PendingUndeducedFunctionDecls.clear();
// We hope we can find the deduced type for the functions by iterating
// redeclarations in other modules.
for (FunctionDecl *UndeducedFD : UDTUpdates)
(void)UndeducedFD->getMostRecentDecl();
}
auto DTUpdates = std::move(PendingDeducedTypeUpdates);
PendingDeducedTypeUpdates.clear();
for (auto Update : DTUpdates) {
ProcessingUpdatesRAIIObj ProcessingUpdates(*this);
// FIXME: If the return type is already deduced, check that it matches.
getContext().adjustDeducedFunctionResultType(Update.first,
Update.second);
}
if (ReadTimer)
ReadTimer->stopTimer();
auto UDTUpdates = std::move(PendingUndeducedFunctionDecls);
PendingUndeducedFunctionDecls.clear();
// We hope we can find the deduced type for the functions by iterating
// redeclarations in other modules.
for (FunctionDecl *UndeducedFD : UDTUpdates)
(void)UndeducedFD->getMostRecentDecl();
diagnoseOdrViolations();
}
if (ReadTimer)
ReadTimer->stopTimer();
diagnoseOdrViolations();
// We are not in recursive loading, so it's safe to pass the "interesting"
// decls to the consumer.
if (Consumer)

View File

@ -4309,12 +4309,12 @@ Decl *ASTReader::ReadDeclRecord(GlobalDeclID ID) {
void ASTReader::PassInterestingDeclsToConsumer() {
assert(Consumer);
if (PassingDeclsToConsumer)
if (!CanPassDeclsToConsumer)
return;
// Guard variable to avoid recursively redoing the process of passing
// decls to consumer.
SaveAndRestore GuardPassingDeclsToConsumer(PassingDeclsToConsumer, true);
SaveAndRestore GuardPassingDeclsToConsumer(CanPassDeclsToConsumer, false);
// Ensure that we've loaded all potentially-interesting declarations
// that need to be eagerly loaded.

View File

@ -0,0 +1,93 @@
// If this test fails, it should be investigated under Debug builds.
// Before the PR, this test was encountering an `llvm_unreachable()`.
// RUN: rm -rf %t
// RUN: mkdir -p %t
// RUN: split-file %s %t
// RUN: cd %t
// RUN: %clang_cc1 -std=c++20 -emit-header-unit -xc++-user-header %t/hu-01.h \
// RUN: -fcxx-exceptions -o %t/hu-01.pcm
// RUN: %clang_cc1 -std=c++20 -emit-header-unit -xc++-user-header %t/hu-02.h \
// RUN: -Wno-experimental-header-units -fcxx-exceptions \
// RUN: -fmodule-file=%t/hu-01.pcm -o %t/hu-02.pcm
// RUN: %clang_cc1 -std=c++20 -emit-header-unit -xc++-user-header %t/hu-03.h \
// RUN: -Wno-experimental-header-units -fcxx-exceptions \
// RUN: -fmodule-file=%t/hu-01.pcm -o %t/hu-03.pcm
// RUN: %clang_cc1 -std=c++20 -emit-header-unit -xc++-user-header %t/hu-04.h \
// RUN: -Wno-experimental-header-units -fcxx-exceptions \
// RUN: -fmodule-file=%t/hu-01.pcm -o %t/hu-04.pcm
// RUN: %clang_cc1 -std=c++20 -emit-header-unit -xc++-user-header %t/hu-05.h \
// RUN: -Wno-experimental-header-units -fcxx-exceptions \
// RUN: -fmodule-file=%t/hu-03.pcm -fmodule-file=%t/hu-04.pcm \
// RUN: -fmodule-file=%t/hu-01.pcm -o %t/hu-05.pcm
// RUN: %clang_cc1 -std=c++20 -emit-obj %t/main.cpp \
// RUN: -Wno-experimental-header-units -fcxx-exceptions \
// RUN: -fmodule-file=%t/hu-02.pcm -fmodule-file=%t/hu-05.pcm \
// RUN: -fmodule-file=%t/hu-04.pcm -fmodule-file=%t/hu-03.pcm \
// RUN: -fmodule-file=%t/hu-01.pcm
//--- hu-01.h
template <typename T>
struct A {
A() {}
~A() {}
};
template <typename T>
struct EBO : T {
EBO() = default;
};
template <typename T>
struct HT : EBO<A<T>> {};
//--- hu-02.h
import "hu-01.h";
inline void f() {
HT<int>();
}
//--- hu-03.h
import "hu-01.h";
struct C {
C();
HT<long> _;
};
//--- hu-04.h
import "hu-01.h";
void g(HT<long> = {});
//--- hu-05.h
import "hu-03.h";
import "hu-04.h";
import "hu-01.h";
struct B {
virtual ~B() = default;
virtual void f() {
HT<long>();
}
};
//--- main.cpp
import "hu-02.h";
import "hu-05.h";
import "hu-03.h";
int main() {
f();
C();
B();
}

View File

@ -0,0 +1,107 @@
// If this test fails, it should be investigated under Debug builds.
// Before the PR, this test was violating an assertion.
// RUN: rm -rf %t
// RUN: mkdir -p %t
// RUN: split-file %s %t
// RUN: %clang_cc1 -std=c++20 -emit-obj -fmodules \
// RUN: -fmodule-map-file=%t/module.modulemap \
// RUN: -fmodules-cache-path=%t %t/a.cpp
//--- module.modulemap
module ebo {
header "ebo.h"
}
module fwd {
header "fwd.h"
}
module s {
header "s.h"
export *
}
module mod {
header "a.h"
header "b.h"
}
//--- ebo.h
#pragma once
namespace N { inline namespace __1 {
template <typename T>
struct EBO : T {
EBO() = default;
};
}}
//--- fwd.h
#pragma once
namespace N { inline namespace __1 {
template <typename T>
struct Empty;
template <typename T>
struct BS;
using S = BS<Empty<char>>;
}}
//--- s.h
#pragma once
#include "fwd.h"
#include "ebo.h"
namespace N { inline namespace __1 {
template <typename T>
struct Empty {};
template <typename T>
struct BS {
EBO<T> _;
void f();
};
extern template void BS<Empty<char>>::f();
}}
//--- b.h
#pragma once
#include "s.h"
struct B {
void f() {
N::S{}.f();
}
};
//--- a.h
#pragma once
#include "s.h"
struct A {
void f(int) {}
void f(const N::S &) {}
void g();
};
//--- a.cpp
#include "a.h"
void A::g() { f(0); }
// expected-no-diagnostics