This is an archive of the discontinued LLVM Phabricator instance.

[ASTReader][ASTWriter][PCH][Modules] Fix (de)serialization of SwitchCases
AbandonedPublic

Authored by martong on Jul 1 2020, 2:36 AM.

Details

Summary

https://github.com/llvm/llvm-project/commit/05843dc6ab97a00cbde7aa4f08bf3692eb83109d
introduces a regression: deserialization a function crashes if it has a switch
and a lambda with a switch (see the test case).
In this case the AST contains the exact same SwitchStmt both in the lambda's
CXXMethodDecl and as a child of the LambdaExpr:

-LambdaExpr 0x55fc0fb0b4a0 <col:12, line:22:3>
 |-CXXRecordDecl 0x55fc0fb0afc8 <line:19:12> col:12 implicit class definition
 | |-CXXMethodDecl 0x55fc0fb0b108 <col:14, line:22:3> line:19:12 constexpr operator() 'auto () const -> void' inline
 | | `-CompoundStmt 0x55fc0fb0b2b0 <col:16, line:22:3>
 | |   `-SwitchStmt 0x55fc0fb0b208 <line:20:5, line:21:12>
 `-CompoundStmt 0x55fc0fb0b2b0 <col:16, line:22:3>
   `-SwitchStmt 0x55fc0fb0b208 <line:20:5, line:21:12>

During the serialization, each SwitchCase gets an id that is unique in a function.
Durint the deserialization, we fail to properly clear the existing ids, thus we receive the

