Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
108 changes: 80 additions & 28 deletions rust/ql/lib/codeql/rust/security/AccessAfterLifetimeExtensions.qll
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,87 @@ module AccessAfterLifetime {
}

/**
* 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`.
* Holds if the pair `(source, sink)` represents a flow from a pointer or reference
* to a dereference.
*/
bindingset[source, sink]
predicate dereferenceAfterLifetime(Source source, Sink sink, Variable target) {
exists(BlockExpr valueScope, BlockExpr accessScope |
sourceValueScope(source, target, valueScope) and
accessScope = sink.asExpr().getEnclosingBlock() and
not mayEncloseOnStack(valueScope, accessScope)
)
signature predicate dereferenceAfterLifetimeCandSig(DataFlow::Node source, DataFlow::Node sink);

/** Provides logic for identifying dereferences after lifetime. */
module DereferenceAfterLifetime<dereferenceAfterLifetimeCandSig/2 dereferenceAfterLifetimeCand> {
private newtype TTcNode =
TSource(Source s, Variable target) {
dereferenceAfterLifetimeCand(s, _) and sourceValueScope(s, target, _)
} or
TBlockExpr(BlockExpr be) or
TSink(Sink s) { dereferenceAfterLifetimeCand(_, s) }

private class TcNode extends TTcNode {
Source asSource(Variable target) { this = TSource(result, target) }

BlockExpr asBlockExpr() { this = TBlockExpr(result) }

Sink asSink() { this = TSink(result) }

string toString() {
result = this.asSource(_).toString()
or
result = this.asBlockExpr().toString()
or
result = this.asSink().toString()
}

Location getLocation() {
result = this.asSource(_).getLocation()
or
result = this.asBlockExpr().getLocation()
or
result = this.asSink().getLocation()
}
}

pragma[nomagic]
private predicate tcStep(TcNode a, TcNode b) {
// `b` is a child of `a`
exists(Source source, Variable target, BlockExpr be |
source = a.asSource(target) and
be = b.asBlockExpr().getEnclosingBlock*() and
sourceValueScope(source, target, be) and
dereferenceAfterLifetimeCand(source, _)
)
or
// propagate through function calls
exists(Call call |
a.asBlockExpr() = call.getEnclosingBlock() and
call.getARuntimeTarget() = b.asBlockExpr().getEnclosingCallable()
)
or
a.asBlockExpr() = b.asSink().asExpr().getEnclosingBlock()
}

private predicate isTcSource(TcNode n) { n instanceof TSource }

private predicate isTcSink(TcNode n) { n instanceof TSink }

/**
* Holds if block `a` contains block `b`, in the sense that a stack allocated variable in
* `a` may still 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
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

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

The comment contains a duplicate word 'then'. The phrase should read "for example if f and g both call b, then depending on the caller..." with only one 'then'.

Suggested change
* (for example if `f` and `g` both call `b`, then then depending on the
* (for example if `f` and `g` both call `b`, then depending on the

Copilot uses AI. Check for mistakes.
* caller a variable in `f` or `g` may or may-not be on the stack during `b`).
*/
private predicate mayEncloseOnStack(TcNode a, TcNode b) =
doublyBoundedFastTC(tcStep/2, isTcSource/1, isTcSink/1)(a, b)

/**
* 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`.
*/
predicate dereferenceAfterLifetime(Source source, Sink sink, Variable target) {
dereferenceAfterLifetimeCand(source, sink) and
sourceValueScope(source, target, _) and
not mayEncloseOnStack(TSource(source, target), TSink(sink))
}
}

/**
Expand Down Expand Up @@ -88,24 +158,6 @@ module AccessAfterLifetime {
valueScope(value.(FieldExpr).getContainer(), target, scope)
}

/**
* Holds if block `a` contains block `b`, in the sense that a stack allocated variable in
* `a` may still 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 mayEncloseOnStack(BlockExpr a, BlockExpr b) {
// `b` is a child of `a`
a = b.getEnclosingBlock*()
or
// propagate through function calls
exists(Call call |
mayEncloseOnStack(a, call.getEnclosingBlock()) and
call.getARuntimeTarget() = b.getEnclosingCallable()
)
}

/**
* A source that is a `RefExpr`.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ 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)
AccessAfterLifetime::DereferenceAfterLifetime<AccessAfterLifetimeFlow::flow/2>::dereferenceAfterLifetime(sourceNode
.getNode(), sinkNode.getNode(), target)
select sinkNode.getNode(), sourceNode, sinkNode,
"Access of a pointer to $@ after its lifetime has ended.", target, target.toString()