-
Notifications
You must be signed in to change notification settings - Fork 1.7k
C++: New Query: missing return-value check for scanf-like functions #10163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
These keep getting added, by the Makefile extension I believe.
…le was initialized, as in real world cases we haven't seen it done safely.
There are still some remaining FPs (haven't fully tested them) that should be ironed out in a follow-up to increase the precision, e.g.: * if scanf(&i) != 1 return if maybe() && scanf(&i) != 1 return use(i) // should be OK on both counts * The minimum guard constant for the *_s variants may not be right. * int i[2] scanf(i, i+1) // second i is flagged as a use of the first * Maybe loosen the "unguarded or badly guarded use() = bad" policy to "unguarded but already-initialized = good" and "badly guarded = bad", since a lot of FPs in MRVA fall into the "unguarded but already- initialized" bucket.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks really good! Here are my first round of comments.
exists(Instruction dst | | ||
this.bigStep(instr) = dst and | ||
dst.getAst() = result and | ||
valueNumber(dst) = valNum |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we move this conjunct into smallStep
instead? i.e., require that every small step preserves the value number? I think that will reduce the size of the smallStep
relation (and more importantly, the bigStep
relation!) quite a bit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be tricky, because while we do want the final Accesses to have the same ValueNumber, we don't want all intermediate instructions to have to have the same ValueNumber too, because then some crucial paths will be severed. Currently, bigStep
is the same the .getASuccessor+()
closure starting from instr
, minus all paths that go through a barrier as defined by isBarrier()
. I would hesitantly wager that this is as compact as it gets.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, on second thought, maybe bigStep(i)
can be optimized by having i = instr
, always :) Because none of the i != instr
tuples are ever needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be tricky, because while we do want the final Accesses to have the same ValueNumber, we don't want all intermediate instructions to have to have the same ValueNumber too, because then some crucial paths will be severed. Currently, bigStep is the same the .getASuccessor+() closure starting from instr, minus all paths that go through a barrier as defined by isBarrier(). I would hesitantly wager that this is as compact as it gets.
Ah, right. Yeah, I do see what you mean here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, on second thought, maybe bigStep(i) can be optimized by having i = instr, always :) Because none of the i != instr tuples are ever needed.
Yes, I think that's a good idea.
Speaking of optimizations: Currently, you're big step is defined something like:
predicate bigStep(Instruction from, Instruction to) { source(from) and smallStep*(from, to) and sink(to) }
The smallStep*(from, to)
computation produces a lot of intermediate tuples that you don't really care about. We have a lot of experience in how to do this efficiently in QL, and I'd be happy to chat tomorrow about how to compute this properly. Let me just dump it here for you to read at your own pace:
The trick is to define a unary predicate to avoid producing a lot of unnecessary tuples:
// Holds if `i` can be reached starting at a source.
predicate reachedFromSource(Instruction i) {
source(i)
or
exists(Instruction mid | reachedFromSource(mid) | step(mid, i))
}
// Holds if `i` can be reached starting at a source, and can eventually reach a sink.
predicate reachesSink(Instruction i) {
reachedFromSource(i) and
(
sink(i)
or
exists(Instruction mid | reachesSink(mid) | step(i, mid))
)
}
Then, you can use this predicate to define a smaller step relation:
predicate prunedSmallStep(Instruction from, Instruction to) {
reachesSink(from) and reachesSink(to) and smallStep(from, to)
}
and finally, you can rewrite your bigStep
as:
predicate bigStep(Instruction from, Instruction to) { source(from) and prunedSmallStep*(from, to) and sink(to) }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have saved the best for last, implementing your proposed optimizations ^. Here are the before-and-after MRVA runs... Let's see how much of a difference it makes :)
Before: https://github.com/github/semmle-code/actions/runs/2927651707
After: https://github.com/github/semmle-code/actions/runs/2927920561
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... Nope, I'm calling it. It's not worth it. I've had to inline some predicates to avoid the runaway you see in the "After", and still it's not a significant difference. This is my WIP, so it could be improved in the future: 7cc0d71
P.S. MRVA top 100 run on that WIP code: https://github.com/github/semmle-code/actions/runs/2929372003
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fact that the new code is not timing out on opencv suggest that's it's a worthwhile improvement to make. I've left a comment on your branch. We can also discuss them tomorrow if you want :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, adding the pragma[inline]
to the cartesian-product-prone predicate removed the timeout on opencv 😛 Maybe a bindingset[this]
annotation would have done the trick too..? And you're right, I missed the first line from reachesSink
, which might make all the difference...
So along with removing the this
argument from these predicates (which might be tricky in some cases, since isBarrier
does depend on this
), I'll leave the exploration of this type of optimization for a follow-up issue, unless you think the performance improvements would be so drastic that they would be worth having in the initial release of this query (that is, if you think we can analyze opencv in significantly less than an hour on GitHub Actions [Locally, it's just a few minutes, fyi])
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, adding the
pragma[inline]
to the cartesian-product-prone predicate removed the timeout on opencv 😛 Maybe abindingset[this]
annotation would have done the trick too..? And you're right, I missed the first line fromreachesSink
, which might make all the difference...
Yep, I think bindingset
would do the same thing as a bindingset
annotation implies inlining (as far as I know, at least). I also think the missing conjunct in reachesSink
might make all the difference 😄, indeed.
I'm still not completely sure how to read the MRVA run output. I assume the 50 minutes of runtime on opencv
is because we also have to compute the IR and all the other underlying things that the query makes use of. If it only takes a few minutes once the cache
d libraries are computed that sounds fine.
cpp/ql/test/query-tests/Critical/MissingCheckScanf/.clang-format
Outdated
Show resolved
Hide resolved
Extract some of the logic from the `cpp/missing-check-scanf` query into the more generally useful `getOutputArgument(int index)`, `getAnOutputArgument()`, and `getNumberOfOutputArguments()` predicates.
Passes tests.
because they are potentially too global, belong in a separate PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Query and .qhelp
look great. It looks like this query became a fair bit more complicated than we had expected!
result = | ||
varargIndex + 1 - | ||
count(ScanfFormatLiteral f, int n | | ||
// Special case: %n writes to an argument without reading any input. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great attention to detail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! I think there are even more special cases, that I'm parking for a follow-up issue for now (namely, the *_s variants of scanf introduced in C11).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@d10c 👋🏻 - I've made a few comments and suggestions, but in generally this looks great to me ✨
It would be awesome if we could simplify the query description. Feel free to ignore any feedback you don't agree with 😃
Thanks to @mchammer01 and @geoffw0 for the suggestions latest.
I'm quite happy with this PR now. I don't see a DCA run anywhere, though. I think we should run one to be safe. |
Another thing, currently the query metadata reads:
I wasn't clear on how to set these numbers, so I just winged the security-severity to "some number in the medium range", and the severity as "recommendation" because the MissingNullCheck query was also a recommendation. But maybe the security properties of this query are sufficiently different that these numbers merit more scrutiny. Maybe @geoffw0 can chime in? |
Severity is about how bad the problem is when you have a true positive.
I think I could justify
Discussed elsewhere.
This is about what proportion of results can be expected to be true positives. The exact meanings are somewhat controversial, I tend to think them as:
Its a bit tricky, because it isn't always easy to determine if a result in someone else's code is a false positive, and in high quality projects FP rates will be higher (as there are fewer TPs). We usually target See https://github.com/github/codeql/blob/main/docs/query-metadata-style-guide.md for more info about this stuff. |
Though the codeql/cwe-scores update-queries.py script did not make any changes on its own, I looked up the score of the CWEs that @geoffw0 suggested using the explain.py script. As discussed elsewhere, this should be more of a warning than a recommendation.
As that seems to be appropriate for this query.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
Introduces
cpp/missing-check-scanf
, a query with a similar idea as this external contribution: findscanf
output arguments that are read without checking that they were written in the first place. The main differences are an IR-based flow and a more conservative treatment of unguarded reads of already-initializedscanf
output variables.This PR squashes and supercedes draft PR #10033.