Assertion `(*CurrSwitchCaseStmts)[ID] == nullptr && "Already have a SwitchCase with this ID"' failed

This patch introduces a solution which removes the enforcment of unique ids per function.
We track the ids and make them unique per PCH.

Diff Detail

Event Timeline

martong created this revision.Jul 1 2020, 2:36 AM
Herald added a project: Restricted Project. · View Herald TranscriptJul 1 2020, 2:36 AM
aaron.ballman accepted this revision.Jul 1 2020, 3:16 AM

LGTM, thank you for the fix!

This revision is now accepted and ready to land.Jul 1 2020, 3:16 AM

Let me take a look.

Reduced test case:

class C {} b;
void should_not_crash_with_switch_in_lambda() {
  switch (1)
  default:;
  auto f = [] {
    switch (1)
    default:;
  };
}

LGTM, thank you for the fix!

Wait, I am not sure that it is the right fix. The body of the lambda should not be different from the body of the CXXMethodDecl.

aaron.ballman requested changes to this revision.Jul 1 2020, 3:54 AM

LGTM, thank you for the fix!

Wait, I am not sure that it is the right fix. The body of the lambda should not be different from the body of the CXXMethodDecl.

I'm not certain I understand, but I'm removing my LG while we figure it out. Can you expound a bit further?

This revision now requires changes to proceed.Jul 1 2020, 3:54 AM

This patch addresses a crash we started seeing in our CTU analysis runs as a result of 05843dc6ab97a00cbde7aa4f08bf3692eb83109d. Gabor, maybe I can generate a regression test for the problems that 05843dc6ab97a00cbde7aa4f08bf3692eb83109d caused as a follow up for whatever is committed here?

I'm not certain I understand, but I'm removing my LG while we figure it out. Can you expound a bit further?

Take a look at the AST dump of a small modification of the reduced test case which doesn't trigger the assertion:

void should_not_crash_with_switch_in_lambda() {
  switch (1)
  default:;
  auto f = [] {
    switch (1)
    default:;
  };
}

Before the serialization of LambdaExpr for the LambdaExpr *E, we have E->getBody() == E->getCallOperator()->getBody().
After serialization this is not true anymore because we are writing and reading the body directly when visiting the LambdaExpr
(the captures have the same problem).

I'm not certain I understand, but I'm removing my LG while we figure it out. Can you expound a bit further?

Take a look at the AST dump of a small modification of the reduced test case which doesn't trigger the assertion:

void should_not_crash_with_switch_in_lambda() {
  switch (1)
  default:;
  auto f = [] {
    switch (1)
    default:;
  };
}

Before the serialization of LambdaExpr for the LambdaExpr *E, we have E->getBody() == E->getCallOperator()->getBody().
After serialization this is not true anymore because we are writing and reading the body directly when visiting the LambdaExpr
(the captures have the same problem).

Ah, thank you for the explanation! That does seem like an issue, good catch.

martong added a comment.EditedJul 1 2020, 6:42 AM

In case of the reproducer the problem is that we clear the switch ids in GetExternalDeclStmt. But we read the SwitchCase with the same ID twice from the same call of GetExternalDeclStmt (bold-italic in the below call stacks):

  • 1)

clang::ASTReader::RecordSwitchCaseID (this=0x4d5710, SC=0x5022f0, ID=0) at ../../git/llvm-project/clang/lib/Serialization/ASTReader.cpp:9070
0x00007ffff0313097 in clang::ASTRecordReader::recordSwitchCaseID (this=0x7fffffff7ca8, SC=0x5022f0, ID=0) at ../../git/llvm-project/clang/include/clang/Serialization/ASTRecordReader.h:331
0x00007ffff0300ebc in clang::ASTStmtReader::VisitSwitchCase (this=0x7fffffff7c98, S=0x5022f0) at ../../git/llvm-project/clang/lib/Serialization/ASTReaderStmt.cpp:165
0x00007ffff0300f3f in clang::ASTStmtReader::VisitCaseStmt (this=0x7fffffff7c98, S=0x5022f0) at ../../git/llvm-project/clang/lib/Serialization/ASTReaderStmt.cpp:171
0x00007ffff031e768 in clang::StmtVisitorBase<std::add_pointer, clang::ASTStmtReader, void>::Visit (this=0x7fffffff7c98, S=0x5022f0) at tools/clang/include/clang/AST/StmtNodes.inc:561
0x00007ffff0312b7c in clang::ASTReader::ReadStmtFromStream (this=0x4d5710, F=...) at ../../git/llvm-project/clang/lib/Serialization/ASTReaderStmt.cpp:3896
0x00007ffff030eb2e in clang::ASTReader::ReadStmt (this=0x4d5710, F=...) at ../../git/llvm-project/clang/lib/Serialization/ASTReaderStmt.cpp:2696
0x00007ffff0312e2d in clang::ASTReader::ReadExpr (this=0x4d5710, F=...) at ../../git/llvm-project/clang/lib/Serialization/ASTReaderStmt.cpp:2705
0x00007ffff01cbb10 in clang::ASTRecordReader::readExpr (this=0x7fffffff8450) at ../../git/llvm-project/clang/include/clang/Serialization/ASTRecordReader.h:131
0x00007ffff0293b23 in clang::ASTDeclReader::VisitVarDeclImpl (this=0x7fffffff8408, VD=0x5010d0) at ../../git/llvm-project/clang/lib/Serialization/ASTReaderDecl.cpp:1419
0x00007ffff02c40ad in clang::ASTDeclReader::VisitVarDecl (this=0x7fffffff8408, VD=0x5010d0) at ../../git/llvm-project/clang/lib/Serialization/ASTReaderDecl.cpp:371
0x00007ffff02be24c in clang::declvisitor::Base<std::add_pointer, clang::ASTDeclReader, void>::Visit (this=0x7fffffff8408, D=0x5010d0) at tools/clang/include/clang/AST/DeclNodes.inc:453
0x00007ffff028e5ca in clang::ASTDeclReader::Visit (this=0x7fffffff8408, D=0x5010d0) at ../../git/llvm-project/clang/lib/Serialization/ASTReaderDecl.cpp:519
0x00007ffff02b938f in clang::ASTReader::ReadDeclRecord (this=0x4d5710, ID=29) at ../../git/llvm-project/clang/lib/Serialization/ASTReaderDecl.cpp:4044
0x00007ffff018c714 in clang::ASTReader::GetDecl (this=0x4d5710, ID=29) at ../../git/llvm-project/clang/lib/Serialization/ASTReader.cpp:7422
0x00007ffff021f9fa in clang::ASTReader::ReadDecl (this=0x4d5710, F=..., R=llvm::SmallVector of Size 3, Capacity 64 = {...}, I=@0x7fffffff9830: 3) at ../../git/llvm-project/clang/include/clang/Serialization/ASTReader.h:1863
0x00007ffff021f9a6 in clang::ASTRecordReader::readDecl (this=0x7fffffff9818) at ../../git/llvm-project/clang/include/clang/Serialization/ASTRecordReader.h:192
0x00007ffff0313d88 in clang::ASTStmtReader::readDecl (this=0x7fffffff9808) at ../../git/llvm-project/clang/lib/Serialization/ASTReaderStmt.cpp:92
0x00007ffff0301d6a in clang::ASTStmtReader::VisitDeclStmt (this=0x7fffffff9808, S=0x5010b0) at ../../git/llvm-project/clang/lib/Serialization/ASTReaderStmt.cpp:340
0x00007ffff031e078 in clang::StmtVisitorBase<std::add_pointer, clang::ASTStmtReader, void>::Visit (this=0x7fffffff9808, S=0x5010b0) at tools/clang/include/clang/AST/StmtNodes.inc:97
0x00007ffff0312b7c in clang::ASTReader::ReadStmtFromStream (this=0x4d5710, F=...) at ../../git/llvm-project/clang/lib/Serialization/ASTReaderStmt.cpp:3896
0x00007ffff019a07c in clang::ASTReader::GetExternalDeclStmt (this=0x4d5710, Offset=107033) at ../../git/llvm-project/clang/lib/Serialization/ASTReader.cpp:7476

  • 2)

clang::ASTReader::RecordSwitchCaseID (this=0x4d5710, SC=0x502440, ID=0) at ../../git/llvm-project/clang/lib/Serialization/ASTReader.cpp:9070
0x00007ffff0313097 in clang::ASTRecordReader::recordSwitchCaseID (this=0x7fffffff9818, SC=0x502440, ID=0) at ../../git/llvm-project/clang/include/clang/Serialization/ASTRecordReader.h:331
0x00007ffff0300ebc in clang::ASTStmtReader::VisitSwitchCase (this=0x7fffffff9808, S=0x502440) at ../../git/llvm-project/clang/lib/Serialization/ASTReaderStmt.cpp:165
0x00007ffff030102f in clang::ASTStmtReader::VisitDefaultStmt (this=0x7fffffff9808, S=0x502440) at ../../git/llvm-project/clang/lib/Serialization/ASTReaderStmt.cpp:182
0x00007ffff031e780 in clang::StmtVisitorBase<std::add_pointer, clang::ASTStmtReader, void>::Visit (this=0x7fffffff9808, S=0x502440) at tools/clang/include/clang/AST/StmtNodes.inc:567
0x00007ffff0312b7c in clang::ASTReader::ReadStmtFromStream (this=0x4d5710, F=...) at ../../git/llvm-project/clang/lib/Serialization/ASTReaderStmt.cpp:3896
0x00007ffff019a07c in clang::ASTReader::GetExternalDeclStmt (this=0x4d5710, Offset=107033) at ../../git/llvm-project/clang/lib/Serialization/ASTReader.cpp:7476

(In Bruno's modified example we always see the 2) call stack, meaning that the ids are cleared and everything is ok.)

My first attempt for the fix was to try to call ClearSwitchCaseIDs in VisitVarDecl, but that fired the other assertion in getSwitchCaseWithID for some other test cases.

The second attempt was to clear the SwitchCaseIDs in ASTStmtReader::VisitSwitchStmt and that indeed succeeded with the particular reproducer, but the original problem (reported by @vabridgers) was still crashing. Unfortunately, I could not create another minimal reproducer for this attempt, seems like there must be some different order of lazy loading of bodies to get the crash. So, I suspected there is something fundamentally wrong with the tracking of the SwitchCases and I came up with this patch.

I'm not certain I understand, but I'm removing my LG while we figure it out. Can you expound a bit further?

Take a look at the AST dump of a small modification of the reduced test case which doesn't trigger the assertion:

void should_not_crash_with_switch_in_lambda() {
  switch (1)
  default:;
  auto f = [] {
    switch (1)
    default:;
  };
}

Before the serialization of LambdaExpr for the LambdaExpr *E, we have E->getBody() == E->getCallOperator()->getBody().
After serialization this is not true anymore because we are writing and reading the body directly when visiting the LambdaExpr
(the captures have the same problem).

The same is true in case of the simplest lambda example:

void foo() {   // line 11
    []{};
}

The AST that we got from the Parser has only one CompoundStmt node, but referenced in the CXXMethodDecl and added as a direct child to LambdaExpr:

`-LambdaExpr 0x5643235372c8 <line:12:5, col:8>
  |-CXXRecordDecl 0x564323536c98 <col:5> col:5 implicit class definition
  | |-CXXMethodDecl 0x564323536dd8 <col:6, col:8> col:5 constexpr operator() 'auto () const -> void' inline
  | | `-CompoundStmt 0x564323536e88 <col:7, col:8>
  `-CompoundStmt 0x564323536e88 <col:7, col:8>

