Skip to content

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
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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 May 30, 2025
8e8374b
Rust: Label source annotations in the test properly.
geoffw0 May 30, 2025
43cb98a
Rust: Fix some warnings in the existing test.
geoffw0 May 30, 2025
ae19ecc
Rust: Add test cases involving lifetimes + closures and async blocks.
geoffw0 May 30, 2025
e2fb1d3
Rust: Add test cases involving lifetimes + lifetime annotations.
geoffw0 May 30, 2025
66c1e2c
Rust: Add test cases for implicit dereferences and more pointer/enum …
geoffw0 Jun 2, 2025
96dc34e
Rust: Even more test cases (inspired by real world results).
geoffw0 Jun 5, 2025
526620c
Rust: Add some helper predicates for finding enclosing blocks.
geoffw0 May 30, 2025
bf4ea02
Rust: Implement the query.
geoffw0 May 30, 2025
79f8584
Rust: Fix spurious results involving closures.
geoffw0 Jun 2, 2025
21b4bae
Rust: Have the alert message cite the variable, so it's easier to und…
geoffw0 Jun 2, 2025
fe20fb4
Rust: More robust fix for closures.
geoffw0 Jun 2, 2025
26f8558
Rust: Add qhelp, examples, and examples as tests.
geoffw0 Jun 3, 2025
7bae451
Rust: Exclude results in macro invocations.
geoffw0 Jun 5, 2025
858eec3
Rust: Exclude results where the source is a reference.
geoffw0 Jun 9, 2025
d3d0a53
Rust: Add test showing yet another spurious result.
geoffw0 Jun 9, 2025
b3330b5
Rust: Allow parameter accesses as sources.
geoffw0 Jun 9, 2025
9b0ee8f
Rust: Add security-severity tag and reduce precision to medium for now.
geoffw0 Jun 9, 2025
e7945e1
Rust: Accept the query in suite listings.
geoffw0 Jun 9, 2025
74ce4e8
Update rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql
geoffw0 Jun 9, 2025
a9d5d8b
Rust: Accept the new alert message in tests.
geoffw0 Jun 9, 2025
ecac0db
Rust: Accept consistency check failures.
geoffw0 Jun 11, 2025
b29deed
Rust: Accept changes in an unrelated test reported by CI.
geoffw0 Jun 11, 2025
1682460
Rust: Extend tests based on cases found in DCA.
geoffw0 Jun 11, 2025
087e666
Rust: Exclude sources in macro expansions.
geoffw0 Jun 11, 2025
14b75a9
Apply suggestions from code review
geoffw0 Jun 13, 2025
df221ea
Rust: Remove excess 'cached' annotation.
geoffw0 Jun 17, 2025
5bf799e
Apply suggestions from code review
geoffw0 Jun 18, 2025
79cedc2
Rust: Rename predicate again.
geoffw0 Jun 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ ql/rust/ql/src/queries/security/CWE-327/BrokenCryptoAlgorithm.ql
ql/rust/ql/src/queries/security/CWE-328/WeakSensitiveDataHashing.ql
ql/rust/ql/src/queries/security/CWE-696/BadCtorInitialization.ql
ql/rust/ql/src/queries/security/CWE-770/UncontrolledAllocationSize.ql
ql/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql
ql/rust/ql/src/queries/security/CWE-825/AccessInvalidPointer.ql
ql/rust/ql/src/queries/summary/LinesOfCode.ql
ql/rust/ql/src/queries/summary/LinesOfUserCode.ql
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ ql/rust/ql/src/queries/security/CWE-312/CleartextLogging.ql
ql/rust/ql/src/queries/security/CWE-327/BrokenCryptoAlgorithm.ql
ql/rust/ql/src/queries/security/CWE-328/WeakSensitiveDataHashing.ql
ql/rust/ql/src/queries/security/CWE-770/UncontrolledAllocationSize.ql
ql/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql
ql/rust/ql/src/queries/security/CWE-825/AccessInvalidPointer.ql
ql/rust/ql/src/queries/summary/LinesOfCode.ql
ql/rust/ql/src/queries/summary/LinesOfUserCode.ql
Expand Down
11 changes: 11 additions & 0 deletions rust/ql/lib/codeql/rust/elements/internal/AstNodeImpl.qll
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ module Impl {
)
}

/** Gets the block that encloses this node, if any. */
cached
BlockExpr getEnclosingBlock() {
exists(AstNode p | p = this.getParentNode() |
result = p
or
not p instanceof BlockExpr and
result = p.getEnclosingBlock()
)
}

/** Holds if this node is inside a macro expansion. */
predicate isInMacroExpansion() { MacroCallImpl::isInMacroExpansion(_, this) }

Expand Down
4 changes: 4 additions & 0 deletions rust/ql/lib/codeql/rust/elements/internal/VariableImpl.qll
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ module Impl {
*/
Name getName() { variableDecl(definingNode, result, text) }

/** Gets the block that encloses this variable, if any. */
cached
BlockExpr getEnclosingBlock() { result = definingNode.getEnclosingBlock() }

/** Gets the `self` parameter that declares this variable, if any. */
SelfParam getSelfParam() { result.getName() = this.getName() }

Expand Down
119 changes: 119 additions & 0 deletions rust/ql/lib/codeql/rust/security/AccessAfterLifetimeExtensions.qll
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) {
// `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 rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.qhelp
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 &gt;&gt; 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 rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql
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 rust/ql/src/queries/security/CWE-825/AccessAfterLifetimeBad.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

fn get_pointer() -> *const i64 {
let val = 123;

return &val;
} // 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit confusing that there is two vals here and the lifetime of the val 5 lines up has not ended. Just renaming one one them would help I think.

}

// ...
}
17 changes: 17 additions & 0 deletions rust/ql/src/queries/security/CWE-825/AccessAfterLifetimeGood.rs
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`.
}

fn example() {
let ptr = get_box();
let val;

// ...

val = *ptr; // GOOD

// ...
}
4 changes: 2 additions & 2 deletions rust/ql/test/extractor-tests/crate_graph/crates.expected
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -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])
Expand Down
Loading