From 06a5648336155748a076ad65903bddc8dc442f89 Mon Sep 17 00:00:00 2001 From: Tom Hvitved Date: Fri, 19 Dec 2025 12:11:23 +0100 Subject: [PATCH 1/4] 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() From 5bc457f6dadd79c66f38483f033eac7c85d065b6 Mon Sep 17 00:00:00 2001 From: Tom Hvitved Date: Mon, 5 Jan 2026 10:49:19 +0100 Subject: [PATCH 2/4] Rust: Move logic from `AccessAfterLifetimeExtensions.qll` to `AccessAfterLifetime.ql` --- .../AccessAfterLifetimeExtensions.qll | 84 ----------------- .../security/CWE-825/AccessAfterLifetime.ql | 90 +++++++++++++++++-- 2 files changed, 82 insertions(+), 92 deletions(-) diff --git a/rust/ql/lib/codeql/rust/security/AccessAfterLifetimeExtensions.qll b/rust/ql/lib/codeql/rust/security/AccessAfterLifetimeExtensions.qll index c99d1e0d0c29..c404f13b5314 100644 --- a/rust/ql/lib/codeql/rust/security/AccessAfterLifetimeExtensions.qll +++ b/rust/ql/lib/codeql/rust/security/AccessAfterLifetimeExtensions.qll @@ -47,90 +47,6 @@ module AccessAfterLifetime { valueScope(source.getTarget(), target, scope) } - /** - * Holds if the pair `(source, sink)` represents a flow from a pointer or reference - * to a dereference. - */ - 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)) - } - } - /** * Holds if `var` has scope `scope`. */ diff --git a/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql b/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql index 9e698d40a1c9..fb278185b198 100644 --- a/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql +++ b/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql @@ -15,7 +15,7 @@ import rust import codeql.rust.dataflow.DataFlow import codeql.rust.dataflow.TaintTracking -import codeql.rust.security.AccessAfterLifetimeExtensions +import codeql.rust.security.AccessAfterLifetimeExtensions::AccessAfterLifetime import AccessAfterLifetimeFlow::PathGraph /** @@ -24,14 +24,14 @@ import AccessAfterLifetimeFlow::PathGraph */ module AccessAfterLifetimeConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node node) { - node instanceof AccessAfterLifetime::Source and + node instanceof Source and // exclude cases with sources in macros, since these results are difficult to interpret not node.asExpr().isFromMacroExpansion() and - AccessAfterLifetime::sourceValueScope(node, _, _) + sourceValueScope(node, _, _) } predicate isSink(DataFlow::Node node) { - node instanceof AccessAfterLifetime::Sink and + node instanceof Sink and // Exclude cases with sinks in macros, since these results are difficult to interpret not node.asExpr().isFromMacroExpansion() and // TODO: Remove this condition if it can be done without negatively @@ -40,13 +40,13 @@ module AccessAfterLifetimeConfig implements DataFlow::ConfigSig { exists(node.asExpr()) } - predicate isBarrier(DataFlow::Node barrier) { barrier instanceof AccessAfterLifetime::Barrier } + predicate isBarrier(DataFlow::Node barrier) { barrier instanceof Barrier } predicate observeDiffInformedIncrementalMode() { any() } Location getASelectedSourceLocation(DataFlow::Node source) { exists(Variable target | - AccessAfterLifetime::sourceValueScope(source, target, _) and + sourceValueScope(source, target, _) and result = [target.getLocation(), source.getLocation()] ) } @@ -54,6 +54,81 @@ module AccessAfterLifetimeConfig implements DataFlow::ConfigSig { module AccessAfterLifetimeFlow = TaintTracking::Global; +private newtype TTcNode = + TSource(Source s, Variable target) { + AccessAfterLifetimeFlow::flow(s, _) and sourceValueScope(s, target, _) + } or + TBlockExpr(BlockExpr be) or + TSink(Sink s) { AccessAfterLifetimeFlow::flow(_, 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 + AccessAfterLifetimeFlow::flow(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) { + AccessAfterLifetimeFlow::flow(source, sink) and + sourceValueScope(source, target, _) and + not mayEncloseOnStack(TSource(source, target), TSink(sink)) +} + from AccessAfterLifetimeFlow::PathNode sourceNode, AccessAfterLifetimeFlow::PathNode sinkNode, Variable target @@ -61,7 +136,6 @@ 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::dereferenceAfterLifetime(sourceNode - .getNode(), sinkNode.getNode(), target) + dereferenceAfterLifetime(sourceNode.getNode(), sinkNode.getNode(), target) select sinkNode.getNode(), sourceNode, sinkNode, "Access of a pointer to $@ after its lifetime has ended.", target, target.toString() From 2543754dd4de00cbb7aa9206e99a2e044da50cdf Mon Sep 17 00:00:00 2001 From: Tom Hvitved Date: Mon, 5 Jan 2026 13:09:45 +0100 Subject: [PATCH 3/4] Rust: Remove `newtype` construction --- .../security/CWE-825/AccessAfterLifetime.ql | 68 ++++++------------- 1 file changed, 20 insertions(+), 48 deletions(-) diff --git a/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql b/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql index fb278185b198..05656bdf8a0a 100644 --- a/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql +++ b/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql @@ -54,68 +54,33 @@ module AccessAfterLifetimeConfig implements DataFlow::ConfigSig { module AccessAfterLifetimeFlow = TaintTracking::Global; -private newtype TTcNode = - TSource(Source s, Variable target) { - AccessAfterLifetimeFlow::flow(s, _) and sourceValueScope(s, target, _) - } or - TBlockExpr(BlockExpr be) or - TSink(Sink s) { AccessAfterLifetimeFlow::flow(_, 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() - } +predicate sourceBlock(Source s, Variable target, BlockExpr be) { + AccessAfterLifetimeFlow::flow(s, _) and + sourceValueScope(s, target, be.getEnclosingBlock*()) } -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 - AccessAfterLifetimeFlow::flow(source, _) - ) - or +predicate sinkBlock(Sink s, BlockExpr be) { be = s.asExpr().getEnclosingBlock() } + +private predicate tcStep(BlockExpr a, BlockExpr b) { // propagate through function calls exists(Call call | - a.asBlockExpr() = call.getEnclosingBlock() and - call.getARuntimeTarget() = b.asBlockExpr().getEnclosingCallable() + a = call.getEnclosingBlock() and + call.getARuntimeTarget() = b.getEnclosingCallable() ) - or - a.asBlockExpr() = b.asSink().asExpr().getEnclosingBlock() } -private predicate isTcSource(TcNode n) { n instanceof TSource } +private predicate isTcSource(BlockExpr be) { sourceBlock(_, _, be) } -private predicate isTcSink(TcNode n) { n instanceof TSink } +private predicate isTcSink(BlockExpr be) { sinkBlock(_, be) } /** * 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 + * (for example if `f` and `g` both call `b`, 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) = +private predicate mayEncloseOnStack(BlockExpr a, BlockExpr b) = doublyBoundedFastTC(tcStep/2, isTcSource/1, isTcSink/1)(a, b) /** @@ -126,7 +91,14 @@ private predicate mayEncloseOnStack(TcNode a, TcNode b) = predicate dereferenceAfterLifetime(Source source, Sink sink, Variable target) { AccessAfterLifetimeFlow::flow(source, sink) and sourceValueScope(source, target, _) and - not mayEncloseOnStack(TSource(source, target), TSink(sink)) + not exists(BlockExpr beSource, BlockExpr beSink | + sourceBlock(source, target, beSource) and + sinkBlock(sink, beSink) + | + beSource = beSink + or + mayEncloseOnStack(beSource, beSink) + ) } from From 836b667a62e75755066cfff1202eb09fdf799980 Mon Sep 17 00:00:00 2001 From: Tom Hvitved Date: Mon, 5 Jan 2026 19:47:02 +0100 Subject: [PATCH 4/4] Address review comment --- rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql b/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql index 05656bdf8a0a..2f2991678930 100644 --- a/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql +++ b/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql @@ -59,7 +59,10 @@ predicate sourceBlock(Source s, Variable target, BlockExpr be) { sourceValueScope(s, target, be.getEnclosingBlock*()) } -predicate sinkBlock(Sink s, BlockExpr be) { be = s.asExpr().getEnclosingBlock() } +predicate sinkBlock(Sink s, BlockExpr be) { + AccessAfterLifetimeFlow::flow(_, s) and + be = s.asExpr().getEnclosingBlock() +} private predicate tcStep(BlockExpr a, BlockExpr b) { // propagate through function calls