After serialization and deserialization, the CompoundStmt is just copied, notice the different pointer values:

`-LambdaExpr 0x55a49f35f830 <line:12:5, col:8>
  |-CXXRecordDecl 0x55a49f35f860 <col:5> col:5 imported implicit <undeserialized declarations> class definition
  | |-CXXMethodDecl 0x55a49f35fac8 <col:6, col:8> col:5 imported constexpr operator() 'auto () const -> void' inline
  | | `-CompoundStmt 0x55a49f360110 <col:7, col:8>
  `-CompoundStmt 0x55a49f35f820 <col:7, col:8>

This suggests that we should prepare the ASTReader/ASTWriter to be able to handle references to Stmts similarly as it can already handle references to Decls (see ASTWriter::DeclIDs).
Does this sound as a good direction, should I move forward this way?

I'm not certain I understand, but I'm removing my LG while we figure it out. Can you expound a bit further?

Take a look at the AST dump of a small modification of the reduced test case which doesn't trigger the assertion:

void should_not_crash_with_switch_in_lambda() {
  switch (1)
  default:;
  auto f = [] {
    switch (1)
    default:;
  };
}

Before the serialization of LambdaExpr for the LambdaExpr *E, we have E->getBody() == E->getCallOperator()->getBody().
After serialization this is not true anymore because we are writing and reading the body directly when visiting the LambdaExpr
(the captures have the same problem).

The same is true in case of the simplest lambda example:

void foo() {   // line 11
    []{};
}

The AST that we got from the Parser has only one CompoundStmt node, but referenced in the CXXMethodDecl and added as a direct child to LambdaExpr:

`-LambdaExpr 0x5643235372c8 <line:12:5, col:8>
  |-CXXRecordDecl 0x564323536c98 <col:5> col:5 implicit class definition
  | |-CXXMethodDecl 0x564323536dd8 <col:6, col:8> col:5 constexpr operator() 'auto () const -> void' inline
  | | `-CompoundStmt 0x564323536e88 <col:7, col:8>
  `-CompoundStmt 0x564323536e88 <col:7, col:8>

