This is an archive of the discontinued LLVM Phabricator instance.

[analyzer] Add failing test case demonstrating buggy taint propagation
ClosedPublic

Authored by steakhal on Feb 4 2022, 5:03 AM.

Details

Summary

Recently we uncovered a serious bug in the GenericTaintChecker.
It was already flawed before D116025, but that was the patch that turned this silent bug into a crash.

It happens if the GenericTaintChecker has a rule for a function, which also has a definition.

char *fgets(char *s, int n, FILE *fp) {
  nested_call();   // no parameters!
  return (char *)0;
}

// Within some function:
fgets(..., tainted_fd);

When the engine inlines the definition and finds a function call within that, the PostCall event for the call will get triggered sooner than the PostCall for the original function.
This mismatch violates the assumption of the GenericTaintChecker which wants to propagate taint information from the PreCall event to the PostCall event, where it can actually bind taint to the return value of the same call.

Let's get back to the example and go through step-by-step.
The GenericTaintChecker will see the PreCall<fgets(..., tainted_fd)> event, so it would 'remember' that it needs to taint the return value and the buffer, from the PostCall handler, where it has access to the return value symbol.
However, the engine will inline fgets and the nested_call() gets evaluated subsequently, which produces an unimportant PreCall<nested_call()>, then a PostCall<nested_call()> event, which is observed by the GenericTaintChecker, which will unconditionally mark tainted the 'remembered' arg indexes, trying to access a non-existing argument, resulting in a crash.
If it doesn't crash, it will behave completely unintuitively, by marking completely unrelated memory regions tainted, which is even worse.

The resulting assertion is something like this:

Expr.h: const Expr *CallExpr::getArg(unsigned int) const: Assertion `Arg < getNumArgs() && "Arg access out of range!"' failed.

The gist of the backtrace:

CallExpr::getArg(unsigned int) const
SimpleFunctionCall::getArgExpr(unsigned int)
CallEvent::getArgSVal(unsigned int) const
GenericTaintChecker::checkPostCall(const CallEvent &, CheckerContext &) const

Prior to D116025, there was a check for the argument count before it applied taint, however, it still suffered from the same underlying issue/bug regarding propagation.

This path does not intend to fix the bug, rather start a discussion on how to fix this.


Let me elaborate on how I see this problem.

This pre-call, post-call juggling is just a workaround.
The engine should by itself propagate taint where necessary right where it invalidates regions.
For the tracked values, which potentially escape, we need to erase the information we know about them; and this is exactly what is done by invalidation.
However, in the case of taint, we basically want to approximate from the opposite side of the spectrum.
We want to preserve taint in most cases, rather than cleansing them.

Now, we basically sanitize all escaping tainted regions implicitly, since invalidation binds a fresh conjured symbol for the given region, and that has not been associated with taint.

IMO this is a bad default behavior, we should be more aggressive about preserving taint if not further spreading taint to the reachable regions.

We have a couple of options for dealing with it (let's call it tainting policy):

  1. Taint only the parameters which were tainted prior to the call.
  2. Taint the return value of the call, since it likely depends on the tainted input - if any arguments were tainted.
  3. Taint all escaped regions - (maybe transitively using the cluster algorithm) - if any arguments were tainted.
  4. Not taint anything - this is what we do right now :D

The ExprEngine should not deal with taint on its own. It should be done by a checker, such as the GenericTaintChecker.
However, the Pre-PostCall checker callbacks are not designed for this. RegionChanges would be a much better fit for modeling taint propagation.
What we would need in the RegionChanges callback is the State prior invalidation, the State after the invalidation, and a CheckerContext in which the checker can create transitions, where it would place NoteTags for the modeled taint propagations and report errors if a taint sink rule gets violated.
In this callback, we could query from the prior State, if the given value was tainted; then act and taint if necessary according to the checker's tainting policy.

By using RegionChanges for this, we would 'fix' the mentioned propagation bug 'by-design'.
WDYT?

Diff Detail

Event Timeline

steakhal created this revision.Feb 4 2022, 5:03 AM
steakhal requested review of this revision.Feb 4 2022, 5:03 AM

Thanks Balázs for investigating this issue!

The way we reach the crash is not immediate from the summary you gave, however, the test cases extend the summary well.

Focusing on the crash, I think it might be solved by including the function's name (or even better, the CallDescription itself) in the key of TaintArgsOnPostVisit.

The engine should by itself propagate taint where necessary right where it invalidates regions.
For the tracked values, which potentially escape, we need to erase the information we know about them; and this is exactly what is done by invalidation.
However, in the case of taint, we basically want to approximate from the opposite side of the spectrum.
We want to preserve taint in most cases, rather than cleansing them.
...
What we would need in the RegionChanges callback is the State prior invalidation, the State after the invalidation, and a CheckerContext in which the checker can create transitions, where it would place NoteTags for the modeled taint propagations and report errors if a taint sink rule gets violated.
In this callback, we could query from the prior State, if the given value was tainted; then act and taint if necessary according to the checker's tainting policy.

I agree and fully support this.

We have a couple of options for dealing with it (let's call it tainting policy):

  1. Taint only the parameters which were tainted prior to the call.
  2. Taint the return value of the call, since it likely depends on the tainted input - if any arguments were tainted.
  3. Taint all escaped regions - (maybe transitively using the cluster algorithm) - if any arguments were tainted.
  4. Not taint anything - this is what we do right now :D

I think this should be configurable via an -analyzer-config option, but with a good default, which should be (1. and 2. together).

Before jumping into the heavy long-term work, Do you think, changing the key of TaintArgsOnPostVisit could solve the crash in the short-term?

Focusing on the crash, I think it might be solved by including the function's name (or even better, the CallDescription itself) in the key of TaintArgsOnPostVisit.

Maybe even a LocationContext would be needed in the key?

The way we reach the crash is not immediate from the summary you gave, however, the test cases extend the summary well.

Yea, I'll update the summary. Include an example from the tests and a stack trace of the crash.

Before jumping into the heavy long-term work, Do you think, changing the key of TaintArgsOnPostVisit could solve the crash in the short-term?

We could map a location context to the TaintArgsOnPostVisit arg index set, and that would solve the issue. But it would make the situation worse in terms of readability.
So, even though we can hotfix it, we should definitely do something about it.

steakhal edited the summary of this revision. (Show Details)Feb 4 2022, 8:37 AM
NoQ added a comment.Feb 6 2022, 8:44 PM

I agree with @martong, LocationContext is the correct key here. The pair (Call expression, Location context) uniquely identifies the call as that call is being evaluated. This is exactly how expression evaluations are identified in the Environment.

An alternative would be to turn TaintArgsOnPostVisit into a stack (implemented as ImmutableList) but location contexts are already a stack designed specifically for that purpose.

Please consider accepting this revision to land the child patches fixing both of the described bugs.

Szelethus accepted this revision.Feb 10 2022, 4:24 AM

Sorry for the slack, I assumed this was accepted already. Thanks!

This revision is now accepted and ready to land.Feb 10 2022, 4:24 AM
This revision was landed with ongoing or failed builds.Feb 14 2022, 7:57 AM
This revision was automatically updated to reflect the committed changes.
Herald added a project: Restricted Project. · View Herald TranscriptFeb 14 2022, 7:57 AM
Herald added a subscriber: cfe-commits. · View Herald Transcript
steakhal reopened this revision.EditedFeb 14 2022, 9:49 AM
steakhal added a subscriber: simoll.

It seems like the clang-ve-ninja doesn't really want to accept any patches from me :D
I hope it's not personal. Let's be friends bot, please.

Link to the breakage: https://lab.llvm.org/buildbot/#/builders/91/builds/3818

I'm inviting @simoll for resolving this, and the underlying issue to prevent future breakages and reverts.

This revision is now accepted and ready to land.Feb 14 2022, 9:49 AM
jrtc27 added a subscriber: jrtc27.Feb 18 2022, 2:20 PM

It seems like the clang-ve-ninja doesn't really want to accept any patches from me :D
I hope it's not personal. Let's be friends bot, please.

Link to the breakage: https://lab.llvm.org/buildbot/#/builders/91/builds/3818

I'm inviting @simoll for resolving this, and the underlying issue to prevent future breakages and reverts.

As stated in https://discourse.llvm.org/t/the-angry-clang-ve-ninja-build-bot/60330/4, it's because the clang-ve-ninja bot does a build without assertions enabled, so you need REQUIRES: asserts or to write the test in a way that avoids the need for -debug-only.

It seems like the clang-ve-ninja doesn't really want to accept any patches from me :D
I hope it's not personal. Let's be friends bot, please.

Link to the breakage: https://lab.llvm.org/buildbot/#/builders/91/builds/3818

I'm inviting @simoll for resolving this, and the underlying issue to prevent future breakages and reverts.

As stated in https://discourse.llvm.org/t/the-angry-clang-ve-ninja-build-bot/60330/4, it's because the clang-ve-ninja bot does a build without assertions enabled, so you need REQUIRES: asserts or to write the test in a way that avoids the need for -debug-only.

It seems like it worked! Thanks again.

MaskRay added inline comments.
clang/lib/StaticAnalyzer/Checkers/GenericTaintChecker.cpp
782

There was a -Wunused-lambda-capture in -DLLVM_ENABLE_ASSERTIONS=off builds. Fixed by 7fd60ee6e0a87957a718297a4a42d9881fc561e3

steakhal marked an inline comment as done.Feb 24 2022, 3:02 AM
steakhal added inline comments.
clang/lib/StaticAnalyzer/Checkers/GenericTaintChecker.cpp
782

Ah, thanks!