From 26a1f4888a7fc82e3361d880e0abb31a8c52f5b4 Mon Sep 17 00:00:00 2001 From: Jeroen Ketema Date: Mon, 16 Feb 2026 17:19:44 +0100 Subject: [PATCH 1/7] C++: Modernize `MustFlow` using parameterized modules --- .../semmle/code/cpp/ir/dataflow/MustFlow.qll | 539 +++++++++--------- .../ReturnStackAllocatedMemory.ql | 24 +- .../Memory Management/UninitializedLocal.ql | 21 +- cpp/ql/src/Likely Bugs/OO/UnsafeUseOfThis.ql | 26 +- 4 files changed, 299 insertions(+), 311 deletions(-) diff --git a/cpp/ql/lib/semmle/code/cpp/ir/dataflow/MustFlow.qll b/cpp/ql/lib/semmle/code/cpp/ir/dataflow/MustFlow.qll index b085440f6bcd..07e79a907c73 100644 --- a/cpp/ql/lib/semmle/code/cpp/ir/dataflow/MustFlow.qll +++ b/cpp/ql/lib/semmle/code/cpp/ir/dataflow/MustFlow.qll @@ -7,290 +7,279 @@ private import cpp private import semmle.code.cpp.ir.IR -/** - * A configuration of a data flow analysis that performs must-flow analysis. This is different - * from `DataFlow.qll` which performs may-flow analysis (i.e., it finds paths where the source _may_ - * flow to the sink). - * - * Like in `DataFlow.qll`, each use of the `MustFlow.qll` library must define its own unique extension - * of this abstract class. To create a configuration, extend this class with a subclass whose - * characteristic predicate is a unique singleton string and override `isSource`, `isSink` (and - * `isAdditionalFlowStep` if additional steps are required). - */ -abstract class MustFlowConfiguration extends string { - bindingset[this] - MustFlowConfiguration() { any() } - - /** - * Holds if `source` is a relevant data flow source. - */ - abstract predicate isSource(Instruction source); - - /** - * Holds if `sink` is a relevant data flow sink. - */ - abstract predicate isSink(Operand sink); - - /** - * Holds if data flow through `instr` is prohibited. - */ - predicate isBarrier(Instruction instr) { none() } - - /** - * Holds if the additional flow step from `node1` to `node2` must be taken - * into account in the analysis. - */ - predicate isAdditionalFlowStep(Operand node1, Instruction node2) { none() } - - /** Holds if this configuration allows flow from arguments to parameters. */ - predicate allowInterproceduralFlow() { any() } - - /** - * Holds if data must flow from `source` to `sink` for this configuration. - * - * The corresponding paths are generated from the end-points and the graph - * included in the module `PathGraph`. - */ - final predicate hasFlowPath(MustFlowPathNode source, MustFlowPathSink sink) { - this.isSource(source.getInstruction()) and - source.getASuccessor*() = sink - } -} - -/** Holds if `node` flows from a source. */ -pragma[nomagic] -private predicate flowsFromSource(Instruction node, MustFlowConfiguration config) { - not config.isBarrier(node) and - ( - config.isSource(node) - or - exists(Instruction mid | - step(mid, node, config) and - flowsFromSource(mid, pragma[only_bind_into](config)) - ) - ) -} - -/** Holds if `node` flows to a sink. */ -pragma[nomagic] -private predicate flowsToSink(Instruction node, MustFlowConfiguration config) { - flowsFromSource(node, pragma[only_bind_into](config)) and - ( - config.isSink(node.getAUse()) - or - exists(Instruction mid | - step(node, mid, config) and - flowsToSink(mid, pragma[only_bind_into](config)) - ) - ) -} - -cached -private module Cached { - /** Holds if `p` is the `n`'th parameter of the non-virtual function `f`. */ - private predicate parameterOf(Parameter p, Function f, int n) { - not f.isVirtual() and f.getParameter(n) = p - } - - /** - * Holds if `instr` is the `n`'th argument to a call to the non-virtual function `f`, and - * `init` is the corresponding initialization instruction that receives the value of `instr` in `f`. - */ - private predicate flowIntoParameter( - Function f, int n, CallInstruction call, Instruction instr, InitializeParameterInstruction init - ) { - not f.isVirtual() and - call.getPositionalArgument(n) = instr and - f = call.getStaticCallTarget() and - getEnclosingNonVirtualFunctionInitializeParameter(init, f) and - init.getParameter().getIndex() = pragma[only_bind_into](pragma[only_bind_out](n)) - } - - /** - * Holds if `instr` is an argument to a call to the function `f`, and `init` is the - * corresponding initialization instruction that receives the value of `instr` in `f`. - */ - pragma[noinline] - private predicate getPositionalArgumentInitParam( - CallInstruction call, Instruction instr, InitializeParameterInstruction init, Function f - ) { - exists(int n | - parameterOf(_, f, n) and - flowIntoParameter(f, pragma[only_bind_into](pragma[only_bind_out](n)), call, instr, init) - ) - } - +module MustFlow { /** - * Holds if `instr` is the qualifier to a call to the non-virtual function `f`, and - * `init` is the corresponding initialization instruction that receives the value of - * `instr` in `f`. + * An input configuration of a data flow analysis that performs must-flow analysis. This is different + * from `DataFlow.qll` which performs may-flow analysis (i.e., it finds paths where the source _may_ + * flow to the sink). */ - pragma[noinline] - private predicate getThisArgumentInitParam( - CallInstruction call, Instruction instr, InitializeParameterInstruction init, Function f - ) { - not f.isVirtual() and - call.getStaticCallTarget() = f and - getEnclosingNonVirtualFunctionInitializeParameter(init, f) and - call.getThisArgument() = instr and - init.getIRVariable() instanceof IRThisVariable + signature module ConfigSig { + /** + * Holds if `source` is a relevant data flow source. + */ + predicate isSource(Instruction source); + + /** + * Holds if `sink` is a relevant data flow sink. + */ + predicate isSink(Operand sink); + + /** + * Holds if data flow through `instr` is prohibited. + */ + default predicate isBarrier(Instruction instr) { none() } + + /** + * Holds if the additional flow step from `node1` to `node2` must be taken + * into account in the analysis. + */ + default predicate isAdditionalFlowStep(Operand node1, Instruction node2) { none() } + + /** Holds if this configuration allows flow from arguments to parameters. */ + default predicate allowInterproceduralFlow() { any() } } - /** Holds if `f` is the enclosing non-virtual function of `init`. */ - private predicate getEnclosingNonVirtualFunctionInitializeParameter( - InitializeParameterInstruction init, Function f - ) { - not f.isVirtual() and - init.getEnclosingFunction() = f - } - - /** Holds if `f` is the enclosing non-virtual function of `init`. */ - private predicate getEnclosingNonVirtualFunctionInitializeIndirection( - InitializeIndirectionInstruction init, Function f - ) { - not f.isVirtual() and - init.getEnclosingFunction() = f - } - - /** - * Holds if `instr` is an argument (or argument indirection) to a call, and - * `succ` is the corresponding initialization instruction in the call target. - */ - private predicate flowThroughCallable(Instruction argument, Instruction parameter) { - // Flow from an argument to a parameter - exists(CallInstruction call, InitializeParameterInstruction init | init = parameter | - getPositionalArgumentInitParam(call, argument, init, call.getStaticCallTarget()) - or - getThisArgumentInitParam(call, argument, init, call.getStaticCallTarget()) - ) - or - // Flow from argument indirection to parameter indirection - exists( - CallInstruction call, ReadSideEffectInstruction read, InitializeIndirectionInstruction init - | - init = parameter and - read.getPrimaryInstruction() = call and - getEnclosingNonVirtualFunctionInitializeIndirection(init, call.getStaticCallTarget()) - | - exists(int n | - read.getSideEffectOperand().getAnyDef() = argument and - read.getIndex() = pragma[only_bind_into](n) and - init.getParameter().getIndex() = pragma[only_bind_into](n) + module Global { + import Config + + /** + * Holds if data must flow from `source` to `sink`. + * + * The corresponding paths are generated from the end-points and the graph + * included in the module `PathGraph`. + */ + predicate flowPath(PathNode source, PathSink sink) { + isSource(source.getInstruction()) and + source.getASuccessor*() = sink + } + + /** Holds if `node` flows from a source. */ + pragma[nomagic] + private predicate flowsFromSource(Instruction node) { + not isBarrier(node) and + ( + isSource(node) + or + exists(Instruction mid | + step(mid, node) and + flowsFromSource(mid) + ) + ) + } + + /** Holds if `node` flows to a sink. */ + pragma[nomagic] + private predicate flowsToSink(Instruction node) { + flowsFromSource(node) and + ( + isSink(node.getAUse()) + or + exists(Instruction mid | + step(node, mid) and + flowsToSink(mid) + ) + ) + } + + /** + * Gets the enclosing callable of `n`. Unlike `n.getEnclosingCallable()`, this + * predicate ensures that joins go from `n` to the result instead of the other + * way around. + */ + pragma[inline] + private IRFunction getEnclosingCallable(Instruction n) { + pragma[only_bind_into](result) = pragma[only_bind_out](n).getEnclosingIRFunction() + } + + /** Holds if `nodeFrom` flows to `nodeTo`. */ + private predicate step(Instruction nodeFrom, Instruction nodeTo) { + Cached::step(pragma[only_bind_into](nodeFrom), pragma[only_bind_into](nodeTo)) and + ( + allowInterproceduralFlow() + or + getEnclosingCallable(nodeFrom) = getEnclosingCallable(nodeTo) ) or - call.getThisArgument() = argument and - init.getIRVariable() instanceof IRThisVariable - ) - } - - private predicate instructionToOperandStep(Instruction instr, Operand operand) { - operand.getDef() = instr - } - - /** - * Holds if data flows from `operand` to `instr`. - * - * This predicate ignores flow through `PhiInstruction`s to create a 'must flow' relation. - */ - private predicate operandToInstructionStep(Operand operand, Instruction instr) { - instr.(CopyInstruction).getSourceValueOperand() = operand - or - instr.(ConvertInstruction).getUnaryOperand() = operand - or - instr.(CheckedConvertOrNullInstruction).getUnaryOperand() = operand - or - instr.(InheritanceConversionInstruction).getUnaryOperand() = operand - or - instr.(ChiInstruction).getTotalOperand() = operand + isAdditionalFlowStep(nodeFrom.getAUse(), nodeTo) + } + + private newtype TLocalPathNode = + MkLocalPathNode(Instruction n) { + flowsToSink(n) and + ( + isSource(n) + or + exists(PathNode mid | step(mid.getInstruction(), n)) + ) + } + + /** A `Node` that is in a path from a source to a sink. */ + class PathNode extends TLocalPathNode { + Instruction n; + + PathNode() { this = MkLocalPathNode(n) } + + /** Gets the underlying node. */ + Instruction getInstruction() { result = n } + + /** Gets a textual representation of this node. */ + string toString() { result = n.getAst().toString() } + + /** Gets the location of this element. */ + Location getLocation() { result = n.getLocation() } + + /** Gets a successor node, if any. */ + PathNode getASuccessor() { step(this.getInstruction(), result.getInstruction()) } + } + + private class PathSink extends PathNode { + PathSink() { isSink(this.getInstruction().getAUse()) } + } + + /** + * Provides the query predicates needed to include a graph in a path-problem query. + */ + module PathGraph { + private predicate reach(PathNode n) { n instanceof PathSink or reach(n.getASuccessor()) } + + /** Holds if `(a,b)` is an edge in the graph of data flow path explanations. */ + query predicate edges(PathNode a, PathNode b) { a.getASuccessor() = b and reach(b) } + + /** Holds if `n` is a node in the graph of data flow path explanations. */ + query predicate nodes(PathNode n, string key, string val) { + reach(n) and key = "semmle.label" and val = n.toString() + } + } } cached - predicate step(Instruction nodeFrom, Instruction nodeTo) { - exists(Operand mid | - instructionToOperandStep(nodeFrom, mid) and - operandToInstructionStep(mid, nodeTo) - ) - or - flowThroughCallable(nodeFrom, nodeTo) - } -} - -/** - * Gets the enclosing callable of `n`. Unlike `n.getEnclosingCallable()`, this - * predicate ensures that joins go from `n` to the result instead of the other - * way around. - */ -pragma[inline] -private IRFunction getEnclosingCallable(Instruction n) { - pragma[only_bind_into](result) = pragma[only_bind_out](n).getEnclosingIRFunction() -} - -/** Holds if `nodeFrom` flows to `nodeTo`. */ -private predicate step(Instruction nodeFrom, Instruction nodeTo, MustFlowConfiguration config) { - exists(config) and - Cached::step(pragma[only_bind_into](nodeFrom), pragma[only_bind_into](nodeTo)) and - ( - config.allowInterproceduralFlow() - or - getEnclosingCallable(nodeFrom) = getEnclosingCallable(nodeTo) - ) - or - config.isAdditionalFlowStep(nodeFrom.getAUse(), nodeTo) -} - -private newtype TLocalPathNode = - MkLocalPathNode(Instruction n, MustFlowConfiguration config) { - flowsToSink(n, config) and - ( - config.isSource(n) + private module Cached { + /** Holds if `p` is the `n`'th parameter of the non-virtual function `f`. */ + private predicate parameterOf(Parameter p, Function f, int n) { + not f.isVirtual() and f.getParameter(n) = p + } + + /** + * Holds if `instr` is the `n`'th argument to a call to the non-virtual function `f`, and + * `init` is the corresponding initialization instruction that receives the value of `instr` in `f`. + */ + private predicate flowIntoParameter( + Function f, int n, CallInstruction call, Instruction instr, + InitializeParameterInstruction init + ) { + not f.isVirtual() and + call.getPositionalArgument(n) = instr and + f = call.getStaticCallTarget() and + getEnclosingNonVirtualFunctionInitializeParameter(init, f) and + init.getParameter().getIndex() = pragma[only_bind_into](pragma[only_bind_out](n)) + } + + /** + * Holds if `instr` is an argument to a call to the function `f`, and `init` is the + * corresponding initialization instruction that receives the value of `instr` in `f`. + */ + pragma[noinline] + private predicate getPositionalArgumentInitParam( + CallInstruction call, Instruction instr, InitializeParameterInstruction init, Function f + ) { + exists(int n | + parameterOf(_, f, n) and + flowIntoParameter(f, pragma[only_bind_into](pragma[only_bind_out](n)), call, instr, init) + ) + } + + /** + * Holds if `instr` is the qualifier to a call to the non-virtual function `f`, and + * `init` is the corresponding initialization instruction that receives the value of + * `instr` in `f`. + */ + pragma[noinline] + private predicate getThisArgumentInitParam( + CallInstruction call, Instruction instr, InitializeParameterInstruction init, Function f + ) { + not f.isVirtual() and + call.getStaticCallTarget() = f and + getEnclosingNonVirtualFunctionInitializeParameter(init, f) and + call.getThisArgument() = instr and + init.getIRVariable() instanceof IRThisVariable + } + + /** Holds if `f` is the enclosing non-virtual function of `init`. */ + private predicate getEnclosingNonVirtualFunctionInitializeParameter( + InitializeParameterInstruction init, Function f + ) { + not f.isVirtual() and + init.getEnclosingFunction() = f + } + + /** Holds if `f` is the enclosing non-virtual function of `init`. */ + private predicate getEnclosingNonVirtualFunctionInitializeIndirection( + InitializeIndirectionInstruction init, Function f + ) { + not f.isVirtual() and + init.getEnclosingFunction() = f + } + + /** + * Holds if `instr` is an argument (or argument indirection) to a call, and + * `succ` is the corresponding initialization instruction in the call target. + */ + private predicate flowThroughCallable(Instruction argument, Instruction parameter) { + // Flow from an argument to a parameter + exists(CallInstruction call, InitializeParameterInstruction init | init = parameter | + getPositionalArgumentInitParam(call, argument, init, call.getStaticCallTarget()) + or + getThisArgumentInitParam(call, argument, init, call.getStaticCallTarget()) + ) or - exists(MustFlowPathNode mid | step(mid.getInstruction(), n, config)) - ) - } - -/** A `Node` that is in a path from a source to a sink. */ -class MustFlowPathNode extends TLocalPathNode { - Instruction n; - - MustFlowPathNode() { this = MkLocalPathNode(n, _) } - - /** Gets the underlying node. */ - Instruction getInstruction() { result = n } - - /** Gets a textual representation of this node. */ - string toString() { result = n.getAst().toString() } - - /** Gets the location of this element. */ - Location getLocation() { result = n.getLocation() } - - /** Gets a successor node, if any. */ - MustFlowPathNode getASuccessor() { - step(this.getInstruction(), result.getInstruction(), this.getConfiguration()) - } - - /** Gets the associated configuration. */ - MustFlowConfiguration getConfiguration() { this = MkLocalPathNode(_, result) } -} - -private class MustFlowPathSink extends MustFlowPathNode { - MustFlowPathSink() { this.getConfiguration().isSink(this.getInstruction().getAUse()) } -} - -/** - * Provides the query predicates needed to include a graph in a path-problem query. - */ -module PathGraph { - private predicate reach(MustFlowPathNode n) { - n instanceof MustFlowPathSink or reach(n.getASuccessor()) - } - - /** Holds if `(a,b)` is an edge in the graph of data flow path explanations. */ - query predicate edges(MustFlowPathNode a, MustFlowPathNode b) { - a.getASuccessor() = b and reach(b) - } - - /** Holds if `n` is a node in the graph of data flow path explanations. */ - query predicate nodes(MustFlowPathNode n, string key, string val) { - reach(n) and key = "semmle.label" and val = n.toString() + // Flow from argument indirection to parameter indirection + exists( + CallInstruction call, ReadSideEffectInstruction read, InitializeIndirectionInstruction init + | + init = parameter and + read.getPrimaryInstruction() = call and + getEnclosingNonVirtualFunctionInitializeIndirection(init, call.getStaticCallTarget()) + | + exists(int n | + read.getSideEffectOperand().getAnyDef() = argument and + read.getIndex() = pragma[only_bind_into](n) and + init.getParameter().getIndex() = pragma[only_bind_into](n) + ) + or + call.getThisArgument() = argument and + init.getIRVariable() instanceof IRThisVariable + ) + } + + private predicate instructionToOperandStep(Instruction instr, Operand operand) { + operand.getDef() = instr + } + + /** + * Holds if data flows from `operand` to `instr`. + * + * This predicate ignores flow through `PhiInstruction`s to create a 'must flow' relation. + */ + private predicate operandToInstructionStep(Operand operand, Instruction instr) { + instr.(CopyInstruction).getSourceValueOperand() = operand + or + instr.(ConvertInstruction).getUnaryOperand() = operand + or + instr.(CheckedConvertOrNullInstruction).getUnaryOperand() = operand + or + instr.(InheritanceConversionInstruction).getUnaryOperand() = operand + or + instr.(ChiInstruction).getTotalOperand() = operand + } + + cached + predicate step(Instruction nodeFrom, Instruction nodeTo) { + exists(Operand mid | + instructionToOperandStep(nodeFrom, mid) and + operandToInstructionStep(mid, nodeTo) + ) + or + flowThroughCallable(nodeFrom, nodeTo) + } } } diff --git a/cpp/ql/src/Likely Bugs/Memory Management/ReturnStackAllocatedMemory.ql b/cpp/ql/src/Likely Bugs/Memory Management/ReturnStackAllocatedMemory.ql index b87889103322..efd136bcd2df 100644 --- a/cpp/ql/src/Likely Bugs/Memory Management/ReturnStackAllocatedMemory.ql +++ b/cpp/ql/src/Likely Bugs/Memory Management/ReturnStackAllocatedMemory.ql @@ -16,17 +16,15 @@ import cpp import semmle.code.cpp.ir.IR import semmle.code.cpp.ir.dataflow.MustFlow -import PathGraph +import ReturnStackAllocatedMemory::PathGraph /** Holds if `f` has a name that we interpret as evidence of intentionally returning the value of the stack pointer. */ predicate intentionallyReturnsStackPointer(Function f) { f.getName().toLowerCase().matches(["%stack%", "%sp%"]) } -class ReturnStackAllocatedMemoryConfig extends MustFlowConfiguration { - ReturnStackAllocatedMemoryConfig() { this = "ReturnStackAllocatedMemoryConfig" } - - override predicate isSource(Instruction source) { +module ReturnStackAllocatedMemoryConfig implements MustFlow::ConfigSig { + predicate isSource(Instruction source) { exists(Function func | // Rule out FPs caused by extraction errors. not func.hasErrors() and @@ -50,7 +48,7 @@ class ReturnStackAllocatedMemoryConfig extends MustFlowConfiguration { ) } - override predicate isSink(Operand sink) { + predicate isSink(Operand sink) { // Holds if `sink` is a node that represents the `StoreInstruction` that is subsequently used in // a `ReturnValueInstruction`. // We use the `StoreInstruction` instead of the instruction that defines the @@ -72,7 +70,7 @@ class ReturnStackAllocatedMemoryConfig extends MustFlowConfiguration { // int* px = id(&x); // } // ``` - override predicate allowInterproceduralFlow() { none() } + predicate allowInterproceduralFlow() { none() } /** * This configuration intentionally conflates addresses of fields and their object, and pointer offsets @@ -87,20 +85,22 @@ class ReturnStackAllocatedMemoryConfig extends MustFlowConfiguration { * } * ``` */ - override predicate isAdditionalFlowStep(Operand node1, Instruction node2) { + predicate isAdditionalFlowStep(Operand node1, Instruction node2) { node2.(FieldAddressInstruction).getObjectAddressOperand() = node1 or node2.(PointerOffsetInstruction).getLeftOperand() = node1 } - override predicate isBarrier(Instruction n) { n.getResultType() instanceof ErroneousType } + predicate isBarrier(Instruction n) { n.getResultType() instanceof ErroneousType } } +module ReturnStackAllocatedMemory = MustFlow::Global; + from - MustFlowPathNode source, MustFlowPathNode sink, Instruction instr, - ReturnStackAllocatedMemoryConfig conf + ReturnStackAllocatedMemory::PathNode source, ReturnStackAllocatedMemory::PathNode sink, + Instruction instr where - conf.hasFlowPath(pragma[only_bind_into](source), pragma[only_bind_into](sink)) and + ReturnStackAllocatedMemory::flowPath(pragma[only_bind_into](source), pragma[only_bind_into](sink)) and source.getInstruction() = instr select sink.getInstruction(), source, sink, "May return stack-allocated memory from $@.", instr.getAst(), instr.getAst().toString() diff --git a/cpp/ql/src/Likely Bugs/Memory Management/UninitializedLocal.ql b/cpp/ql/src/Likely Bugs/Memory Management/UninitializedLocal.ql index 763a142f1b90..1697ad318105 100644 --- a/cpp/ql/src/Likely Bugs/Memory Management/UninitializedLocal.ql +++ b/cpp/ql/src/Likely Bugs/Memory Management/UninitializedLocal.ql @@ -15,7 +15,7 @@ import cpp import semmle.code.cpp.ir.IR import semmle.code.cpp.ir.dataflow.MustFlow -import PathGraph +import UninitializedLocal::PathGraph /** * Auxiliary predicate: Types that don't require initialization @@ -70,25 +70,26 @@ predicate isSinkImpl(Instruction sink, VariableAccess va) { ) } -class MustFlow extends MustFlowConfiguration { - MustFlow() { this = "MustFlow" } - - override predicate isSource(Instruction source) { +module UninitializedLocalConfig implements MustFlow::ConfigSig { + predicate isSource(Instruction source) { source instanceof UninitializedInstruction and exists(Type t | t = source.getResultType() | not allocatedType(t)) } - override predicate isSink(Operand sink) { isSinkImpl(sink.getDef(), _) } + predicate isSink(Operand sink) { isSinkImpl(sink.getDef(), _) } - override predicate allowInterproceduralFlow() { none() } + predicate allowInterproceduralFlow() { none() } - override predicate isBarrier(Instruction instr) { instr instanceof ChiInstruction } + predicate isBarrier(Instruction instr) { instr instanceof ChiInstruction } } +module UninitializedLocal = MustFlow::Global; + from - VariableAccess va, LocalVariable v, MustFlow conf, MustFlowPathNode source, MustFlowPathNode sink + VariableAccess va, LocalVariable v, UninitializedLocal::PathNode source, + UninitializedLocal::PathNode sink where - conf.hasFlowPath(source, sink) and + UninitializedLocal::flowPath(source, sink) and isSinkImpl(sink.getInstruction(), va) and v = va.getTarget() select va, source, sink, "The variable $@ may not be initialized at this access.", v, v.getName() diff --git a/cpp/ql/src/Likely Bugs/OO/UnsafeUseOfThis.ql b/cpp/ql/src/Likely Bugs/OO/UnsafeUseOfThis.ql index bb62cfc17553..63b56d470e2d 100644 --- a/cpp/ql/src/Likely Bugs/OO/UnsafeUseOfThis.ql +++ b/cpp/ql/src/Likely Bugs/OO/UnsafeUseOfThis.ql @@ -17,16 +17,16 @@ import cpp import semmle.code.cpp.ir.IR import semmle.code.cpp.ir.dataflow.MustFlow -import PathGraph +import UnsafeUseOfThis::PathGraph -class UnsafeUseOfThisConfig extends MustFlowConfiguration { - UnsafeUseOfThisConfig() { this = "UnsafeUseOfThisConfig" } +module UnsafeUseOfThisConfig implements MustFlow::ConfigSig { + predicate isSource(Instruction source) { isSource(source, _, _) } - override predicate isSource(Instruction source) { isSource(source, _, _) } - - override predicate isSink(Operand sink) { isSink(sink, _) } + predicate isSink(Operand sink) { isSink(sink, _) } } +module UnsafeUseOfThis = MustFlow::Global; + /** Holds if `sink` is a `this` pointer used by the call instruction `call`. */ predicate isSink(Operand sink, CallInstruction call) { exists(PureVirtualFunction func | @@ -66,19 +66,17 @@ predicate isSource(InitializeParameterInstruction source, string msg, Class c) { * - `msg` is a string describing whether `source` is from a constructor or destructor. */ predicate flows( - MustFlowPathNode source, string msg, Class sourceClass, MustFlowPathNode sink, + UnsafeUseOfThis::PathNode source, string msg, Class sourceClass, UnsafeUseOfThis::PathNode sink, CallInstruction call ) { - exists(UnsafeUseOfThisConfig conf | - conf.hasFlowPath(source, sink) and - isSource(source.getInstruction(), msg, sourceClass) and - isSink(sink.getInstruction().getAUse(), call) - ) + UnsafeUseOfThis::flowPath(source, sink) and + isSource(source.getInstruction(), msg, sourceClass) and + isSink(sink.getInstruction().getAUse(), call) } from - MustFlowPathNode source, MustFlowPathNode sink, CallInstruction call, string msg, - Class sourceClass + UnsafeUseOfThis::PathNode source, UnsafeUseOfThis::PathNode sink, CallInstruction call, + string msg, Class sourceClass where flows(source, msg, sourceClass, sink, call) and // Only raise an alert if there is no override of the pure virtual function in any base class. From 73194a5e86b5e6abc614fa75caaeed3110857554 Mon Sep 17 00:00:00 2001 From: Jeroen Ketema Date: Mon, 16 Feb 2026 17:40:18 +0100 Subject: [PATCH 2/7] C++: Fix QL-for-QL warnings and missing QLDoc --- .../semmle/code/cpp/ir/dataflow/MustFlow.qll | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/cpp/ql/lib/semmle/code/cpp/ir/dataflow/MustFlow.qll b/cpp/ql/lib/semmle/code/cpp/ir/dataflow/MustFlow.qll index 07e79a907c73..5a0f34c6dc1a 100644 --- a/cpp/ql/lib/semmle/code/cpp/ir/dataflow/MustFlow.qll +++ b/cpp/ql/lib/semmle/code/cpp/ir/dataflow/MustFlow.qll @@ -7,6 +7,9 @@ private import cpp private import semmle.code.cpp.ir.IR +/** + * Provides an inter-procedural must-flow data flow analysis. + */ module MustFlow { /** * An input configuration of a data flow analysis that performs must-flow analysis. This is different @@ -39,6 +42,9 @@ module MustFlow { default predicate allowInterproceduralFlow() { any() } } + /** + * Constructs a global must-flow computation. + */ module Global { import Config @@ -170,7 +176,7 @@ module MustFlow { not f.isVirtual() and call.getPositionalArgument(n) = instr and f = call.getStaticCallTarget() and - getEnclosingNonVirtualFunctionInitializeParameter(init, f) and + isEnclosingNonVirtualFunctionInitializeParameter(init, f) and init.getParameter().getIndex() = pragma[only_bind_into](pragma[only_bind_out](n)) } @@ -179,7 +185,7 @@ module MustFlow { * corresponding initialization instruction that receives the value of `instr` in `f`. */ pragma[noinline] - private predicate getPositionalArgumentInitParam( + private predicate isPositionalArgumentInitParam( CallInstruction call, Instruction instr, InitializeParameterInstruction init, Function f ) { exists(int n | @@ -194,18 +200,18 @@ module MustFlow { * `instr` in `f`. */ pragma[noinline] - private predicate getThisArgumentInitParam( + private predicate isThisArgumentInitParam( CallInstruction call, Instruction instr, InitializeParameterInstruction init, Function f ) { not f.isVirtual() and call.getStaticCallTarget() = f and - getEnclosingNonVirtualFunctionInitializeParameter(init, f) and + isEnclosingNonVirtualFunctionInitializeParameter(init, f) and call.getThisArgument() = instr and init.getIRVariable() instanceof IRThisVariable } /** Holds if `f` is the enclosing non-virtual function of `init`. */ - private predicate getEnclosingNonVirtualFunctionInitializeParameter( + private predicate isEnclosingNonVirtualFunctionInitializeParameter( InitializeParameterInstruction init, Function f ) { not f.isVirtual() and @@ -213,7 +219,7 @@ module MustFlow { } /** Holds if `f` is the enclosing non-virtual function of `init`. */ - private predicate getEnclosingNonVirtualFunctionInitializeIndirection( + private predicate isEnclosingNonVirtualFunctionInitializeIndirection( InitializeIndirectionInstruction init, Function f ) { not f.isVirtual() and @@ -221,15 +227,15 @@ module MustFlow { } /** - * Holds if `instr` is an argument (or argument indirection) to a call, and - * `succ` is the corresponding initialization instruction in the call target. + * Holds if `argument` is an argument (or argument indirection) to a call, and + * `parameter` is the corresponding initialization instruction in the call target. */ private predicate flowThroughCallable(Instruction argument, Instruction parameter) { // Flow from an argument to a parameter exists(CallInstruction call, InitializeParameterInstruction init | init = parameter | - getPositionalArgumentInitParam(call, argument, init, call.getStaticCallTarget()) + isPositionalArgumentInitParam(call, argument, init, call.getStaticCallTarget()) or - getThisArgumentInitParam(call, argument, init, call.getStaticCallTarget()) + isThisArgumentInitParam(call, argument, init, call.getStaticCallTarget()) ) or // Flow from argument indirection to parameter indirection @@ -238,7 +244,7 @@ module MustFlow { | init = parameter and read.getPrimaryInstruction() = call and - getEnclosingNonVirtualFunctionInitializeIndirection(init, call.getStaticCallTarget()) + isEnclosingNonVirtualFunctionInitializeIndirection(init, call.getStaticCallTarget()) | exists(int n | read.getSideEffectOperand().getAnyDef() = argument and From 366ebcad834dc2a8b93e314fa80624f7ed7b5124 Mon Sep 17 00:00:00 2001 From: Jeroen Ketema Date: Mon, 16 Feb 2026 18:58:39 +0100 Subject: [PATCH 3/7] C++: Add `cpp/return-stack-allocated-memory` test case --- .../ReturnStackAllocatedMemory.expected | 8 ++++++++ .../Memory Management/ReturnStackAllocatedMemory/test.cpp | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/cpp/ql/test/query-tests/Likely Bugs/Memory Management/ReturnStackAllocatedMemory/ReturnStackAllocatedMemory.expected b/cpp/ql/test/query-tests/Likely Bugs/Memory Management/ReturnStackAllocatedMemory/ReturnStackAllocatedMemory.expected index 6aa457b1e8a4..23b23dc4a3b2 100644 --- a/cpp/ql/test/query-tests/Likely Bugs/Memory Management/ReturnStackAllocatedMemory/ReturnStackAllocatedMemory.expected +++ b/cpp/ql/test/query-tests/Likely Bugs/Memory Management/ReturnStackAllocatedMemory/ReturnStackAllocatedMemory.expected @@ -48,6 +48,9 @@ edges | test.cpp:249:13:249:20 | call to strndupa | test.cpp:249:13:249:20 | call to strndupa | | test.cpp:249:13:249:20 | call to strndupa | test.cpp:250:9:250:10 | s2 | | test.cpp:250:9:250:10 | s2 | test.cpp:250:9:250:10 | (void *)... | +| test.cpp:253:17:253:17 | p | test.cpp:256:10:256:10 | p | +| test.cpp:255:19:255:20 | & ... | test.cpp:253:17:253:17 | p | +| test.cpp:255:20:255:20 | x | test.cpp:255:19:255:20 | & ... | nodes | test.cpp:17:9:17:11 | & ... | semmle.label | & ... | | test.cpp:17:10:17:11 | mc | semmle.label | mc | @@ -114,6 +117,10 @@ nodes | test.cpp:249:13:249:20 | call to strndupa | semmle.label | call to strndupa | | test.cpp:250:9:250:10 | (void *)... | semmle.label | (void *)... | | test.cpp:250:9:250:10 | s2 | semmle.label | s2 | +| test.cpp:253:17:253:17 | p | semmle.label | p | +| test.cpp:255:19:255:20 | & ... | semmle.label | & ... | +| test.cpp:255:20:255:20 | x | semmle.label | x | +| test.cpp:256:10:256:10 | p | semmle.label | p | #select | test.cpp:17:9:17:11 | CopyValue: & ... | test.cpp:17:10:17:11 | mc | test.cpp:17:9:17:11 | & ... | May return stack-allocated memory from $@. | test.cpp:17:10:17:11 | mc | mc | | test.cpp:25:9:25:11 | Load: ptr | test.cpp:23:18:23:19 | mc | test.cpp:25:9:25:11 | ptr | May return stack-allocated memory from $@. | test.cpp:23:18:23:19 | mc | mc | @@ -131,3 +138,4 @@ nodes | test.cpp:238:9:238:9 | Load: p | test.cpp:237:12:237:17 | call to alloca | test.cpp:238:9:238:9 | p | May return stack-allocated memory from $@. | test.cpp:237:12:237:17 | call to alloca | call to alloca | | test.cpp:245:9:245:15 | Call: call to strdupa | test.cpp:245:9:245:15 | call to strdupa | test.cpp:245:9:245:15 | call to strdupa | May return stack-allocated memory from $@. | test.cpp:245:9:245:15 | call to strdupa | call to strdupa | | test.cpp:250:9:250:10 | Convert: (void *)... | test.cpp:249:13:249:20 | call to strndupa | test.cpp:250:9:250:10 | (void *)... | May return stack-allocated memory from $@. | test.cpp:249:13:249:20 | call to strndupa | call to strndupa | +| test.cpp:256:10:256:10 | Load: p | test.cpp:255:20:255:20 | x | test.cpp:256:10:256:10 | p | May return stack-allocated memory from $@. | test.cpp:255:20:255:20 | x | x | diff --git a/cpp/ql/test/query-tests/Likely Bugs/Memory Management/ReturnStackAllocatedMemory/test.cpp b/cpp/ql/test/query-tests/Likely Bugs/Memory Management/ReturnStackAllocatedMemory/test.cpp index abc21aa74d81..ab1a626a4b07 100644 --- a/cpp/ql/test/query-tests/Likely Bugs/Memory Management/ReturnStackAllocatedMemory/test.cpp +++ b/cpp/ql/test/query-tests/Likely Bugs/Memory Management/ReturnStackAllocatedMemory/test.cpp @@ -250,3 +250,8 @@ void* test_strndupa(const char* s, size_t size) { return s2; // BAD } +int* f_rec(int *p, bool b) { + int x; + int* px = f_rec(&x, b); // GOOD [FALSE POSITIVE] + return p; +} From 4efbc6ea9beebfe21544fee894c2e0dcf1cdc11f Mon Sep 17 00:00:00 2001 From: Jeroen Ketema Date: Mon, 16 Feb 2026 19:04:23 +0100 Subject: [PATCH 4/7] C++: Handle `allowInterproceduralFlow` correctly in case of recursive functions --- .../semmle/code/cpp/ir/dataflow/MustFlow.qll | 27 +++++-------------- .../ReturnStackAllocatedMemory.expected | 8 ------ .../ReturnStackAllocatedMemory/test.cpp | 2 +- 3 files changed, 8 insertions(+), 29 deletions(-) diff --git a/cpp/ql/lib/semmle/code/cpp/ir/dataflow/MustFlow.qll b/cpp/ql/lib/semmle/code/cpp/ir/dataflow/MustFlow.qll index 5a0f34c6dc1a..a8adb16849c8 100644 --- a/cpp/ql/lib/semmle/code/cpp/ir/dataflow/MustFlow.qll +++ b/cpp/ql/lib/semmle/code/cpp/ir/dataflow/MustFlow.qll @@ -87,24 +87,12 @@ module MustFlow { ) } - /** - * Gets the enclosing callable of `n`. Unlike `n.getEnclosingCallable()`, this - * predicate ensures that joins go from `n` to the result instead of the other - * way around. - */ - pragma[inline] - private IRFunction getEnclosingCallable(Instruction n) { - pragma[only_bind_into](result) = pragma[only_bind_out](n).getEnclosingIRFunction() - } - /** Holds if `nodeFrom` flows to `nodeTo`. */ private predicate step(Instruction nodeFrom, Instruction nodeTo) { - Cached::step(pragma[only_bind_into](nodeFrom), pragma[only_bind_into](nodeTo)) and - ( - allowInterproceduralFlow() - or - getEnclosingCallable(nodeFrom) = getEnclosingCallable(nodeTo) - ) + Cached::localStep(pragma[only_bind_into](nodeFrom), pragma[only_bind_into](nodeTo)) + or + allowInterproceduralFlow() and + Cached::flowThroughCallable(pragma[only_bind_into](nodeFrom), pragma[only_bind_into](nodeTo)) or isAdditionalFlowStep(nodeFrom.getAUse(), nodeTo) } @@ -230,7 +218,8 @@ module MustFlow { * Holds if `argument` is an argument (or argument indirection) to a call, and * `parameter` is the corresponding initialization instruction in the call target. */ - private predicate flowThroughCallable(Instruction argument, Instruction parameter) { + cached + predicate flowThroughCallable(Instruction argument, Instruction parameter) { // Flow from an argument to a parameter exists(CallInstruction call, InitializeParameterInstruction init | init = parameter | isPositionalArgumentInitParam(call, argument, init, call.getStaticCallTarget()) @@ -279,13 +268,11 @@ module MustFlow { } cached - predicate step(Instruction nodeFrom, Instruction nodeTo) { + predicate localStep(Instruction nodeFrom, Instruction nodeTo) { exists(Operand mid | instructionToOperandStep(nodeFrom, mid) and operandToInstructionStep(mid, nodeTo) ) - or - flowThroughCallable(nodeFrom, nodeTo) } } } diff --git a/cpp/ql/test/query-tests/Likely Bugs/Memory Management/ReturnStackAllocatedMemory/ReturnStackAllocatedMemory.expected b/cpp/ql/test/query-tests/Likely Bugs/Memory Management/ReturnStackAllocatedMemory/ReturnStackAllocatedMemory.expected index 23b23dc4a3b2..6aa457b1e8a4 100644 --- a/cpp/ql/test/query-tests/Likely Bugs/Memory Management/ReturnStackAllocatedMemory/ReturnStackAllocatedMemory.expected +++ b/cpp/ql/test/query-tests/Likely Bugs/Memory Management/ReturnStackAllocatedMemory/ReturnStackAllocatedMemory.expected @@ -48,9 +48,6 @@ edges | test.cpp:249:13:249:20 | call to strndupa | test.cpp:249:13:249:20 | call to strndupa | | test.cpp:249:13:249:20 | call to strndupa | test.cpp:250:9:250:10 | s2 | | test.cpp:250:9:250:10 | s2 | test.cpp:250:9:250:10 | (void *)... | -| test.cpp:253:17:253:17 | p | test.cpp:256:10:256:10 | p | -| test.cpp:255:19:255:20 | & ... | test.cpp:253:17:253:17 | p | -| test.cpp:255:20:255:20 | x | test.cpp:255:19:255:20 | & ... | nodes | test.cpp:17:9:17:11 | & ... | semmle.label | & ... | | test.cpp:17:10:17:11 | mc | semmle.label | mc | @@ -117,10 +114,6 @@ nodes | test.cpp:249:13:249:20 | call to strndupa | semmle.label | call to strndupa | | test.cpp:250:9:250:10 | (void *)... | semmle.label | (void *)... | | test.cpp:250:9:250:10 | s2 | semmle.label | s2 | -| test.cpp:253:17:253:17 | p | semmle.label | p | -| test.cpp:255:19:255:20 | & ... | semmle.label | & ... | -| test.cpp:255:20:255:20 | x | semmle.label | x | -| test.cpp:256:10:256:10 | p | semmle.label | p | #select | test.cpp:17:9:17:11 | CopyValue: & ... | test.cpp:17:10:17:11 | mc | test.cpp:17:9:17:11 | & ... | May return stack-allocated memory from $@. | test.cpp:17:10:17:11 | mc | mc | | test.cpp:25:9:25:11 | Load: ptr | test.cpp:23:18:23:19 | mc | test.cpp:25:9:25:11 | ptr | May return stack-allocated memory from $@. | test.cpp:23:18:23:19 | mc | mc | @@ -138,4 +131,3 @@ nodes | test.cpp:238:9:238:9 | Load: p | test.cpp:237:12:237:17 | call to alloca | test.cpp:238:9:238:9 | p | May return stack-allocated memory from $@. | test.cpp:237:12:237:17 | call to alloca | call to alloca | | test.cpp:245:9:245:15 | Call: call to strdupa | test.cpp:245:9:245:15 | call to strdupa | test.cpp:245:9:245:15 | call to strdupa | May return stack-allocated memory from $@. | test.cpp:245:9:245:15 | call to strdupa | call to strdupa | | test.cpp:250:9:250:10 | Convert: (void *)... | test.cpp:249:13:249:20 | call to strndupa | test.cpp:250:9:250:10 | (void *)... | May return stack-allocated memory from $@. | test.cpp:249:13:249:20 | call to strndupa | call to strndupa | -| test.cpp:256:10:256:10 | Load: p | test.cpp:255:20:255:20 | x | test.cpp:256:10:256:10 | p | May return stack-allocated memory from $@. | test.cpp:255:20:255:20 | x | x | diff --git a/cpp/ql/test/query-tests/Likely Bugs/Memory Management/ReturnStackAllocatedMemory/test.cpp b/cpp/ql/test/query-tests/Likely Bugs/Memory Management/ReturnStackAllocatedMemory/test.cpp index ab1a626a4b07..abde10eb6e75 100644 --- a/cpp/ql/test/query-tests/Likely Bugs/Memory Management/ReturnStackAllocatedMemory/test.cpp +++ b/cpp/ql/test/query-tests/Likely Bugs/Memory Management/ReturnStackAllocatedMemory/test.cpp @@ -252,6 +252,6 @@ void* test_strndupa(const char* s, size_t size) { int* f_rec(int *p, bool b) { int x; - int* px = f_rec(&x, b); // GOOD [FALSE POSITIVE] + int* px = f_rec(&x, b); // GOOD return p; } From e299cccb6e7133c05bbb1e6c0fcc2f55151bd766 Mon Sep 17 00:00:00 2001 From: Jeroen Ketema Date: Mon, 16 Feb 2026 19:09:30 +0100 Subject: [PATCH 5/7] C++: Simplify test --- .../Memory Management/ReturnStackAllocatedMemory/test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/ql/test/query-tests/Likely Bugs/Memory Management/ReturnStackAllocatedMemory/test.cpp b/cpp/ql/test/query-tests/Likely Bugs/Memory Management/ReturnStackAllocatedMemory/test.cpp index abde10eb6e75..07e3520fa814 100644 --- a/cpp/ql/test/query-tests/Likely Bugs/Memory Management/ReturnStackAllocatedMemory/test.cpp +++ b/cpp/ql/test/query-tests/Likely Bugs/Memory Management/ReturnStackAllocatedMemory/test.cpp @@ -250,8 +250,8 @@ void* test_strndupa(const char* s, size_t size) { return s2; // BAD } -int* f_rec(int *p, bool b) { +int* f_rec(int *p) { int x; - int* px = f_rec(&x, b); // GOOD + int* px = f_rec(&x); // GOOD return p; } From 31895c04f80e56b832cba8f39359a4a56a0d45f4 Mon Sep 17 00:00:00 2001 From: Jeroen Ketema Date: Tue, 17 Feb 2026 09:06:36 +0100 Subject: [PATCH 6/7] C++: `MustFlow` minor clean up --- .../semmle/code/cpp/ir/dataflow/MustFlow.qll | 237 +++++++++--------- 1 file changed, 118 insertions(+), 119 deletions(-) diff --git a/cpp/ql/lib/semmle/code/cpp/ir/dataflow/MustFlow.qll b/cpp/ql/lib/semmle/code/cpp/ir/dataflow/MustFlow.qll index a8adb16849c8..2b61190fb71e 100644 --- a/cpp/ql/lib/semmle/code/cpp/ir/dataflow/MustFlow.qll +++ b/cpp/ql/lib/semmle/code/cpp/ir/dataflow/MustFlow.qll @@ -89,10 +89,10 @@ module MustFlow { /** Holds if `nodeFrom` flows to `nodeTo`. */ private predicate step(Instruction nodeFrom, Instruction nodeTo) { - Cached::localStep(pragma[only_bind_into](nodeFrom), pragma[only_bind_into](nodeTo)) + Cached::localStep(nodeFrom, nodeTo) or allowInterproceduralFlow() and - Cached::flowThroughCallable(pragma[only_bind_into](nodeFrom), pragma[only_bind_into](nodeTo)) + Cached::flowThroughCallable(nodeFrom, nodeTo) or isAdditionalFlowStep(nodeFrom.getAUse(), nodeTo) } @@ -145,134 +145,133 @@ module MustFlow { } } } +} - cached - private module Cached { - /** Holds if `p` is the `n`'th parameter of the non-virtual function `f`. */ - private predicate parameterOf(Parameter p, Function f, int n) { - not f.isVirtual() and f.getParameter(n) = p - } +cached +private module Cached { + /** Holds if `p` is the `n`'th parameter of the non-virtual function `f`. */ + private predicate parameterOf(Parameter p, Function f, int n) { + not f.isVirtual() and f.getParameter(n) = p + } - /** - * Holds if `instr` is the `n`'th argument to a call to the non-virtual function `f`, and - * `init` is the corresponding initialization instruction that receives the value of `instr` in `f`. - */ - private predicate flowIntoParameter( - Function f, int n, CallInstruction call, Instruction instr, - InitializeParameterInstruction init - ) { - not f.isVirtual() and - call.getPositionalArgument(n) = instr and - f = call.getStaticCallTarget() and - isEnclosingNonVirtualFunctionInitializeParameter(init, f) and - init.getParameter().getIndex() = pragma[only_bind_into](pragma[only_bind_out](n)) - } + /** + * Holds if `instr` is the `n`'th argument to a call to the non-virtual function `f`, and + * `init` is the corresponding initialization instruction that receives the value of `instr` in `f`. + */ + private predicate flowIntoParameter( + Function f, int n, CallInstruction call, Instruction instr, InitializeParameterInstruction init + ) { + not f.isVirtual() and + call.getPositionalArgument(n) = instr and + f = call.getStaticCallTarget() and + isEnclosingNonVirtualFunctionInitializeParameter(init, f) and + init.getParameter().getIndex() = pragma[only_bind_into](pragma[only_bind_out](n)) + } - /** - * Holds if `instr` is an argument to a call to the function `f`, and `init` is the - * corresponding initialization instruction that receives the value of `instr` in `f`. - */ - pragma[noinline] - private predicate isPositionalArgumentInitParam( - CallInstruction call, Instruction instr, InitializeParameterInstruction init, Function f - ) { - exists(int n | - parameterOf(_, f, n) and - flowIntoParameter(f, pragma[only_bind_into](pragma[only_bind_out](n)), call, instr, init) - ) - } + /** + * Holds if `instr` is an argument to a call to the function `f`, and `init` is the + * corresponding initialization instruction that receives the value of `instr` in `f`. + */ + pragma[noinline] + private predicate isPositionalArgumentInitParam( + CallInstruction call, Instruction instr, InitializeParameterInstruction init, Function f + ) { + exists(int n | + parameterOf(_, f, n) and + flowIntoParameter(f, pragma[only_bind_into](pragma[only_bind_out](n)), call, instr, init) + ) + } - /** - * Holds if `instr` is the qualifier to a call to the non-virtual function `f`, and - * `init` is the corresponding initialization instruction that receives the value of - * `instr` in `f`. - */ - pragma[noinline] - private predicate isThisArgumentInitParam( - CallInstruction call, Instruction instr, InitializeParameterInstruction init, Function f - ) { - not f.isVirtual() and - call.getStaticCallTarget() = f and - isEnclosingNonVirtualFunctionInitializeParameter(init, f) and - call.getThisArgument() = instr and - init.getIRVariable() instanceof IRThisVariable - } + /** + * Holds if `instr` is the qualifier to a call to the non-virtual function `f`, and + * `init` is the corresponding initialization instruction that receives the value of + * `instr` in `f`. + */ + pragma[noinline] + private predicate isThisArgumentInitParam( + CallInstruction call, Instruction instr, InitializeParameterInstruction init, Function f + ) { + not f.isVirtual() and + call.getStaticCallTarget() = f and + isEnclosingNonVirtualFunctionInitializeParameter(init, f) and + call.getThisArgument() = instr and + init.getIRVariable() instanceof IRThisVariable + } - /** Holds if `f` is the enclosing non-virtual function of `init`. */ - private predicate isEnclosingNonVirtualFunctionInitializeParameter( - InitializeParameterInstruction init, Function f - ) { - not f.isVirtual() and - init.getEnclosingFunction() = f - } + /** Holds if `f` is the enclosing non-virtual function of `init`. */ + private predicate isEnclosingNonVirtualFunctionInitializeParameter( + InitializeParameterInstruction init, Function f + ) { + not f.isVirtual() and + init.getEnclosingFunction() = f + } - /** Holds if `f` is the enclosing non-virtual function of `init`. */ - private predicate isEnclosingNonVirtualFunctionInitializeIndirection( - InitializeIndirectionInstruction init, Function f - ) { - not f.isVirtual() and - init.getEnclosingFunction() = f - } + /** Holds if `f` is the enclosing non-virtual function of `init`. */ + private predicate isEnclosingNonVirtualFunctionInitializeIndirection( + InitializeIndirectionInstruction init, Function f + ) { + not f.isVirtual() and + init.getEnclosingFunction() = f + } - /** - * Holds if `argument` is an argument (or argument indirection) to a call, and - * `parameter` is the corresponding initialization instruction in the call target. - */ - cached - predicate flowThroughCallable(Instruction argument, Instruction parameter) { - // Flow from an argument to a parameter - exists(CallInstruction call, InitializeParameterInstruction init | init = parameter | - isPositionalArgumentInitParam(call, argument, init, call.getStaticCallTarget()) - or - isThisArgumentInitParam(call, argument, init, call.getStaticCallTarget()) - ) + /** + * Holds if `argument` is an argument (or argument indirection) to a call, and + * `parameter` is the corresponding initialization instruction in the call target. + */ + cached + predicate flowThroughCallable(Instruction argument, Instruction parameter) { + // Flow from an argument to a parameter + exists(CallInstruction call, InitializeParameterInstruction init | init = parameter | + isPositionalArgumentInitParam(call, argument, init, call.getStaticCallTarget()) or - // Flow from argument indirection to parameter indirection - exists( - CallInstruction call, ReadSideEffectInstruction read, InitializeIndirectionInstruction init - | - init = parameter and - read.getPrimaryInstruction() = call and - isEnclosingNonVirtualFunctionInitializeIndirection(init, call.getStaticCallTarget()) - | - exists(int n | - read.getSideEffectOperand().getAnyDef() = argument and - read.getIndex() = pragma[only_bind_into](n) and - init.getParameter().getIndex() = pragma[only_bind_into](n) - ) - or - call.getThisArgument() = argument and - init.getIRVariable() instanceof IRThisVariable + isThisArgumentInitParam(call, argument, init, call.getStaticCallTarget()) + ) + or + // Flow from argument indirection to parameter indirection + exists( + CallInstruction call, ReadSideEffectInstruction read, InitializeIndirectionInstruction init + | + init = parameter and + read.getPrimaryInstruction() = call and + isEnclosingNonVirtualFunctionInitializeIndirection(init, call.getStaticCallTarget()) + | + exists(int n | + read.getSideEffectOperand().getAnyDef() = argument and + read.getIndex() = pragma[only_bind_into](n) and + init.getParameter().getIndex() = pragma[only_bind_into](n) ) - } + or + call.getThisArgument() = argument and + init.getIRVariable() instanceof IRThisVariable + ) + } - private predicate instructionToOperandStep(Instruction instr, Operand operand) { - operand.getDef() = instr - } + private predicate instructionToOperandStep(Instruction instr, Operand operand) { + operand.getDef() = instr + } - /** - * Holds if data flows from `operand` to `instr`. - * - * This predicate ignores flow through `PhiInstruction`s to create a 'must flow' relation. - */ - private predicate operandToInstructionStep(Operand operand, Instruction instr) { - instr.(CopyInstruction).getSourceValueOperand() = operand - or - instr.(ConvertInstruction).getUnaryOperand() = operand - or - instr.(CheckedConvertOrNullInstruction).getUnaryOperand() = operand - or - instr.(InheritanceConversionInstruction).getUnaryOperand() = operand - or - instr.(ChiInstruction).getTotalOperand() = operand - } + /** + * Holds if data flows from `operand` to `instr`. + * + * This predicate ignores flow through `PhiInstruction`s to create a 'must flow' relation. + */ + private predicate operandToInstructionStep(Operand operand, Instruction instr) { + instr.(CopyInstruction).getSourceValueOperand() = operand + or + instr.(ConvertInstruction).getUnaryOperand() = operand + or + instr.(CheckedConvertOrNullInstruction).getUnaryOperand() = operand + or + instr.(InheritanceConversionInstruction).getUnaryOperand() = operand + or + instr.(ChiInstruction).getTotalOperand() = operand + } - cached - predicate localStep(Instruction nodeFrom, Instruction nodeTo) { - exists(Operand mid | - instructionToOperandStep(nodeFrom, mid) and - operandToInstructionStep(mid, nodeTo) - ) - } + cached + predicate localStep(Instruction nodeFrom, Instruction nodeTo) { + exists(Operand mid | + instructionToOperandStep(nodeFrom, mid) and + operandToInstructionStep(mid, nodeTo) + ) } } From 3aa21242cdf08ac14dfc0ef85832734a2251df87 Mon Sep 17 00:00:00 2001 From: Jeroen Ketema Date: Tue, 17 Feb 2026 10:28:29 +0100 Subject: [PATCH 7/7] C++: Add change notes --- cpp/ql/lib/change-notes/2026-02-14-must-flow-fix.md | 4 ++++ cpp/ql/lib/change-notes/2026-02-14-must-flow.md | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 cpp/ql/lib/change-notes/2026-02-14-must-flow-fix.md create mode 100644 cpp/ql/lib/change-notes/2026-02-14-must-flow.md diff --git a/cpp/ql/lib/change-notes/2026-02-14-must-flow-fix.md b/cpp/ql/lib/change-notes/2026-02-14-must-flow-fix.md new file mode 100644 index 000000000000..fc838f51c068 --- /dev/null +++ b/cpp/ql/lib/change-notes/2026-02-14-must-flow-fix.md @@ -0,0 +1,4 @@ +--- +category: fix +--- +* The `allowInterproceduralFlow` predicate of must-flow data flow configurations now correctly handles direct recursion. diff --git a/cpp/ql/lib/change-notes/2026-02-14-must-flow.md b/cpp/ql/lib/change-notes/2026-02-14-must-flow.md new file mode 100644 index 000000000000..3d1afaa6344b --- /dev/null +++ b/cpp/ql/lib/change-notes/2026-02-14-must-flow.md @@ -0,0 +1,4 @@ +--- +category: breaking +--- +* `MustFlow`, the inter-procedural must-flow data flow analysis library, has been re-worked to use parameterized modules. Like in the case of data flow and taint tracking, instead of extending the `MustFlowConfiguration` class, the user should now implement a module with the `MustFlow::ConfigSig` signature, and instantiate the `MustFlow::Global` parameterized module with the implemented module.