After serialization and deserialization, the CompoundStmt is just copied, notice the different pointer values:

`-LambdaExpr 0x55a49f35f830 <line:12:5, col:8>
  |-CXXRecordDecl 0x55a49f35f860 <col:5> col:5 imported implicit <undeserialized declarations> class definition
  | |-CXXMethodDecl 0x55a49f35fac8 <col:6, col:8> col:5 imported constexpr operator() 'auto () const -> void' inline
  | | `-CompoundStmt 0x55a49f360110 <col:7, col:8>
  `-CompoundStmt 0x55a49f35f820 <col:7, col:8>

This suggests that we should prepare the ASTReader/ASTWriter to be able to handle references to Stmts similarly as it can already handle references to Decls (see ASTWriter::DeclIDs).
Does this sound as a good direction, should I move forward this way?

Yes, I think that my patch just traded-off a bug for another bug. What about the following instead:

Initially zero the Stmt * for the body, then when the body is needed (either with getBody or with children)
populate it if needed from the call operator. This is what was done before with getBody.

This preserve the benefit of having a lazily deserialized body while being a simple fix.

Thoughts? Am I missing something?

If not I can cook-up a patch either tonight or tomorrow.

Yes, I think that my patch just traded-off a bug for another bug. What about the following instead:

Initially zero the Stmt * for the body, then when the body is needed (either with getBody or with children)
populate it if needed from the call operator. This is what was done before with getBody.

This preserve the benefit of having a lazily deserialized body while being a simple fix.

Thoughts? Am I missing something?

If not I can cook-up a patch either tonight or tomorrow.

D83009

martong abandoned this revision.Jul 2 2020, 2:34 AM

Abandoning for a better fix in D83009