-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Rust: New query rust/access-after-lifetime-ended #19702
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
Open
geoffw0
wants to merge
29
commits into
github:main
Choose a base branch
from
geoffw0:lifetime
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 26 commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
da4fbfb
Rust: Placeholder new query.
geoffw0 8e8374b
Rust: Label source annotations in the test properly.
geoffw0 43cb98a
Rust: Fix some warnings in the existing test.
geoffw0 ae19ecc
Rust: Add test cases involving lifetimes + closures and async blocks.
geoffw0 e2fb1d3
Rust: Add test cases involving lifetimes + lifetime annotations.
geoffw0 66c1e2c
Rust: Add test cases for implicit dereferences and more pointer/enum …
geoffw0 96dc34e
Rust: Even more test cases (inspired by real world results).
geoffw0 526620c
Rust: Add some helper predicates for finding enclosing blocks.
geoffw0 bf4ea02
Rust: Implement the query.
geoffw0 79f8584
Rust: Fix spurious results involving closures.
geoffw0 21b4bae
Rust: Have the alert message cite the variable, so it's easier to und…
geoffw0 fe20fb4
Rust: More robust fix for closures.
geoffw0 26f8558
Rust: Add qhelp, examples, and examples as tests.
geoffw0 7bae451
Rust: Exclude results in macro invocations.
geoffw0 858eec3
Rust: Exclude results where the source is a reference.
geoffw0 d3d0a53
Rust: Add test showing yet another spurious result.
geoffw0 b3330b5
Rust: Allow parameter accesses as sources.
geoffw0 9b0ee8f
Rust: Add security-severity tag and reduce precision to medium for now.
geoffw0 e7945e1
Rust: Accept the query in suite listings.
geoffw0 74ce4e8
Update rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql
geoffw0 a9d5d8b
Rust: Accept the new alert message in tests.
geoffw0 ecac0db
Rust: Accept consistency check failures.
geoffw0 b29deed
Rust: Accept changes in an unrelated test reported by CI.
geoffw0 1682460
Rust: Extend tests based on cases found in DCA.
geoffw0 087e666
Rust: Exclude sources in macro expansions.
geoffw0 14b75a9
Apply suggestions from code review
geoffw0 df221ea
Rust: Remove excess 'cached' annotation.
geoffw0 5bf799e
Apply suggestions from code review
geoffw0 79cedc2
Rust: Rename predicate again.
geoffw0 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
rust/ql/lib/codeql/rust/security/AccessAfterLifetimeExtensions.qll
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
/** | ||
* Provides classes and predicates for reasoning about accesses to a pointer | ||
* after its lifetime has ended. | ||
*/ | ||
|
||
import rust | ||
private import codeql.rust.dataflow.DataFlow | ||
private import codeql.rust.security.AccessInvalidPointerExtensions | ||
private import codeql.rust.internal.Type | ||
private import codeql.rust.internal.TypeInference as TypeInference | ||
|
||
/** | ||
* Provides default sources, sinks and barriers for detecting accesses to a | ||
* pointer after its lifetime has ended, as well as extension points for | ||
* adding your own. Note that a particular `(source, sink)` pair must be | ||
* checked with `dereferenceAfterLifetime` to determine if it is a result. | ||
*/ | ||
module AccessAfterLifetime { | ||
/** | ||
* A data flow source for accesses to a pointer after its lifetime has ended, | ||
* that is, creation of a pointer or reference. | ||
*/ | ||
abstract class Source extends DataFlow::Node { | ||
/** | ||
* Gets the value this pointer or reference points to. | ||
*/ | ||
abstract Expr getTarget(); | ||
} | ||
|
||
/** | ||
* A data flow sink for accesses to a pointer after its lifetime has ended, | ||
* that is, a dereference. We re-use the same sinks as for the accesses to | ||
* invalid pointers query. | ||
*/ | ||
class Sink = AccessInvalidPointer::Sink; | ||
|
||
/** | ||
* A barrier for accesses to a pointer after its lifetime has ended. | ||
*/ | ||
abstract class Barrier extends DataFlow::Node { } | ||
|
||
/** | ||
* Holds if the pair `(source, sink)`, that represents a flow from a | ||
* pointer or reference to a dereference, has its dereference outside the | ||
* lifetime of the target variable `target`. | ||
*/ | ||
bindingset[source, sink] | ||
predicate dereferenceAfterLifetime(Source source, Sink sink, Variable target) { | ||
exists(BlockExpr valueScope, BlockExpr accessScope | | ||
valueScope(source.getTarget(), target, valueScope) and | ||
accessScope = sink.asExpr().getExpr().getEnclosingBlock() and | ||
not maybeOnStack(valueScope, accessScope) | ||
) | ||
} | ||
|
||
/** | ||
* Holds if `var` has scope `scope`. | ||
*/ | ||
private predicate variableScope(Variable var, BlockExpr scope) { | ||
// local variable | ||
scope = var.getEnclosingBlock() | ||
or | ||
// parameter | ||
exists(Callable c | | ||
var.getParameter().getEnclosingCallable() = c and | ||
scope.getParentNode() = c | ||
) | ||
} | ||
|
||
/** | ||
* Holds if `value` accesses a variable `target` with scope `scope`. | ||
*/ | ||
private predicate valueScope(Expr value, Variable target, BlockExpr scope) { | ||
// variable access (to a non-reference) | ||
target = value.(VariableAccess).getVariable() and | ||
variableScope(target, scope) and | ||
not TypeInference::inferType(value) instanceof RefType | ||
or | ||
// field access | ||
valueScope(value.(FieldExpr).getContainer(), target, scope) | ||
} | ||
|
||
/** | ||
* Holds if block `a` contains block `b`, in the sense that a variable in | ||
* `a` may be on the stack during execution of `b`. This is interprocedural, | ||
* but is an overapproximation that doesn't accurately track call contexts | ||
* (for example if `f` and `g` both call `b`, then then depending on the | ||
* caller a variable in `f` or `g` may or may-not be on the stack during `b`). | ||
*/ | ||
private predicate maybeOnStack(BlockExpr a, BlockExpr b) { | ||
geoffw0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// `b` is a child of `a` | ||
a = b.getEnclosingBlock*() | ||
or | ||
// propagate through function calls | ||
exists(CallExprBase ce | | ||
maybeOnStack(a, ce.getEnclosingBlock()) and | ||
ce.getStaticTarget() = b.getEnclosingCallable() | ||
) | ||
} | ||
|
||
/** | ||
* A source that is a `RefExpr`. | ||
*/ | ||
private class RefExprSource extends Source { | ||
Expr targetValue; | ||
|
||
RefExprSource() { this.asExpr().getExpr().(RefExpr).getExpr() = targetValue } | ||
|
||
override Expr getTarget() { result = targetValue } | ||
} | ||
|
||
/** | ||
* A barrier for nodes inside closures, as we don't model lifetimes of | ||
* variables through closures properly. | ||
*/ | ||
private class ClosureBarrier extends Barrier { | ||
ClosureBarrier() { this.asExpr().getExpr().getEnclosingCallable() instanceof ClosureExpr } | ||
} | ||
} |
50 changes: 50 additions & 0 deletions
50
rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.qhelp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
<!DOCTYPE qhelp PUBLIC | ||
"-//Semmle//qhelp//EN" | ||
"qhelp.dtd"> | ||
<qhelp> | ||
<overview> | ||
|
||
<p> | ||
Dereferencing a pointer after the lifetime of its target has ended causes undefined behavior. Memory | ||
may be corrupted, causing the program to crash or behave incorrectly, in some cases exposing the program | ||
to potential attacks. | ||
</p> | ||
|
||
</overview> | ||
<recommendation> | ||
|
||
<p> | ||
When dereferencing a pointer in <code>unsafe</code> code, take care that the pointer is still valid | ||
at the time it is dereferenced. Code may need to be rearranged or changed to extend lifetimes. If | ||
possible, rewrite the code using safe Rust types to avoid this kind of problem altogether. | ||
</p> | ||
|
||
</recommendation> | ||
<example> | ||
|
||
<p> | ||
In the following example, <code>val</code> is local to <code>get_pointer</code> so its lifetime | ||
ends when that function returns. However, a pointer to <code>val</code> is returned and dereferenced | ||
after that lifetime has ended, causing undefined behavior: | ||
</p> | ||
|
||
<sample src="AccessAfterLifetimeBad.rs" /> | ||
|
||
<p> | ||
One way to fix this is to change the return type of the function from a pointer to a <code>Box</code>, | ||
which ensures that the value it points to remains on the heap for the lifetime of the <code>Box</code> | ||
itself. Note that there is no longer a need for an <code>unsafe</code> block as the code no longer | ||
handles pointers directly: | ||
</p> | ||
|
||
<sample src="AccessAfterLifetimeGood.rs" /> | ||
|
||
</example> | ||
<references> | ||
|
||
<li>Rust Documentation: <a href="https://doc.rust-lang.org/reference/behavior-considered-undefined.html#dangling-pointers">Behavior considered undefined >> Dangling pointers</a>.</li> | ||
<li>Rust Documentation: <a href="https://doc.rust-lang.org/std/ptr/index.html#safety">Module ptr - Safety</a>.</li> | ||
<li>Massachusetts Institute of Technology: <a href="https://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/book/second-edition/ch19-01-unsafe-rust.html#dereferencing-a-raw-pointer">Unsafe Rust - Dereferencing a Raw Pointer</a>.</li> | ||
|
||
</references> | ||
</qhelp> |
47 changes: 47 additions & 0 deletions
47
rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
/** | ||
* @name Access of a pointer after its lifetime has ended | ||
* @description Dereferencing a pointer after the lifetime of its target has ended | ||
* causes undefined behavior and may result in memory corruption. | ||
* @kind path-problem | ||
* @problem.severity error | ||
* @security-severity 9.8 | ||
* @precision medium | ||
* @id rust/access-after-lifetime-ended | ||
* @tags reliability | ||
* security | ||
* external/cwe/cwe-825 | ||
*/ | ||
|
||
import rust | ||
import codeql.rust.dataflow.DataFlow | ||
import codeql.rust.dataflow.TaintTracking | ||
import codeql.rust.security.AccessAfterLifetimeExtensions | ||
import AccessAfterLifetimeFlow::PathGraph | ||
|
||
/** | ||
* A data flow configuration for detecting accesses to a pointer after its | ||
* lifetime has ended. | ||
*/ | ||
module AccessAfterLifetimeConfig implements DataFlow::ConfigSig { | ||
predicate isSource(DataFlow::Node node) { node instanceof AccessAfterLifetime::Source } | ||
|
||
predicate isSink(DataFlow::Node node) { node instanceof AccessAfterLifetime::Sink } | ||
|
||
predicate isBarrier(DataFlow::Node barrier) { barrier instanceof AccessAfterLifetime::Barrier } | ||
} | ||
|
||
module AccessAfterLifetimeFlow = TaintTracking::Global<AccessAfterLifetimeConfig>; | ||
|
||
from | ||
AccessAfterLifetimeFlow::PathNode sourceNode, AccessAfterLifetimeFlow::PathNode sinkNode, | ||
Variable target | ||
where | ||
// flow from a pointer or reference to the dereference | ||
AccessAfterLifetimeFlow::flowPath(sourceNode, sinkNode) and | ||
// check that the dereference is outside the lifetime of the target | ||
AccessAfterLifetime::dereferenceAfterLifetime(sourceNode.getNode(), sinkNode.getNode(), target) and | ||
// exclude cases with sources / sinks in macros, since these results are difficult to interpret | ||
not sourceNode.getNode().asExpr().getExpr().isFromMacroExpansion() and | ||
not sinkNode.getNode().asExpr().getExpr().isFromMacroExpansion() | ||
select sinkNode.getNode(), sourceNode, sinkNode, | ||
"Access of a pointer to $@ after its lifetime has ended.", target, target.toString() |
19 changes: 19 additions & 0 deletions
19
rust/ql/src/queries/security/CWE-825/AccessAfterLifetimeBad.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
|
||
fn get_pointer() -> *const i64 { | ||
let val = 123; | ||
|
||
return &val; | ||
geoffw0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} // lifetime of `val` ends here, the pointer becomes dangling | ||
|
||
fn example() { | ||
let ptr = get_pointer(); | ||
let val; | ||
|
||
// ... | ||
|
||
unsafe { | ||
val = *ptr; // BAD: dereferences `ptr` after the lifetime of `val` has ended | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a bit confusing that there is two |
||
} | ||
|
||
// ... | ||
} |
17 changes: 17 additions & 0 deletions
17
rust/ql/src/queries/security/CWE-825/AccessAfterLifetimeGood.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
|
||
fn get_box() -> Box<i64> { | ||
let val = 123; | ||
|
||
return Box::new(val); // copies `val` onto the heap, where it remains for the lifetime of the `Box`. | ||
geoffw0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
fn example() { | ||
let ptr = get_box(); | ||
let val; | ||
|
||
// ... | ||
|
||
val = *ptr; // GOOD | ||
|
||
// ... | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,7 +18,7 @@ | |
#-----| core -> Crate([email protected]) | ||
#-----| compiler_builtins -> Crate([email protected]) | ||
|
||
#-----| Crate([email protected].0) | ||
#-----| Crate([email protected].1) | ||
#-----| proc_macro -> Crate([email protected]) | ||
#-----| alloc -> Crate([email protected]) | ||
#-----| core -> Crate([email protected]) | ||
|
@@ -89,7 +89,7 @@ main.rs: | |
#-----| core -> Crate([email protected]) | ||
#-----| std -> Crate([email protected]) | ||
#-----| test -> Crate([email protected]) | ||
#-----| cfg_if -> Crate([email protected].0) | ||
#-----| cfg_if -> Crate([email protected].1) | ||
#-----| digest -> Crate([email protected]) | ||
|
||
#-----| Crate([email protected]) | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.