From 51f3acf10440280792366439d81f9d0e44076f89 Mon Sep 17 00:00:00 2001 From: Tom Hvitved Date: Thu, 6 Aug 2026 09:34:07 +0200 Subject: [PATCH 1/3] Rust: Add test for missing flow through captured variables --- rust/ql/lib/codeql/rust/controlflow/internal/Scope.qll | 3 ++- .../library-tests/dataflow/lambdas/external_file.rs | 5 +++++ .../library-tests/dataflow/lambdas/inline-flow.ext.yml | 6 ++++++ rust/ql/test/library-tests/dataflow/lambdas/main.rs | 10 ++++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 rust/ql/test/library-tests/dataflow/lambdas/external_file.rs create mode 100644 rust/ql/test/library-tests/dataflow/lambdas/inline-flow.ext.yml diff --git a/rust/ql/lib/codeql/rust/controlflow/internal/Scope.qll b/rust/ql/lib/codeql/rust/controlflow/internal/Scope.qll index 303920ce7d0e..3ee1430f556e 100644 --- a/rust/ql/lib/codeql/rust/controlflow/internal/Scope.qll +++ b/rust/ql/lib/codeql/rust/controlflow/internal/Scope.qll @@ -35,7 +35,8 @@ final class CallableScope extends CfgScopeImpl, Callable { CallableScope() { // A function without a body corresponds to a trait method signature and // should not have a CFG scope. - this.hasBody() + this.hasBody() and + this.fromSource() // exclude stubs in tests defined using `additionalExternalFile` } override predicate scopeFirst(AstNode first) { diff --git a/rust/ql/test/library-tests/dataflow/lambdas/external_file.rs b/rust/ql/test/library-tests/dataflow/lambdas/external_file.rs new file mode 100644 index 000000000000..190733fd482a --- /dev/null +++ b/rust/ql/test/library-tests/dataflow/lambdas/external_file.rs @@ -0,0 +1,5 @@ +pub fn may_invoke_callback1(f: F) {} + +pub fn may_invoke_callback2(f: F) {} + +pub fn may_invoke_callback3(f: impl Fn(i64)) {} diff --git a/rust/ql/test/library-tests/dataflow/lambdas/inline-flow.ext.yml b/rust/ql/test/library-tests/dataflow/lambdas/inline-flow.ext.yml new file mode 100644 index 000000000000..542463856481 --- /dev/null +++ b/rust/ql/test/library-tests/dataflow/lambdas/inline-flow.ext.yml @@ -0,0 +1,6 @@ +extensions: + - addsTo: + pack: codeql/rust-all + extensible: additionalExternalFile + data: + - ["external_file.rs"] diff --git a/rust/ql/test/library-tests/dataflow/lambdas/main.rs b/rust/ql/test/library-tests/dataflow/lambdas/main.rs index 66295f004a22..817409bbc369 100644 --- a/rust/ql/test/library-tests/dataflow/lambdas/main.rs +++ b/rust/ql/test/library-tests/dataflow/lambdas/main.rs @@ -102,6 +102,16 @@ fn test_apply_wrap() { apply_wrap(|x| sink(x), 0); } +mod external_file; +use external_file::*; + +fn test_external_call() { + let a = source(81); + may_invoke_callback1(|x| sink(a)); // $ MISSING: hasValueFlow=81 + may_invoke_callback2(|x| sink(a)); // $ MISSING: hasValueFlow=81 + may_invoke_callback3(|x| sink(a)); // $ MISSING: hasValueFlow=81 +} + fn main() { closure_flow_out(); closure_flow_in(); From e9bd6988c82d0d085f82beb96f5d31aea720b4a4 Mon Sep 17 00:00:00 2001 From: Tom Hvitved Date: Thu, 6 Aug 2026 09:51:47 +0200 Subject: [PATCH 2/3] Rust: Assume callbacks will be invoked in library functions --- .../rust/dataflow/internal/DataFlowImpl.qll | 2 +- .../rust/dataflow/internal/ModelsAsData.qll | 34 +++++++++++++++++++ .../dataflow/lambdas/inline-flow.expected | 16 +++++++++ .../library-tests/dataflow/lambdas/main.rs | 6 ++-- 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/rust/ql/lib/codeql/rust/dataflow/internal/DataFlowImpl.qll b/rust/ql/lib/codeql/rust/dataflow/internal/DataFlowImpl.qll index c18ac5e026b1..f466249ada99 100644 --- a/rust/ql/lib/codeql/rust/dataflow/internal/DataFlowImpl.qll +++ b/rust/ql/lib/codeql/rust/dataflow/internal/DataFlowImpl.qll @@ -440,7 +440,7 @@ module RustDataFlowGen implements InputSig or result = "self" and this.isSelf() or - result = "closure self" and this.isClosureSelf() + result = "closure-self" and this.isClosureSelf() } ParamBase getParameterIn(ParamList ps) { diff --git a/rust/ql/lib/codeql/rust/dataflow/internal/ModelsAsData.qll b/rust/ql/lib/codeql/rust/dataflow/internal/ModelsAsData.qll index 8652b93f4aa9..e49d5cc21d29 100644 --- a/rust/ql/lib/codeql/rust/dataflow/internal/ModelsAsData.qll +++ b/rust/ql/lib/codeql/rust/dataflow/internal/ModelsAsData.qll @@ -58,6 +58,9 @@ private import codeql.rust.dataflow.FlowBarrier private import codeql.rust.dataflow.FlowSummary private import codeql.rust.dataflow.FlowSource private import codeql.rust.dataflow.FlowSink +private import codeql.rust.internal.typeinference.FunctionType +private import codeql.rust.internal.typeinference.TypeMention +private import codeql.rust.frameworks.stdlib.Stdlib /** * Holds if in a call to the function with canonical path `path`, the value referred @@ -207,6 +210,37 @@ private class SummarizedCallableFromModel extends SummarizedCallable::Range { } } +/** + * Holds if library function `f` has a callback at position `n`. In this case we + * add a flow model that achieves the effect of simulating that the callback is + * invoked, which is needed for flow through captured variables to work. + */ +private predicate mayInvokeCallback(Function f, int n) { + exists(TypeMention tm, Trait trait | + tm = f.getParam(n).getTypeRepr() and + trait = getALookupTrait(f, tm.getType()) and + trait.getSupertrait*() instanceof FnOnceTrait and + not f.fromSource() + ) +} + +private class SummarizedCallableWithCallback extends SummarizedCallable::Range { + private int pos; + + SummarizedCallableWithCallback() { mayInvokeCallback(this, pos) } + + override predicate propagatesFlow( + string input, string output, boolean preservesValue, Provenance p, boolean isExact, string model + ) { + input = "Argument[" + pos + "]" and + output = "Argument[" + pos + "].Parameter[closure-self]" and + preservesValue = true and + p = "hq-generated" and + isExact = true and + model = "heuristic-callback" + } +} + private class FlowSourceFromModel extends FlowSource::Range { private string path; diff --git a/rust/ql/test/library-tests/dataflow/lambdas/inline-flow.expected b/rust/ql/test/library-tests/dataflow/lambdas/inline-flow.expected index ca4bbe77479a..049c30c06add 100644 --- a/rust/ql/test/library-tests/dataflow/lambdas/inline-flow.expected +++ b/rust/ql/test/library-tests/dataflow/lambdas/inline-flow.expected @@ -55,6 +55,12 @@ edges | main.rs:100:13:100:22 | source(...) | main.rs:100:9:100:9 | b | provenance | | | main.rs:101:17:101:17 | ... | main.rs:101:25:101:25 | x | provenance | | | main.rs:101:29:101:29 | b | main.rs:93:33:93:38 | ...: i64 | provenance | | +| main.rs:109:13:109:22 | source(...) | main.rs:110:26:110:36 | \|...\| ... : ... => .. [captured a] | provenance | | +| main.rs:109:13:109:22 | source(...) | main.rs:111:26:111:36 | \|...\| ... : ... => .. [captured a] | provenance | | +| main.rs:109:13:109:22 | source(...) | main.rs:112:26:112:36 | \|...\| ... : ... => .. [captured a] | provenance | | +| main.rs:110:26:110:36 | \|...\| ... : ... => .. [captured a] | main.rs:110:35:110:35 | a | provenance | heuristic-callback | +| main.rs:111:26:111:36 | \|...\| ... : ... => .. [captured a] | main.rs:111:35:111:35 | a | provenance | heuristic-callback | +| main.rs:112:26:112:36 | \|...\| ... : ... => .. [captured a] | main.rs:112:35:112:35 | a | provenance | heuristic-callback | nodes | main.rs:10:20:10:52 | if cond {...} else {...} | semmle.label | if cond {...} else {...} | | main.rs:10:30:10:39 | source(...) | semmle.label | source(...) | @@ -121,6 +127,13 @@ nodes | main.rs:101:17:101:17 | ... | semmle.label | ... | | main.rs:101:25:101:25 | x | semmle.label | x | | main.rs:101:29:101:29 | b | semmle.label | b | +| main.rs:109:13:109:22 | source(...) | semmle.label | source(...) | +| main.rs:110:26:110:36 | \|...\| ... : ... => .. [captured a] | semmle.label | \|...\| ... : ... => .. [captured a] | +| main.rs:110:35:110:35 | a | semmle.label | a | +| main.rs:111:26:111:36 | \|...\| ... : ... => .. [captured a] | semmle.label | \|...\| ... : ... => .. [captured a] | +| main.rs:111:35:111:35 | a | semmle.label | a | +| main.rs:112:26:112:36 | \|...\| ... : ... => .. [captured a] | semmle.label | \|...\| ... : ... => .. [captured a] | +| main.rs:112:35:112:35 | a | semmle.label | a | subpaths | main.rs:29:21:29:21 | a | main.rs:27:20:27:23 | ... | main.rs:27:26:27:52 | if cond {...} else {...} | main.rs:29:13:29:22 | f(...) | | main.rs:77:21:77:21 | a | main.rs:66:24:66:32 | ...: i64 | main.rs:66:42:72:1 | { ... } | main.rs:77:13:77:22 | f(...) | @@ -140,3 +153,6 @@ testFailures | main.rs:99:25:99:25 | x | main.rs:100:13:100:22 | source(...) | main.rs:99:25:99:25 | x | $@ | main.rs:100:13:100:22 | source(...) | source(...) | | main.rs:101:25:101:25 | x | main.rs:98:13:98:22 | source(...) | main.rs:101:25:101:25 | x | $@ | main.rs:98:13:98:22 | source(...) | source(...) | | main.rs:101:25:101:25 | x | main.rs:100:13:100:22 | source(...) | main.rs:101:25:101:25 | x | $@ | main.rs:100:13:100:22 | source(...) | source(...) | +| main.rs:110:35:110:35 | a | main.rs:109:13:109:22 | source(...) | main.rs:110:35:110:35 | a | $@ | main.rs:109:13:109:22 | source(...) | source(...) | +| main.rs:111:35:111:35 | a | main.rs:109:13:109:22 | source(...) | main.rs:111:35:111:35 | a | $@ | main.rs:109:13:109:22 | source(...) | source(...) | +| main.rs:112:35:112:35 | a | main.rs:109:13:109:22 | source(...) | main.rs:112:35:112:35 | a | $@ | main.rs:109:13:109:22 | source(...) | source(...) | diff --git a/rust/ql/test/library-tests/dataflow/lambdas/main.rs b/rust/ql/test/library-tests/dataflow/lambdas/main.rs index 817409bbc369..476a7570e018 100644 --- a/rust/ql/test/library-tests/dataflow/lambdas/main.rs +++ b/rust/ql/test/library-tests/dataflow/lambdas/main.rs @@ -107,9 +107,9 @@ use external_file::*; fn test_external_call() { let a = source(81); - may_invoke_callback1(|x| sink(a)); // $ MISSING: hasValueFlow=81 - may_invoke_callback2(|x| sink(a)); // $ MISSING: hasValueFlow=81 - may_invoke_callback3(|x| sink(a)); // $ MISSING: hasValueFlow=81 + may_invoke_callback1(|x| sink(a)); // $ hasValueFlow=81 + may_invoke_callback2(|x| sink(a)); // $ hasValueFlow=81 + may_invoke_callback3(|x| sink(a)); // $ hasValueFlow=81 } fn main() { From 1e6ef80a7e2de97b4b45f36ce2365330af65d644 Mon Sep 17 00:00:00 2001 From: Tom Hvitved Date: Thu, 6 Aug 2026 21:35:30 +0200 Subject: [PATCH 3/3] Rust: Add another captured varible flow test --- .../ql/test/library-tests/dataflow/lambdas/external_file.rs | 6 ++++++ .../library-tests/dataflow/lambdas/inline-flow.expected | 5 +++++ rust/ql/test/library-tests/dataflow/lambdas/main.rs | 1 + 3 files changed, 12 insertions(+) diff --git a/rust/ql/test/library-tests/dataflow/lambdas/external_file.rs b/rust/ql/test/library-tests/dataflow/lambdas/external_file.rs index 190733fd482a..c43269e48c16 100644 --- a/rust/ql/test/library-tests/dataflow/lambdas/external_file.rs +++ b/rust/ql/test/library-tests/dataflow/lambdas/external_file.rs @@ -3,3 +3,9 @@ pub fn may_invoke_callback1(f: F) {} pub fn may_invoke_callback2(f: F) {} pub fn may_invoke_callback3(f: impl Fn(i64)) {} + +pub fn may_invoke_callback4(f: T) +where + T: for<'a> FnOnce(&'a mut i64), +{ +} diff --git a/rust/ql/test/library-tests/dataflow/lambdas/inline-flow.expected b/rust/ql/test/library-tests/dataflow/lambdas/inline-flow.expected index 049c30c06add..08fb2a66015e 100644 --- a/rust/ql/test/library-tests/dataflow/lambdas/inline-flow.expected +++ b/rust/ql/test/library-tests/dataflow/lambdas/inline-flow.expected @@ -58,9 +58,11 @@ edges | main.rs:109:13:109:22 | source(...) | main.rs:110:26:110:36 | \|...\| ... : ... => .. [captured a] | provenance | | | main.rs:109:13:109:22 | source(...) | main.rs:111:26:111:36 | \|...\| ... : ... => .. [captured a] | provenance | | | main.rs:109:13:109:22 | source(...) | main.rs:112:26:112:36 | \|...\| ... : ... => .. [captured a] | provenance | | +| main.rs:109:13:109:22 | source(...) | main.rs:113:26:113:36 | \|...\| ... : ... => .. [captured a] | provenance | | | main.rs:110:26:110:36 | \|...\| ... : ... => .. [captured a] | main.rs:110:35:110:35 | a | provenance | heuristic-callback | | main.rs:111:26:111:36 | \|...\| ... : ... => .. [captured a] | main.rs:111:35:111:35 | a | provenance | heuristic-callback | | main.rs:112:26:112:36 | \|...\| ... : ... => .. [captured a] | main.rs:112:35:112:35 | a | provenance | heuristic-callback | +| main.rs:113:26:113:36 | \|...\| ... : ... => .. [captured a] | main.rs:113:35:113:35 | a | provenance | heuristic-callback | nodes | main.rs:10:20:10:52 | if cond {...} else {...} | semmle.label | if cond {...} else {...} | | main.rs:10:30:10:39 | source(...) | semmle.label | source(...) | @@ -134,6 +136,8 @@ nodes | main.rs:111:35:111:35 | a | semmle.label | a | | main.rs:112:26:112:36 | \|...\| ... : ... => .. [captured a] | semmle.label | \|...\| ... : ... => .. [captured a] | | main.rs:112:35:112:35 | a | semmle.label | a | +| main.rs:113:26:113:36 | \|...\| ... : ... => .. [captured a] | semmle.label | \|...\| ... : ... => .. [captured a] | +| main.rs:113:35:113:35 | a | semmle.label | a | subpaths | main.rs:29:21:29:21 | a | main.rs:27:20:27:23 | ... | main.rs:27:26:27:52 | if cond {...} else {...} | main.rs:29:13:29:22 | f(...) | | main.rs:77:21:77:21 | a | main.rs:66:24:66:32 | ...: i64 | main.rs:66:42:72:1 | { ... } | main.rs:77:13:77:22 | f(...) | @@ -156,3 +160,4 @@ testFailures | main.rs:110:35:110:35 | a | main.rs:109:13:109:22 | source(...) | main.rs:110:35:110:35 | a | $@ | main.rs:109:13:109:22 | source(...) | source(...) | | main.rs:111:35:111:35 | a | main.rs:109:13:109:22 | source(...) | main.rs:111:35:111:35 | a | $@ | main.rs:109:13:109:22 | source(...) | source(...) | | main.rs:112:35:112:35 | a | main.rs:109:13:109:22 | source(...) | main.rs:112:35:112:35 | a | $@ | main.rs:109:13:109:22 | source(...) | source(...) | +| main.rs:113:35:113:35 | a | main.rs:109:13:109:22 | source(...) | main.rs:113:35:113:35 | a | $@ | main.rs:109:13:109:22 | source(...) | source(...) | diff --git a/rust/ql/test/library-tests/dataflow/lambdas/main.rs b/rust/ql/test/library-tests/dataflow/lambdas/main.rs index 476a7570e018..742974eaab9c 100644 --- a/rust/ql/test/library-tests/dataflow/lambdas/main.rs +++ b/rust/ql/test/library-tests/dataflow/lambdas/main.rs @@ -110,6 +110,7 @@ fn test_external_call() { may_invoke_callback1(|x| sink(a)); // $ hasValueFlow=81 may_invoke_callback2(|x| sink(a)); // $ hasValueFlow=81 may_invoke_callback3(|x| sink(a)); // $ hasValueFlow=81 + may_invoke_callback4(|x| sink(a)); // $ hasValueFlow=81 } fn main() {