This is an archive of the discontinued LLVM Phabricator instance.

[analyzer] Fix assertion failure with conflicting prototype calls
ClosedPublic

Authored by steakhal on Oct 18 2022, 5:29 AM.

Details

Summary

It turns out we can reach the Init.castAs<nonlock::CompoundVal>()
expression with other kinds of SVals. Such as by nonloc::ConcreteInt
in this example: https://godbolt.org/z/s4fdxrcs9

int buffer[10];
void b();
void top() {
  b(&buffer);
}
void b(int *c) {
  *c = 42; // would crash
}

In this example, we try to store 42 to the Elem{buffer, 0}.

This situation can appear if the CallExpr refers to a function
declaration without prototype. In such cases, the engine will pick the
redecl of the referred function decl which has function body, hence has
a function prototype.

This weird situation will have an interesting effect to the AST, such as
the argument at the callsite will miss a cast, which would cast the
int (*)[10] expression into int *, which means that when we evaluate
the *c = 42 expression, we want to bind 42 to an array, causing the
crash.

Look at the AST of the callsite with and without the function prototype: https://godbolt.org/z/Gncebcbdb
The only difference is that without the proper function prototype, we will not have the ImplicitCastExpr BitCasting from int (*)[10] to int * to match the expected type of the parameter declaration.

In this patch, I'm proposing to emit a cast in the mentioned edge-case,
to bind the argument value of the expected type to the parameter.

I'm only proposing this if the runtime definition has exactly the same
number of parameters as the callsite feeds it by arguments.
If that's not the case, I believe, we are better off by binding Unknown
to those parameters.

Diff Detail

Event Timeline

steakhal created this revision.Oct 18 2022, 5:29 AM
Herald added a project: Restricted Project. · View Herald TranscriptOct 18 2022, 5:29 AM
steakhal requested review of this revision.Oct 18 2022, 5:29 AM
Herald added a project: Restricted Project. · View Herald TranscriptOct 18 2022, 5:29 AM
Herald added a subscriber: cfe-commits. · View Herald Transcript

Hmm, seems like the conflicting prototype (i.e. the obsolescent use of zero parameters) is needed to reproduce the assertion failure. That makes me wonder, how does the redecl chain of b looks like? Is void b() chained with void b(int*), or are they represented independently from each other? I guess they form the same redecl chain. Which drives us to the next questions.
When the analyzer reaches the CallExpr b(&buffer) which FunctionDecl does it see? Is it b() or b(int*)? My bet, it sees and works with b(). Could we detect if the arguments of the CallExpr does not match the parameters of the FunctionDecl? And if that is the case, could we iterate through the redecl chain to find an appropriate matching FunctionDecl? That would be b(int*) in this case ... and the original bindArray() should work then.

A similar situation could happen if we reinterpret cast pointers, etc. so the situation is not limited to conflicting function prototypes.

Please provide tests for those cases.

Hmm, seems like the conflicting prototype (i.e. the obsolescent use of zero parameters) is needed to reproduce the assertion failure. That makes me wonder, how does the redecl chain of b looks like? Is void b() chained with void b(int*), or are they represented independently from each other? I guess they form the same redecl chain. Which drives us to the next questions.
When the analyzer reaches the CallExpr b(&buffer) which FunctionDecl does it see? Is it b() or b(int*)? My bet, it sees and works with b().

Yes, they form the same redecl chain, indeed.
The call b(&buffer) refers to the void () decl - with no parameters.
The range of ->redecls() of that decl has two items:
The decl of void (), and the decl of void (int *).

Could we detect if the arguments of the CallExpr does not match the parameters of the FunctionDecl? And if that is the case, could we iterate through the redecl chain to find an appropriate matching FunctionDecl? That would be b(int*) in this case ... and the original bindArray() should work then.

Good idea. Actually, the CFG already refers to the void () delc, so we should probably change it there instead of doing the same in the CSA.
Let me investigate this route.

A similar situation could happen if we reinterpret cast pointers, etc. so the situation is not limited to conflicting function prototypes.

Please provide tests for those cases.

It was a harsh guess on my part, simply by looking at the missing ElementRegion, I thought that I could reconstruct the example by some reinterpret-cast tricks - but I could not pull it off.
Disregard that part.

NoQ added a comment.Oct 19 2022, 3:47 PM

In this example, we try to store 42 to the Elem{buffer, 0}.

In this case the natural question is, why does it go through bindArray()? We're not binding an array. Can we try to preserve the contract that bindArray() only deals with arrays?

@martong's comment makes sense to me, it sounds like this could be a case of CallEvent::getRuntimeDefinition() not picking up the right definition, at least not consistently.

steakhal updated this revision to Diff 469234.Oct 20 2022, 8:01 AM
steakhal retitled this revision from [analyzer] Fix assertion failure in RegionStore within bindArray() to [analyzer] Fix assertion failure with conflicting prototype calls.
steakhal edited the summary of this revision. (Show Details)
  • Updated the summary.
  • Implementing a completely new approach focusing on the runtime definition & argument-parameter binding

