-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Rust: Reduce the number of sinks in DereferenceSink
#20941
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
base: main
Are you sure you want to change the base?
Conversation
b569633 to
40f629e
Compare
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.
Pull request overview
This PR adds barriers and refines sink definitions to reduce false positives in the rust/access-invalid-pointer query. The changes separate concerns between the AccessInvalidPointer and AccessAfterLifetime queries by giving each its own sink definitions with appropriate restrictions.
- Adds type-based barriers (numeric, boolean, fieldless enum) to filter out non-pointer types
- Restricts
DereferenceSinkinAccessInvalidPointerto only match raw pointer dereferences in unsafe contexts - Introduces a
DefaultBarrierto handle impreciseDefault::defaultcalls that can resolve to null pointer implementations
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
rust/ql/src/queries/summary/Stats.qll |
Adds import for AccessAfterLifetimeExtensions to ensure all query sink extensions are loaded |
rust/ql/lib/codeql/rust/security/AccessInvalidPointerExtensions.qll |
Adds multiple barriers (type-based, NonNull::new, Default::default) and restricts DereferenceSink to unsafe contexts with raw pointer type checks |
rust/ql/lib/codeql/rust/security/AccessAfterLifetimeExtensions.qll |
Refactors Sink from type alias to abstract class and defines its own sink implementations separate from AccessInvalidPointer |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Dereferencing a raw pointer is an unsafe operation. Hence relevant | ||
| // dereferences must occur inside code marked as unsafe. | ||
| // See: https://doc.rust-lang.org/reference/types/pointer.html#r-type.pointer.raw.safety | ||
| (p.getEnclosingBlock*().isUnsafe() or p.getEnclosingCallable().(Function).isUnsafe()) and |
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.
Do you think this check is better here, in the sink definition, or in the place where the query uses the sink (as it is for rust/access-after-lifetime-ended)? Should we be consistent between the two?
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 think it makes sense to have for the sink, if it's intended to represent pointer dereferences. We should remove the check in rust/access-after-lifetime-ended. I've done that now.
| call.getStaticTarget().getCanonicalPath() = canonicalName and | ||
| this.asExpr() = call.getArgument(pos) | ||
| | | ||
| canonicalName = "<core::ptr::non_null::NonNull>::new" and pos.asPosition() = 0 |
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.
Should this be a barrier or a sink???
Either way I'd like to see a test case for this.
| } | ||
| } | ||
|
|
||
| private class NumericTypeBarrier extends Barrier instanceof Barriers::NumericTypeBarrier { } |
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 this end up blocking legitimate results where a null value is cast through an integer type and back, perhaps for integration with C-like interfaces or something? Admittedly literal 0 isn't currently a source for this query, so we don't really support this case as it is now.
| // to trait functions is more precise. | ||
| this.asExpr().(Call).getStaticTarget().getCanonicalPath() = | ||
| "<_ as core::default::Default>::default" | ||
| } |
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'd like to see an example (test case) for this as well. I'm not quite clear what it looks like.
| // dereferences must occur inside code marked as unsafe. | ||
| // See: https://doc.rust-lang.org/reference/types/pointer.html#r-type.pointer.raw.safety | ||
| (p.getEnclosingBlock*().isUnsafe() or p.getEnclosingCallable().(Function).isUnsafe()) and | ||
| (not exists(TypeInference::inferType(p)) or TypeInference::inferType(p) instanceof PtrType) |
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'm interested to see if this PtrType restriction effects real world [edge case] results (DCA and/or MRVA).
35cf657 to
0b3a694
Compare
| // TODO: Remove this condition if it can be done without negatively | ||
| // impacting performance. This condition only include nodes with | ||
| // corresponding to an expression. This excludes sinks from models-as-data. | ||
| exists(node.asExpr()) |
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 check is weird, but note that the check before the changes had the same effect of excluding all nodes without an expression. So this is only preserving what was already there.
Since my present goal is to improve performance I don't really have appetite for introducing additional sinks, hence I left this condition in place.
As the todo says I think we should try to remove this in the future and see what it does for performance and results.
rust/access-invalid-pointerDereferenceSink
| // exclude cases with sources in macros, since these results are difficult to interpret | ||
| not node.asExpr().isFromMacroExpansion() | ||
| not node.asExpr().isFromMacroExpansion() and | ||
| AccessAfterLifetime::sourceValueScope(node, _, _) |
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.
Sources needs to satisfy this in the end anyway, so we might check for that here to prune some sources earlier in the process.
|
Thanks for the review @geoffw0. I've now updated the PR to (almost) only include the change to |
This PR reduces the number of sinks in
DereferenceSinkby not including dereference expressions that are not the source of unsafety.The
DereferenceSinkis used inrust/access-invalid-pointerandrust/access-after-lifetime-ended.The number of sinks is reduced in two ways:
unsafeblock. Onrustthis restriction reduces the number of sinks from 29,857 to 1,618. This filter was already applied inrust/access-after-lifetime-endedso it only makes a difference for therust/access-invalid-pointerquery.rustthis further reduces the number of sinks from 1,618 to 1,301.I've looked at some of the sink that are removed on
rustdue to the type check, and they look like good exclusions to me. Here's a typical example:This deref is excluded because
get_uncheckedreturns a&. Excluding this seem fine because 1/ the query can't handle this anyway and 2/ if the aboveunsafeblock goes wrong the root cause for blame is withinget_uncheckedand not*.MRVA
MRVA top 100 for "Access of invalid pointer"
Overall there's only a small change in the number of results, which seems good. The removed results that I've looked at all seemed like FPs.