From 2f04451e31939bd1a339124a72aab9207216b8fa Mon Sep 17 00:00:00 2001 From: Tom Hvitved Date: Fri, 19 Dec 2025 12:11:23 +0100 Subject: [PATCH] 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 ``` --- .../AccessAfterLifetimeExtensions.qll | 108 +++++++++++++----- .../security/CWE-825/AccessAfterLifetime.ql | 3 +- 2 files changed, 82 insertions(+), 29 deletions(-) diff --git a/rust/ql/lib/codeql/rust/security/AccessAfterLifetimeExtensions.qll b/rust/ql/lib/codeql/rust/security/AccessAfterLifetimeExtensions.qll index 64b806331a66..c99d1e0d0c29 100644 --- a/rust/ql/lib/codeql/rust/security/AccessAfterLifetimeExtensions.qll +++ b/rust/ql/lib/codeql/rust/security/AccessAfterLifetimeExtensions.qll @@ -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 { + 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`. */ diff --git a/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql b/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql index edc22a86409b..9e698d40a1c9 100644 --- a/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql +++ b/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql @@ -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::dereferenceAfterLifetime(sourceNode + .getNode(), sinkNode.getNode(), target) select sinkNode.getNode(), sourceNode, sinkNode, "Access of a pointer to $@ after its lifetime has ended.", target, target.toString()