In this example, we try to store 42 to the Elem{buffer, 0}.

In this case the natural question is, why does it go through bindArray()? We're not binding an array. Can we try to preserve the contract that bindArray() only deals with arrays?

@martong's comment makes sense to me, it sounds like this could be a case of CallEvent::getRuntimeDefinition() not picking up the right definition, at least not consistently.

Thank you @martong and @NoQ for the helpful comments. They really helped a lot to look at this problem from a new perspective!
I hope, this new approach is more aligned with your expectations.

steakhal edited the summary of this revision. (Show Details)Oct 20 2022, 8:09 AM
clang/lib/StaticAnalyzer/Core/CallEvent.cpp
490

Previously we didng make bindings if ArgVal was unknown, and we may want to preserve this invariant.

clang/test/Analysis/region-store.c
66

I would like to see an example where the called function is implicitly defined.

clang/test/Analysis/region-store.c
66

After rethinking it, I have not idea how to construct that example.

steakhal marked an inline comment as done.Oct 20 2022, 9:20 AM
steakhal added inline comments.
clang/test/Analysis/region-store.c
66

I could not construct such an example.
It seems like clang errors out for cases when an implicit declaration of a call mismatches with the definition of that function.
https://godbolt.org/z/rM9ajeTf7

NoQ added a comment.Oct 20 2022, 10:21 PM

Ok so you're saying that it's not just a wrong Decl but there's like an entire cast-expression missing in the AST? This fix is probably good enough for us but it begs a question, what does CodeGen do in such cases? Does it also need to emit a cast instruction into LLVM IR that doesn't correspond to anything in the AST? Or is this entirely about our weird pointer cast handling, that needs to act even when the numeric value of the pointer doesn't change?

NoQ added inline comments.Oct 20 2022, 10:24 PM
clang/test/Analysis/region-store.c
66

Yeah, if you scroll really far to the right, you'll see that the first error is actually a warning auto-promoted to an error. So you can pass -Wno-implicit-function-declaration and it'll disappear. Not sure what to do with the other error though, it really does notice that the implicit definition conflicts with the later explicit definition. So, dunno.

steakhal marked an inline comment as done.Oct 21 2022, 12:11 AM

Ok so you're saying that it's not just a wrong Decl but there's like an entire cast-expression missing in the AST? This fix is probably good enough for us but it begs a question, what does CodeGen do in such cases? Does it also need to emit a cast instruction into LLVM IR that doesn't correspond to anything in the AST? Or is this entirely about our weird pointer cast handling, that needs to act even when the numeric value of the pointer doesn't change?

Good question. The IR is identical for the two cases - except for metadata markers e.g. for deb info, etc.
So, it might be simply that the "C" language just doesn't care if there is a bitcast or not in the AST, because the hardware register is just an untyped register; hence this ImplicitCastExpr (bitcast) is getting lowered as a noop at the IR level.

Unfortunately, it's beyond my expertise. Do you think we should invite someone, like Shafik or Aaron?

clang/lib/StaticAnalyzer/Core/CallEvent.cpp
490

IDK what are the implications of not having a binding or having a binding to unknown.
I'll change this anyway. Thanks for noticing.

clang/test/Analysis/region-store.c
66

Yup, I should have been more clear on this. See the test, I'm also passing the -Wno-implicit-function-declaration :)
Maybe Shafik or Aaron knows some weird stuff about how to make it 'compile'. WDYT?

martong accepted this revision.Oct 21 2022, 9:12 AM

Thanks for the update. Nice Work!

clang/test/Analysis/region-store.c
66

Interestingly, GCC trunk compiles it without errors, the warnings are there though.

This revision is now accepted and ready to land.Oct 21 2022, 9:12 AM
steakhal marked 2 inline comments as done.Oct 23 2022, 11:47 PM

Thanks for the update. Nice Work!

Thanks! I'll land this tomorrow.

clang/test/Analysis/region-store.c
66

Hm, interesting.

steakhal marked 2 inline comments as done.Oct 24 2022, 1:30 AM
steakhal added inline comments.
clang/lib/StaticAnalyzer/Core/CallEvent.cpp
490

Hm, I tried to move the unknown check after the cast. It would result in a difference in the last example:

void c(); // expected-warning {{a function declaration without a prototype is deprecated in all versions of C and is treated as a zero-parameter prototype in C2x, conflicting with a subsequent definition}}
void missingPrototypeCallsiteMismatchingArgsAndParams() {
  // expected-warning@+1 {{passing arguments to 'c' without a prototype is deprecated in all versions of C and is not supported in C2x}}
  c(&buffer, &buffer);
}
void c(int *c) { // expected-note {{conflicting prototype is here}}
  clang_analyzer_dump(c); // ???
  *c = 42; // no-crash
}

The clang_analyzer_dump(c) would result in &SymRegion{reg_$0<int * c>} instead of Unknown.
Which is not exactly what I want. I want to bind Unknown in case the cast would result in Unknown or some weird parameter passing is detected, what we don't want to support/model, such as mismatching number of arguments & parameters, etc.