This is an archive of the discontinued LLVM Phabricator instance.

[Clang] Fix crash in isCXXDeclarationSpecifier when attempting to annotate template name
ClosedPublic

Authored by shafik on Sep 20 2022, 9:13 PM.

Details

Summary

When attempting to decide if in C++17 a type template for class template argument deduction and the code is ill-formed the condition to break is checking the current token is an identifier when it should be checking if the next token is an identifier.

This fixes: https://github.com/llvm/llvm-project/issues/57495
https://github.com/llvm/llvm-project/issues/63052

Diff Detail

Event Timeline

shafik created this revision.Sep 20 2022, 9:13 PM
Herald added a project: Restricted Project. · View Herald TranscriptSep 20 2022, 9:13 PM
shafik requested review of this revision.Sep 20 2022, 9:13 PM

I have no real idea what is going on here, the parser isn't an area where I spend much time. Can you ELI5?

I have no real idea what is going on here, the parser isn't an area where I spend much time. Can you ELI5?

I am going to try but perhaps fail to explain this in more detail and more clearly.

IIUC we are trying to error recover, we get to

	  // Try to resolve the name. If it doesn't exist, assume it was
          // intended to name a type and keep disambiguating.
          switch (TryAnnotateName()) {

At this point we know the current token is :: and the next token is an identifier. We are trying to annotate the name and it could be a C++17 class template argument deduction case: https://en.cppreference.com/w/cpp/language/class_template_argument_deduction

If we annotate it we should not be left with current token as :: and the next token as an identifier, this is what the assert verifies here:

// Annotated it, check again.
assert(Tok.isNot(tok::annot_cxxscope) ||
       NextToken().isNot(tok::identifier));

So the line I modified says we should only break if we actually annotated and therefore the next token IS NOT an identifier b/c we would have advanced.

I have no real idea what is going on here, the parser isn't an area where I spend much time. Can you ELI5?

I am going to try but perhaps fail to explain this in more detail and more clearly.

IIUC we are trying to error recover, we get to

	  // Try to resolve the name. If it doesn't exist, assume it was
          // intended to name a type and keep disambiguating.
          switch (TryAnnotateName()) {

At this point we know the current token is :: and the next token is an identifier. We are trying to annotate the name and it could be a C++17 class template argument deduction case: https://en.cppreference.com/w/cpp/language/class_template_argument_deduction

If we annotate it we should not be left with current token as :: and the next token as an identifier, this is what the assert verifies here:

// Annotated it, check again.
assert(Tok.isNot(tok::annot_cxxscope) ||
       NextToken().isNot(tok::identifier));

So the line I modified says we should only break if we actually annotated and therefore the next token IS NOT an identifier b/c we would have advanced.

In that case: 1- could you document that? 2- is there some level of more-clear check you could do? Someting like "if (NextToken().is(tok::annotateTok)"? 3- Should TryAnnotateTypeOrScopeToken give SOME sort of 'I successfully did the thing?' instead?

shafik added a reviewer: Restricted Project.Jun 1 2023, 8:36 AM

Adding more reviewers for more visibility.

I does think this patch probably makes sense but I agree with Erich that a comment is in order.
I'm also wondering whether we can end up in a situation where Tok and NextToken() get replaced by a single annotation in which case this would not work. I don't think so.

rsmith requested changes to this revision.Jun 1 2023, 11:52 AM
rsmith added a subscriber: rsmith.
rsmith added inline comments.
clang/lib/Parse/ParseTentative.cpp
1659–1663

This doesn't seem correct to me. If we had scope::foo bar, and we annotate scope::foo as a type, then this will get confused by the next token now being an (unrelated) identifier. This code is trying to detect if an annotation was performed, so I think it intended to check if the current token's kind has changed, like is done on line 1295.

This revision now requires changes to proceed.Jun 1 2023, 11:52 AM
cor3ntin added inline comments.Jun 1 2023, 12:31 PM
clang/lib/Parse/ParseTentative.cpp
1659–1663

The confusing bit is that Tok is always an annotated scope already here (L1598), so TryAnnotateName should not modify that first token (unless TryAnnotateTypeOrScopeTokenAfterScopeSpec can somehow replace the current annot_cxxscope by another one, which i don't think can happen?)

pmatos added a subscriber: pmatos.Jun 1 2023, 11:04 PM
shafik updated this revision to Diff 527937.Jun 2 2023, 12:41 PM
  • Update check to use tok::annot_cxxscope
shafik marked an inline comment as done.Jun 2 2023, 12:43 PM
shafik added inline comments.
clang/lib/Parse/ParseTentative.cpp
1659–1663

Ok using tok::annot_cxxscope also works and I agree it makes sense as well, check-clang also passes.

So then is the assert below wrong?

// Annotated it, check again.
assert(Tok.isNot(tok::annot_cxxscope) ||
       NextToken().isNot(tok::identifier));

It looks like it will work by accident for most cases b/c it checks tok::annot_cxxscope first.

shafik edited the summary of this revision. (Show Details)Jun 2 2023, 1:44 PM
shafik edited the summary of this revision. (Show Details)
shafik added a comment.Jun 2 2023, 2:29 PM

clang/test/ClangScanDeps/modules-full.cpp passes locally for me and I don't see why my change would effect this test.

rsmith added inline comments.Jun 2 2023, 4:36 PM
clang/lib/Parse/ParseTentative.cpp
1659–1663

The confusing bit is that Tok is always an annotated scope already here (L1598), so TryAnnotateName should not modify that first token (unless TryAnnotateTypeOrScopeTokenAfterScopeSpec can somehow replace the current annot_cxxscope by another one, which i don't think can happen?)

Yeah, I think TryAnnotateTypeOrScopeToken shouldn't ever replace an annot_cxxscope token with a different annot_cxxscope token representing a longer scope specifier -- an annot_cxxscope token should always be as long as it can be. But it might replace the annot_cxxscope token with an annot_typename, in which case we want to jump out to line 1671 and try again.

So then is the assert below wrong?

I think it's right -- we either reach the assert if we replace the annot_cxxscope with something else (an annot_typename), in the ANK_TemplateName case, or if we've successfully annotated the name (as one of various non-identifier things), in the ANK_Success case. In either case, we only reach the assert if we successfully replaced the identifier with an annotation token, so the assert should succeed.

And the point of the assert, I think, is to ensure that the recursive call to isCXXDeclarationSpecifier cannot reach this same codepath again and recurse forever, so checking the same condition that we checked on entry seems appropriate.

cor3ntin accepted this revision.Jun 3 2023, 8:23 AM

This makes sense to me, thanks!

shafik added inline comments.Jun 6 2023, 10:14 AM
clang/lib/Parse/ParseTentative.cpp
1659–1663

The confusing bit is that Tok is always an annotated scope already here (L1598), so TryAnnotateName should not modify that first token (unless TryAnnotateTypeOrScopeTokenAfterScopeSpec can somehow replace the current annot_cxxscope by another one, which i don't think can happen?)

Yeah, I think TryAnnotateTypeOrScopeToken shouldn't ever replace an annot_cxxscope token with a different annot_cxxscope token representing a longer scope specifier -- an annot_cxxscope token should always be as long as it can be. But it might replace the annot_cxxscope token with an annot_typename, in which case we want to jump out to line 1671 and try again.

I see the code that can generate annot_typename but I am so far not able to come up w/ a scenario that hits that case. So I am a little hesitant to handle that w/o adding a test that covers it. Although using

if (Tok.isNot(tok::annot_cxxscope) || Tok.is(tok::annot_typename))
                 break;

does pass check-clang

I think I am going to land this as is and if we can come up w/ an example that covers the annot_typename I can do a follow-up. As this is now it fixes a crash bug.

This revision was not accepted when it landed; it landed in state Needs Review.Jun 29 2023, 3:43 PM
This revision was landed with ongoing or failed builds.
This revision was automatically updated to reflect the committed changes.
Herald added a project: Restricted Project. · View Herald TranscriptJun 29 2023, 3:43 PM