Rust: Speedup AccessAfterLifetime.ql

Before
```
Pipeline standard for AccessAfterLifetimeExtensions::AccessAfterLifetime::mayEncloseOnStack/2#3cdefece#bf@61cb32j5 was evaluated in 30 iterations totaling 44856ms (delta sizes total: 241646328).
         241404616     ~1%    {2} r1 = SCAN `AccessAfterLifetimeExtensions::AccessAfterLifetime::mayEncloseOnStack/2#3cdefece#bf#prev_delta` OUTPUT In.1, In.0
        7379161442  ~1080%    {2}    | JOIN WITH `_AstNode::AstNode.getEnclosingBlock/0#5c38e65a_AstNode::AstNode.getEnclosingCallable/0#5a548913_Bloc__#join_rhs` ON FIRST 1 OUTPUT Lhs.1, Rhs.1
         333897324    ~40%    {2}    | AND NOT `AccessAfterLifetimeExtensions::AccessAfterLifetime::mayEncloseOnStack/2#3cdefece#bf#prev`(FIRST 2)
         297961888    ~24%    {2}    | JOIN WITH `project#AccessAfterLifetimeExtensions::AccessAfterLifetime::sourceValueScope/3#d065ba16#2` ON FIRST 1 OUTPUT Lhs.0, Lhs.1
                              return r1
```
This commit is contained in:
Tom Hvitved
2025-12-19 12:11:23 +01:00
parent ac859d90ef
commit 06a5648336
2 changed files with 82 additions and 29 deletions

View File

@@ -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
* 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))
}
}
/**
@@ -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`.
*/

View File

@@ -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()