diff --git a/cpp/ql/lib/Customizations.qll b/cpp/ql/lib/Customizations.qll
new file mode 100644
index 00000000000..c9d899e07e0
--- /dev/null
+++ b/cpp/ql/lib/Customizations.qll
@@ -0,0 +1,11 @@
+/**
+ * Contains customizations to the standard library.
+ *
+ * This module is imported by `cpp.qll`, so any customizations defined here automatically
+ * apply to all queries.
+ *
+ * Typical examples of customizations include adding new subclasses of abstract classes such as
+ * the `RemoteFlowSource` class to model frameworks that are not covered by the standard library.
+ */
+
+import cpp
diff --git a/cpp/ql/lib/change-notes/2025-08-19-virtual-dispatch.md b/cpp/ql/lib/change-notes/2025-08-19-virtual-dispatch.md
new file mode 100644
index 00000000000..4342bb7f62d
--- /dev/null
+++ b/cpp/ql/lib/change-notes/2025-08-19-virtual-dispatch.md
@@ -0,0 +1,4 @@
+---
+category: minorAnalysis
+---
+* The new dataflow/taint-tracking library (`semmle.code.cpp.dataflow.new.DataFlow` and `semmle.code.cpp.dataflow.new.TaintTracking`) now resolves virtual function calls more precisely. This results in fewer false positives when running dataflow/taint-tracking queries on C++ projects.
\ No newline at end of file
diff --git a/cpp/ql/lib/cpp.qll b/cpp/ql/lib/cpp.qll
index ccd32c368e4..c8afac1c7ae 100644
--- a/cpp/ql/lib/cpp.qll
+++ b/cpp/ql/lib/cpp.qll
@@ -13,6 +13,7 @@
* https://github.com/cplusplus/draft/raw/master/papers/n4140.pdf
*/
+import Customizations
import semmle.code.cpp.File
import semmle.code.cpp.Linkage
import semmle.code.cpp.Location
diff --git a/cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/DataFlowDispatch.qll b/cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/DataFlowDispatch.qll
index b5e899bf0aa..0d63558c956 100644
--- a/cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/DataFlowDispatch.qll
+++ b/cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/DataFlowDispatch.qll
@@ -1,218 +1,21 @@
private import cpp
private import semmle.code.cpp.ir.IR
-private import DataFlowPrivate
+private import semmle.code.cpp.ir.dataflow.DataFlow
+private import DataFlowPrivate as DataFlowPrivate
private import DataFlowUtil
private import DataFlowImplCommon as DataFlowImplCommon
+private import codeql.typetracking.TypeTracking
+private import SsaImpl as SsaImpl
/**
- * Gets a function that might be called by `call`.
- *
- * This predicate does not take additional call targets
- * from `AdditionalCallTarget` into account.
+ * Holds if `f` has name `qualifiedName` and `nparams` parameter count. This is
+ * an approximation of its signature for the purpose of matching functions that
+ * might be the same across link targets.
*/
-cached
-DataFlowCallable defaultViableCallable(DataFlowCall call) {
- DataFlowImplCommon::forceCachingInSameStage() and
- result = call.getStaticCallTarget()
- or
- // If the target of the call does not have a body in the snapshot, it might
- // be because the target is just a header declaration, and the real target
- // will be determined at run time when the caller and callee are linked
- // together by the operating system's dynamic linker. In case a _unique_
- // function with the right signature is present in the database, we return
- // that as a potential callee.
- exists(string qualifiedName, int nparams |
- callSignatureWithoutBody(qualifiedName, nparams, call.asCallInstruction()) and
- functionSignatureWithBody(qualifiedName, nparams, result.getUnderlyingCallable()) and
- strictcount(Function other | functionSignatureWithBody(qualifiedName, nparams, other)) = 1
- )
- or
- // Virtual dispatch
- result.asSourceCallable() = call.(VirtualDispatch::DataSensitiveCall).resolve()
-}
-
-/**
- * Gets a function that might be called by `call`.
- */
-cached
-DataFlowCallable viableCallable(DataFlowCall call) {
- result = defaultViableCallable(call)
- or
- // Additional call targets
- result.getUnderlyingCallable() =
- any(AdditionalCallTarget additional)
- .viableTarget(call.asCallInstruction().getUnconvertedResultExpression())
-}
-
-/**
- * Provides virtual dispatch support compatible with the original
- * implementation of `semmle.code.cpp.security.TaintTracking`.
- */
-private module VirtualDispatch {
- /** A call that may dispatch differently depending on the qualifier value. */
- abstract class DataSensitiveCall extends DataFlowCall {
- /**
- * Gets the node whose value determines the target of this call. This node
- * could be the qualifier of a virtual dispatch or the function-pointer
- * expression in a call to a function pointer. What they have in common is
- * that we need to find out which data flows there, and then it's up to the
- * `resolve` predicate to stitch that information together and resolve the
- * call.
- */
- abstract Node getDispatchValue();
-
- /** Gets a candidate target for this call. */
- abstract Function resolve();
-
- /**
- * Whether `src` can flow to this call.
- *
- * Searches backwards from `getDispatchValue()` to `src`. The `allowFromArg`
- * parameter is true when the search is allowed to continue backwards into
- * a parameter; non-recursive callers should pass `_` for `allowFromArg`.
- */
- predicate flowsFrom(Node src, boolean allowFromArg) {
- src = this.getDispatchValue() and allowFromArg = true
- or
- exists(Node other, boolean allowOtherFromArg | this.flowsFrom(other, allowOtherFromArg) |
- // Call argument
- exists(DataFlowCall call, Position i |
- other.(ParameterNode).isParameterOf(pragma[only_bind_into](call).getStaticCallTarget(), i) and
- src.(ArgumentNode).argumentOf(call, pragma[only_bind_into](pragma[only_bind_out](i)))
- ) and
- allowOtherFromArg = true and
- allowFromArg = true
- or
- // Call return
- exists(DataFlowCall call, ReturnKind returnKind |
- other = getAnOutNode(call, returnKind) and
- returnNodeWithKindAndEnclosingCallable(src, returnKind, call.getStaticCallTarget())
- ) and
- allowFromArg = false
- or
- // Local flow
- localFlowStep(src, other) and
- allowFromArg = allowOtherFromArg
- or
- // Flow from global variable to load.
- exists(LoadInstruction load, GlobalOrNamespaceVariable var |
- var = src.asVariable() and
- other.asInstruction() = load and
- addressOfGlobal(load.getSourceAddress(), var) and
- // The `allowFromArg` concept doesn't play a role when `src` is a
- // global variable, so we just set it to a single arbitrary value for
- // performance.
- allowFromArg = true
- )
- or
- // Flow from store to global variable.
- exists(StoreInstruction store, GlobalOrNamespaceVariable var |
- var = other.asVariable() and
- store = src.asInstruction() and
- storeIntoGlobal(store, var) and
- // Setting `allowFromArg` to `true` like in the base case means we
- // treat a store to a global variable like the dispatch itself: flow
- // may come from anywhere.
- allowFromArg = true
- )
- )
- }
- }
-
- pragma[noinline]
- private predicate storeIntoGlobal(StoreInstruction store, GlobalOrNamespaceVariable var) {
- addressOfGlobal(store.getDestinationAddress(), var)
- }
-
- /** Holds if `addressInstr` is an instruction that produces the address of `var`. */
- private predicate addressOfGlobal(Instruction addressInstr, GlobalOrNamespaceVariable var) {
- // Access directly to the global variable
- addressInstr.(VariableAddressInstruction).getAstVariable() = var
- or
- // Access to a field on a global union
- exists(FieldAddressInstruction fa |
- fa = addressInstr and
- fa.getObjectAddress().(VariableAddressInstruction).getAstVariable() = var and
- fa.getField().getDeclaringType() instanceof Union
- )
- }
-
- /**
- * A ReturnNode with its ReturnKind and its enclosing callable.
- *
- * Used to fix a join ordering issue in flowsFrom.
- */
- pragma[noinline]
- private predicate returnNodeWithKindAndEnclosingCallable(
- ReturnNode node, ReturnKind kind, DataFlowCallable callable
- ) {
- node.getKind() = kind and
- node.getFunction() = callable.getUnderlyingCallable()
- }
-
- /** Call through a function pointer. */
- private class DataSensitiveExprCall extends DataSensitiveCall {
- DataSensitiveExprCall() { not exists(this.getStaticCallTarget()) }
-
- override Node getDispatchValue() { result.asOperand() = this.getCallTargetOperand() }
-
- override Function resolve() {
- exists(FunctionInstruction fi |
- this.flowsFrom(instructionNode(fi), _) and
- result = fi.getFunctionSymbol()
- ) and
- (
- this.getNumberOfArguments() <= result.getEffectiveNumberOfParameters() and
- this.getNumberOfArguments() >= result.getEffectiveNumberOfParameters()
- or
- result.isVarargs()
- )
- }
- }
-
- /** Call to a virtual function. */
- private class DataSensitiveOverriddenFunctionCall extends DataSensitiveCall {
- DataSensitiveOverriddenFunctionCall() {
- exists(
- this.getStaticCallTarget()
- .getUnderlyingCallable()
- .(VirtualFunction)
- .getAnOverridingFunction()
- )
- }
-
- override Node getDispatchValue() { result.asInstruction() = this.getArgument(-1) }
-
- override MemberFunction resolve() {
- exists(Class overridingClass |
- this.overrideMayAffectCall(overridingClass, result) and
- this.hasFlowFromCastFrom(overridingClass)
- )
- }
-
- /**
- * Holds if `this` is a virtual function call whose static target is
- * overridden by `overridingFunction` in `overridingClass`.
- */
- pragma[noinline]
- private predicate overrideMayAffectCall(Class overridingClass, MemberFunction overridingFunction) {
- overridingFunction.getAnOverriddenFunction+() =
- this.getStaticCallTarget().getUnderlyingCallable().(VirtualFunction) and
- overridingFunction.getDeclaringType() = overridingClass
- }
-
- /**
- * Holds if the qualifier of `this` has flow from an upcast from
- * `derivedClass`.
- */
- pragma[noinline]
- private predicate hasFlowFromCastFrom(Class derivedClass) {
- exists(ConvertToBaseInstruction toBase |
- this.flowsFrom(instructionNode(toBase), _) and
- derivedClass = toBase.getDerivedClass()
- )
- }
- }
+private predicate functionSignature(Function f, string qualifiedName, int nparams) {
+ qualifiedName = f.getQualifiedName() and
+ nparams = f.getNumberOfParameters() and
+ not f.isStatic()
}
/**
@@ -238,34 +41,319 @@ private predicate callSignatureWithoutBody(string qualifiedName, int nparams, Ca
}
/**
- * Holds if `f` has name `qualifiedName` and `nparams` parameter count. This is
- * an approximation of its signature for the purpose of matching functions that
- * might be the same across link targets.
+ * Gets a function that might be called by `call`.
+ *
+ * This predicate does not take additional call targets
+ * from `AdditionalCallTarget` into account.
*/
-private predicate functionSignature(Function f, string qualifiedName, int nparams) {
- qualifiedName = f.getQualifiedName() and
- nparams = f.getNumberOfParameters() and
- not f.isStatic()
+cached
+DataFlowPrivate::DataFlowCallable defaultViableCallable(DataFlowPrivate::DataFlowCall call) {
+ result = defaultViableCallableWithoutLambda(call)
+ or
+ result = DataFlowImplCommon::viableCallableLambda(call, _)
+}
+
+private DataFlowPrivate::DataFlowCallable defaultViableCallableWithoutLambda(
+ DataFlowPrivate::DataFlowCall call
+) {
+ DataFlowImplCommon::forceCachingInSameStage() and
+ result = call.getStaticCallTarget()
+ or
+ // If the target of the call does not have a body in the snapshot, it might
+ // be because the target is just a header declaration, and the real target
+ // will be determined at run time when the caller and callee are linked
+ // together by the operating system's dynamic linker. In case a _unique_
+ // function with the right signature is present in the database, we return
+ // that as a potential callee.
+ exists(string qualifiedName, int nparams |
+ callSignatureWithoutBody(qualifiedName, nparams, call.asCallInstruction()) and
+ functionSignatureWithBody(qualifiedName, nparams, result.getUnderlyingCallable()) and
+ strictcount(Function other | functionSignatureWithBody(qualifiedName, nparams, other)) = 1
+ )
+}
+
+/**
+ * Gets a function that might be called by `call`.
+ */
+private DataFlowPrivate::DataFlowCallable nonVirtualDispatch(DataFlowPrivate::DataFlowCall call) {
+ result = defaultViableCallableWithoutLambda(call)
+ or
+ // Additional call targets
+ result.getUnderlyingCallable() =
+ any(AdditionalCallTarget additional)
+ .viableTarget(call.asCallInstruction().getUnconvertedResultExpression())
+}
+
+private class RelevantNode extends Node {
+ RelevantNode() { this.getType().stripType() instanceof Class }
+}
+
+private signature DataFlowPrivate::DataFlowCallable methodDispatchSig(
+ DataFlowPrivate::DataFlowCall c
+);
+
+private predicate ignoreConstructor(Expr e) {
+ e instanceof ConstructorDirectInit or
+ e instanceof ConstructorVirtualInit or
+ e instanceof ConstructorDelegationInit or
+ exists(ConstructorFieldInit init | init.getExpr() = e)
+}
+
+/**
+ * Holds if `n` is either:
+ * - the post-update node of a qualifier after a call to a constructor which
+ * constructs an object containing at least one virtual function.
+ * - a node which represents a derived-to-base instruction that converts from `c`.
+ */
+private predicate qualifierSourceImpl(RelevantNode n, Class c) {
+ // Object construction
+ exists(CallInstruction call, ThisArgumentOperand qualifier, Call e |
+ qualifier = call.getThisArgumentOperand() and
+ n.(PostUpdateNode).getPreUpdateNode().asOperand() = qualifier and
+ call.getStaticCallTarget() instanceof Constructor and
+ qualifier.getType().stripType() = c and
+ c.getABaseClass*().getAMemberFunction().isVirtual() and
+ e = call.getUnconvertedResultExpression() and
+ not ignoreConstructor(e)
+ |
+ exists(c.getABaseClass())
+ or
+ exists(c.getADerivedClass())
+ )
+ or
+ // Conversion to a base class
+ exists(ConvertToBaseInstruction convert |
+ // Only keep the most specific cast
+ not convert.getUnary() instanceof ConvertToBaseInstruction and
+ n.asInstruction() = convert and
+ convert.getDerivedClass() = c and
+ c.getABaseClass*().getAMemberFunction().isVirtual()
+ )
+}
+
+private module TrackVirtualDispatch {
+ /**
+ * Gets a possible runtime target of `c` using both static call-target
+ * information, and call-target resolution from `virtualDispatch0`.
+ */
+ private DataFlowPrivate::DataFlowCallable dispatch(DataFlowPrivate::DataFlowCall c) {
+ result = nonVirtualDispatch(c) or
+ result = virtualDispatch0(c)
+ }
+
+ private module TtInput implements TypeTrackingInput {
+ final class Node = RelevantNode;
+
+ class LocalSourceNode extends Node {
+ LocalSourceNode() {
+ this instanceof ParameterNode
+ or
+ this instanceof DataFlowPrivate::OutNode
+ or
+ DataFlowPrivate::readStep(_, _, this)
+ or
+ DataFlowPrivate::storeStep(_, _, this)
+ or
+ DataFlowPrivate::jumpStep(_, this)
+ or
+ qualifierSourceImpl(this, _)
+ }
+ }
+
+ final private class ContentSetFinal = ContentSet;
+
+ class Content extends ContentSetFinal {
+ Content() {
+ exists(DataFlow::Content c |
+ this.isSingleton(c) and
+ c.getIndirectionIndex() = 1
+ )
+ }
+ }
+
+ class ContentFilter extends Content {
+ Content getAMatchingContent() { result = this }
+ }
+
+ predicate compatibleContents(Content storeContents, Content loadContents) {
+ storeContents = loadContents
+ }
+
+ predicate simpleLocalSmallStep(Node nodeFrom, Node nodeTo) {
+ nodeFrom.getFunction() instanceof Function and
+ simpleLocalFlowStep(nodeFrom, nodeTo, _)
+ }
+
+ predicate levelStepNoCall(Node n1, LocalSourceNode n2) { none() }
+
+ predicate levelStepCall(Node n1, LocalSourceNode n2) { none() }
+
+ predicate storeStep(Node n1, Node n2, Content f) { DataFlowPrivate::storeStep(n1, f, n2) }
+
+ predicate callStep(Node n1, LocalSourceNode n2) {
+ exists(DataFlowPrivate::DataFlowCall call, DataFlowPrivate::Position pos |
+ n1.(DataFlowPrivate::ArgumentNode).argumentOf(call, pos) and
+ n2.(ParameterNode).isParameterOf(dispatch(call), pos)
+ )
+ }
+
+ predicate returnStep(Node n1, LocalSourceNode n2) {
+ exists(DataFlowPrivate::DataFlowCallable callable, DataFlowPrivate::DataFlowCall call |
+ n1.(DataFlowPrivate::ReturnNode).getEnclosingCallable() = callable and
+ callable = dispatch(call) and
+ n2 = DataFlowPrivate::getAnOutNode(call, n1.(DataFlowPrivate::ReturnNode).getKind())
+ )
+ }
+
+ predicate loadStep(Node n1, LocalSourceNode n2, Content f) {
+ DataFlowPrivate::readStep(n1, f, n2)
+ }
+
+ predicate loadStoreStep(Node nodeFrom, Node nodeTo, Content f1, Content f2) { none() }
+
+ predicate withContentStep(Node nodeFrom, LocalSourceNode nodeTo, ContentFilter f) { none() }
+
+ predicate withoutContentStep(Node nodeFrom, LocalSourceNode nodeTo, ContentFilter f) { none() }
+
+ predicate jumpStep(Node n1, LocalSourceNode n2) { DataFlowPrivate::jumpStep(n1, n2) }
+
+ predicate hasFeatureBacktrackStoreTarget() { none() }
+ }
+
+ private predicate qualifierSource(RelevantNode n) { qualifierSourceImpl(n, _) }
+
+ /**
+ * Holds if `n` is the qualifier of `call` which targets the virtual member
+ * function `mf`.
+ */
+ private predicate qualifierOfVirtualCallImpl(
+ RelevantNode n, CallInstruction call, MemberFunction mf
+ ) {
+ n.asOperand() = call.getThisArgumentOperand() and
+ call.getStaticCallTarget() = mf and
+ mf.isVirtual()
+ }
+
+ private predicate qualifierOfVirtualCall(RelevantNode n) { qualifierOfVirtualCallImpl(n, _, _) }
+
+ private import TypeTracking::TypeTrack::Graph
+
+ private predicate edgePlus(PathNode n1, PathNode n2) = fastTC(edges/2)(n1, n2)
+
+ /**
+ * Gets the most specific implementation of `mf` that may be called when the
+ * qualifier has runtime type `c`.
+ */
+ private MemberFunction mostSpecific(MemberFunction mf, Class c) {
+ qualifierOfVirtualCallImpl(_, _, mf) and
+ mf.getAnOverridingFunction*() = result and
+ (
+ result.getDeclaringType() = c
+ or
+ not c.getAMemberFunction().getAnOverriddenFunction*() = mf and
+ result = mostSpecific(mf, c.getABaseClass())
+ )
+ }
+
+ /**
+ * Gets a possible pair of end-points `(p1, p2)` where:
+ * - `p1` is a derived-to-base conversion that converts from some
+ * class `derived`, and
+ * - `p2` is the qualifier of a call to a virtual function that may
+ * target `callable`, and
+ * - `callable` is the most specific implementation that may be called when
+ * the qualifier has type `derived`.
+ */
+ private predicate pairCand(
+ PathNode p1, PathNode p2, DataFlowPrivate::DataFlowCallable callable,
+ DataFlowPrivate::DataFlowCall call
+ ) {
+ exists(Class derived, MemberFunction mf |
+ qualifierSourceImpl(p1.getNode(), derived) and
+ qualifierOfVirtualCallImpl(p2.getNode(), call.asCallInstruction(), mf) and
+ p1.isSource() and
+ p2.isSink() and
+ callable.asSourceCallable() = mostSpecific(mf, derived)
+ )
+ }
+
+ /** Gets a possible run-time target of `call`. */
+ DataFlowPrivate::DataFlowCallable virtualDispatch(DataFlowPrivate::DataFlowCall call) {
+ exists(PathNode p1, PathNode p2 | p1 = p2 or edgePlus(p1, p2) | pairCand(p1, p2, result, call))
+ }
+}
+
+private DataFlowPrivate::DataFlowCallable noDisp(DataFlowPrivate::DataFlowCall call) { none() }
+
+pragma[nomagic]
+private DataFlowPrivate::DataFlowCallable d1(DataFlowPrivate::DataFlowCall call) {
+ result = TrackVirtualDispatch::virtualDispatch(call)
+}
+
+pragma[nomagic]
+private DataFlowPrivate::DataFlowCallable d2(DataFlowPrivate::DataFlowCall call) {
+ result = TrackVirtualDispatch::virtualDispatch(call)
+}
+
+pragma[nomagic]
+private DataFlowPrivate::DataFlowCallable d3(DataFlowPrivate::DataFlowCall call) {
+ result = TrackVirtualDispatch::virtualDispatch(call)
+}
+
+pragma[nomagic]
+private DataFlowPrivate::DataFlowCallable d4(DataFlowPrivate::DataFlowCall call) {
+ result = TrackVirtualDispatch::virtualDispatch(call)
+}
+
+pragma[nomagic]
+private DataFlowPrivate::DataFlowCallable d5(DataFlowPrivate::DataFlowCall call) {
+ result = TrackVirtualDispatch::virtualDispatch(call)
+}
+
+pragma[nomagic]
+private DataFlowPrivate::DataFlowCallable d6(DataFlowPrivate::DataFlowCall call) {
+ result = TrackVirtualDispatch::virtualDispatch(call)
+}
+
+/** Gets a function that might be called by `call`. */
+cached
+DataFlowPrivate::DataFlowCallable viableCallable(DataFlowPrivate::DataFlowCall call) {
+ not exists(d6(call)) and
+ result = nonVirtualDispatch(call)
+ or
+ result = d6(call)
}
/**
* Holds if the set of viable implementations that can be called by `call`
* might be improved by knowing the call context.
*/
-predicate mayBenefitFromCallContext(DataFlowCall call) { mayBenefitFromCallContext(call, _, _) }
+predicate mayBenefitFromCallContext(DataFlowPrivate::DataFlowCall call) {
+ mayBenefitFromCallContext(call, _, _)
+}
+
+private predicate localLambdaFlowStep(Node nodeFrom, Node nodeTo) {
+ localFlowStep(nodeFrom, nodeTo)
+ or
+ DataFlowPrivate::additionalLambdaFlowStep(nodeFrom, nodeTo, _)
+}
/**
* Holds if `call` is a call through a function pointer, and the pointer
* value is given as the `arg`'th argument to `f`.
*/
private predicate mayBenefitFromCallContext(
- VirtualDispatch::DataSensitiveCall call, DataFlowCallable f, int arg
+ DataFlowPrivate::DataFlowCall call, DataFlowPrivate::DataFlowCallable f, int arg
) {
f = pragma[only_bind_out](call).getEnclosingCallable() and
exists(InitializeParameterInstruction init |
- not exists(call.getStaticCallTarget()) and
+ not exists(call.getStaticCallTarget())
+ or
+ exists(call.getStaticCallSourceTarget().(VirtualFunction).getAnOverridingFunction())
+ |
init.getEnclosingFunction() = f.getUnderlyingCallable() and
- call.flowsFrom(instructionNode(init), _) and
+ localLambdaFlowStep+(instructionNode(init),
+ operandNode(call.asCallInstruction().getCallTargetOperand())) and
init.getParameter().getIndex() = arg
)
}
@@ -274,9 +362,11 @@ private predicate mayBenefitFromCallContext(
* Gets a viable dispatch target of `call` in the context `ctx`. This is
* restricted to those `call`s for which a context might make a difference.
*/
-DataFlowCallable viableImplInCallContext(DataFlowCall call, DataFlowCall ctx) {
+DataFlowPrivate::DataFlowCallable viableImplInCallContext(
+ DataFlowPrivate::DataFlowCall call, DataFlowPrivate::DataFlowCall ctx
+) {
result = viableCallable(call) and
- exists(int i, DataFlowCallable f |
+ exists(int i, DataFlowPrivate::DataFlowCallable f |
mayBenefitFromCallContext(pragma[only_bind_into](call), f, i) and
f = ctx.getStaticCallTarget() and
result.asSourceCallable() =
@@ -286,4 +376,8 @@ DataFlowCallable viableImplInCallContext(DataFlowCall call, DataFlowCall ctx) {
/** Holds if arguments at position `apos` match parameters at position `ppos`. */
pragma[inline]
-predicate parameterMatch(ParameterPosition ppos, ArgumentPosition apos) { ppos = apos }
+predicate parameterMatch(
+ DataFlowPrivate::ParameterPosition ppos, DataFlowPrivate::ArgumentPosition apos
+) {
+ ppos = apos
+}
diff --git a/cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/DataFlowPrivate.qll b/cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/DataFlowPrivate.qll
index a03042a77ff..3aa8994a449 100644
--- a/cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/DataFlowPrivate.qll
+++ b/cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/DataFlowPrivate.qll
@@ -1492,7 +1492,14 @@ predicate lambdaCall(DataFlowCall call, LambdaCallKind kind, Node receiver) {
}
/** Extra data-flow steps needed for lambda flow analysis. */
-predicate additionalLambdaFlowStep(Node nodeFrom, Node nodeTo, boolean preservesValue) { none() }
+predicate additionalLambdaFlowStep(Node nodeFrom, Node nodeTo, boolean preservesValue) {
+ preservesValue = false and
+ exists(ContentSet cs | cs.isSingleton(any(UnionContent uc)) |
+ storeStep(nodeFrom, cs, nodeTo)
+ or
+ readStep(nodeFrom, cs, nodeTo)
+ )
+}
predicate knownSourceModel(Node source, string model) { External::sourceNode(source, _, model) }
diff --git a/cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/DataFlowUtil.qll b/cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/DataFlowUtil.qll
index a0a99711552..ef4051171af 100644
--- a/cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/DataFlowUtil.qll
+++ b/cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/DataFlowUtil.qll
@@ -795,7 +795,7 @@ class FinalGlobalValue extends Node, TFinalGlobalValue {
override DataFlowType getType() {
exists(int indirectionIndex |
indirectionIndex = globalUse.getIndirectionIndex() and
- result = getTypeImpl(globalUse.getUnderlyingType(), indirectionIndex - 1)
+ result = getTypeImpl(globalUse.getUnderlyingType(), indirectionIndex)
)
}
diff --git a/cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/SsaImpl.qll b/cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/SsaImpl.qll
index b9f320e57b2..1cf45439d8b 100644
--- a/cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/SsaImpl.qll
+++ b/cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/SsaImpl.qll
@@ -756,9 +756,9 @@ private predicate modeledFlowBarrier(Node n) {
partialFlowFunc = call.getStaticCallTarget() and
not partialFlowFunc.isPartialWrite(output)
|
- call.getStaticCallTarget().(DataFlow::DataFlowFunction).hasDataFlow(_, output)
+ partialFlowFunc.(DataFlow::DataFlowFunction).hasDataFlow(_, output)
or
- call.getStaticCallTarget().(Taint::TaintFunction).hasTaintFlow(_, output)
+ partialFlowFunc.(Taint::TaintFunction).hasTaintFlow(_, output)
)
or
exists(Operand operand, Instruction instr, Node n0, int indirectionIndex |
diff --git a/cpp/ql/lib/semmle/code/cpp/models/interfaces/FormattingFunction.qll b/cpp/ql/lib/semmle/code/cpp/models/interfaces/FormattingFunction.qll
index ce65a65319a..757db13fe8c 100644
--- a/cpp/ql/lib/semmle/code/cpp/models/interfaces/FormattingFunction.qll
+++ b/cpp/ql/lib/semmle/code/cpp/models/interfaces/FormattingFunction.qll
@@ -170,6 +170,16 @@ abstract class FormattingFunction extends ArrayFunction, TaintFunction {
output.isParameterDeref(this.getOutputParameterIndex(_))
)
}
+
+ final override predicate isPartialWrite(FunctionOutput output) {
+ exists(int outputParameterIndex |
+ output.isParameterDeref(outputParameterIndex) and
+ // We require the output to be a stream since that definitely means that
+ // it's a partial write. If it's not a stream then it will most likely
+ // fill the whole buffer.
+ outputParameterIndex = this.getOutputParameterIndex(true)
+ )
+ }
}
/**
diff --git a/cpp/ql/src/change-notes/2025-08-20-add-customizations.md b/cpp/ql/src/change-notes/2025-08-20-add-customizations.md
new file mode 100644
index 00000000000..3a01298283c
--- /dev/null
+++ b/cpp/ql/src/change-notes/2025-08-20-add-customizations.md
@@ -0,0 +1,4 @@
+---
+category: fix
+---
+* Fixed an inconsistency across languages where most have a `Customizations.qll` file for adding customizations, but not all did.
\ No newline at end of file
diff --git a/cpp/ql/test/library-tests/dataflow/dataflow-tests/dispatch.cpp b/cpp/ql/test/library-tests/dataflow/dataflow-tests/dispatch.cpp
index 105212ccca6..63528d712c0 100644
--- a/cpp/ql/test/library-tests/dataflow/dataflow-tests/dispatch.cpp
+++ b/cpp/ql/test/library-tests/dataflow/dataflow-tests/dispatch.cpp
@@ -8,7 +8,7 @@ struct Top {
virtual void isSink(int x) { }
virtual int notSource1() { return source(); }
virtual int notSource2() { return source(); }
- virtual void notSink(int x) { sink(x); } // $ SPURIOUS: ast,ir=37:19 ast,ir=45:18
+ virtual void notSink(int x) { sink(x); } // $ SPURIOUS: ast=37:19 ast=45:18
};
// This class has the correct behavior for just the functions ending in 2.
@@ -32,16 +32,16 @@ void VirtualDispatch(Bottom *bottomPtr, Bottom &bottomRef) { // $ ast-def=bottom
sink(topPtr->isSource2()); // $ ir MISSING: ast
topPtr->isSink(source()); // causing a MISSING for ast
- sink(topPtr->notSource1()); // $ SPURIOUS: ast,ir
- sink(topPtr->notSource2()); // $ SPURIOUS: ast,ir
+ sink(topPtr->notSource1()); // $ SPURIOUS: ast
+ sink(topPtr->notSource2()); // $ SPURIOUS: ast
topPtr->notSink(source()); // causing SPURIOUS for ast,ir
sink(topRef.isSource1()); // $ ir MISSING: ast
sink(topRef.isSource2()); // $ ir MISSING: ast
topRef.isSink(source()); // causing a MISSING for ast
- sink(topRef.notSource1()); // $ SPURIOUS: ast,ir
- sink(topRef.notSource2()); // $ SPURIOUS: ast,ir
+ sink(topRef.notSource1()); // $ SPURIOUS: ast
+ sink(topRef.notSource2()); // $ SPURIOUS: ast
topRef.notSink(source()); // causing SPURIOUS for ast,ir
}
@@ -126,8 +126,8 @@ namespace virtual_inheritance {
// get flow from a `Middle` value to the call qualifier.
Top *topPtr = bottomPtr, &topRef = bottomRef;
- sink(topPtr->isSource()); // $ MISSING: ast,ir
- sink(topRef.isSource()); // $ MISSING: ast,ir
+ sink(topPtr->isSource()); // $ ir MISSING: ast
+ sink(topRef.isSource()); // $ ir MISSING: ast
}
}
diff --git a/cpp/ql/test/library-tests/dataflow/dataflow-tests/test-source-sink.expected b/cpp/ql/test/library-tests/dataflow/dataflow-tests/test-source-sink.expected
index 6e0b03be9c6..8c009241734 100644
--- a/cpp/ql/test/library-tests/dataflow/dataflow-tests/test-source-sink.expected
+++ b/cpp/ql/test/library-tests/dataflow/dataflow-tests/test-source-sink.expected
@@ -169,10 +169,6 @@ irFlow
| clang.cpp:50:35:50:40 | call to source | clang.cpp:53:17:53:26 | *stackArray |
| clang.cpp:51:19:51:24 | call to source | clang.cpp:53:17:53:26 | *stackArray |
| clang.cpp:57:21:57:28 | call to source | clang.cpp:59:8:59:8 | d |
-| dispatch.cpp:9:37:9:42 | call to source | dispatch.cpp:35:16:35:25 | call to notSource1 |
-| dispatch.cpp:9:37:9:42 | call to source | dispatch.cpp:43:15:43:24 | call to notSource1 |
-| dispatch.cpp:10:37:10:42 | call to source | dispatch.cpp:36:16:36:25 | call to notSource2 |
-| dispatch.cpp:10:37:10:42 | call to source | dispatch.cpp:44:15:44:24 | call to notSource2 |
| dispatch.cpp:16:37:16:42 | call to source | dispatch.cpp:32:16:32:24 | call to isSource2 |
| dispatch.cpp:16:37:16:42 | call to source | dispatch.cpp:40:15:40:23 | call to isSource2 |
| dispatch.cpp:22:37:22:42 | call to source | dispatch.cpp:31:16:31:24 | call to isSource1 |
@@ -180,13 +176,13 @@ irFlow
| dispatch.cpp:22:37:22:42 | call to source | dispatch.cpp:55:22:55:30 | call to isSource1 |
| dispatch.cpp:22:37:22:42 | call to source | dispatch.cpp:58:28:58:36 | call to isSource1 |
| dispatch.cpp:33:18:33:23 | call to source | dispatch.cpp:23:38:23:38 | x |
-| dispatch.cpp:37:19:37:24 | call to source | dispatch.cpp:11:38:11:38 | x |
| dispatch.cpp:41:17:41:22 | call to source | dispatch.cpp:23:38:23:38 | x |
-| dispatch.cpp:45:18:45:23 | call to source | dispatch.cpp:11:38:11:38 | x |
| dispatch.cpp:69:15:69:20 | call to source | dispatch.cpp:23:38:23:38 | x |
| dispatch.cpp:73:14:73:19 | call to source | dispatch.cpp:23:38:23:38 | x |
| dispatch.cpp:81:13:81:18 | call to source | dispatch.cpp:23:38:23:38 | x |
| dispatch.cpp:107:17:107:22 | call to source | dispatch.cpp:96:8:96:8 | x |
+| dispatch.cpp:117:38:117:43 | call to source | dispatch.cpp:129:18:129:25 | call to isSource |
+| dispatch.cpp:117:38:117:43 | call to source | dispatch.cpp:130:17:130:24 | call to isSource |
| dispatch.cpp:140:8:140:13 | call to source | dispatch.cpp:96:8:96:8 | x |
| dispatch.cpp:144:8:144:13 | call to source | dispatch.cpp:96:8:96:8 | x |
| flowOut.cpp:5:16:5:21 | call to source | flowOut.cpp:31:9:31:9 | x |
diff --git a/cpp/ql/test/library-tests/dataflow/dispatch/test.cpp b/cpp/ql/test/library-tests/dataflow/dispatch/test.cpp
new file mode 100644
index 00000000000..f243b76ad14
--- /dev/null
+++ b/cpp/ql/test/library-tests/dataflow/dispatch/test.cpp
@@ -0,0 +1,127 @@
+struct Base {
+ void f();
+ virtual void virtual_f();
+};
+
+struct Derived : Base {
+ void f();
+ void virtual_f();
+};
+
+void test_simple() {
+ Base b;
+ b.f(); // $ target=2
+ b.virtual_f(); // $ target=3
+
+ Derived d;
+ d.f(); // $ target=7
+ d.virtual_f(); // $ target=8
+
+ Base* b_ptr = &d;
+ b_ptr->f(); // $ target=2
+ b_ptr->virtual_f(); // $ target=8
+
+ Base& b_ref = d;
+ b_ref.f(); // $ target=2
+ b_ref.virtual_f(); // $ target=8
+
+ Base* b_null = nullptr;
+ b_null->f(); // $ target=2
+ b_null->virtual_f(); // $ target=3
+
+ Base* base_is_derived = new Derived();
+ base_is_derived->f(); // $ target=2
+ base_is_derived->virtual_f(); // $ target=8
+
+ Base* base_is_base = new Base();
+ base_is_base->f(); // $ target=2
+ base_is_base->virtual_f(); // $ target=3
+
+ Derived* derived_is_derived = new Derived();
+ derived_is_derived->f(); // $ target=7
+ derived_is_derived->virtual_f(); // $ target=8
+
+ Base& b_ref2 = b;
+ b_ref2 = d;
+ b_ref2.f(); // $ target=2
+ b_ref2.virtual_f(); // $ target=3
+}
+
+struct S {
+ Base* b1;
+ Base* b2;
+};
+
+void test_fields() {
+ S s;
+
+ s.b1 = new Base();
+ s.b2 = new Derived();
+
+ s.b1->virtual_f(); // $ target=3
+ s.b2->virtual_f(); // $ target=8
+
+ s.b1 = new Derived();
+ s.b2 = new Base();
+ s.b1->virtual_f(); // $ target=8 SPURIOUS: target=3 // type-tracking has no 'clearsContent' feature and C/C++ doesn't have field-based SSA
+ s.b2->virtual_f(); // $ target=3 SPURIOUS: target=8 // type-tracking has no 'clearsContent' feature and C/C++ doesn't have field-based SSA
+}
+
+Base* getDerived() {
+ return new Derived();
+}
+
+void test_getDerived() {
+ Base* b = getDerived();
+ b->virtual_f(); // $ target=8
+
+ Derived d = *(Derived*)getDerived();
+ d.virtual_f(); // $ target=8
+}
+
+void write_to_arg(Base* b) {
+ *b = Derived();
+}
+
+void write_to_arg_2(Base** b) {
+ Derived* d = new Derived();
+ *b = d;
+}
+
+void test_write_to_arg() {
+ {
+ Base b;
+ write_to_arg(&b);
+ b.virtual_f(); // $ SPURIOUS: target=3 MISSING: target=8 // missing flow through the copy-constructor in write_to_arg
+ }
+
+ {
+ Base* b;
+ write_to_arg_2(&b);
+ b->virtual_f(); // $ target=8
+ }
+}
+
+Base* global_derived;
+
+void set_global_to_derived() {
+ global_derived = new Derived();
+}
+
+void read_global() {
+ global_derived->virtual_f(); // $ target=8
+}
+
+Base* global_base_or_derived;
+
+void set_global_base_or_derived_1() {
+ global_base_or_derived = new Base();
+}
+
+void set_global_base_or_derived_2() {
+ global_base_or_derived = new Derived();
+}
+
+void read_global_base_or_derived() {
+ global_base_or_derived->virtual_f(); // $ target=3 target=8
+}
\ No newline at end of file
diff --git a/cpp/ql/test/library-tests/dataflow/dispatch/test.expected b/cpp/ql/test/library-tests/dataflow/dispatch/test.expected
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/cpp/ql/test/library-tests/dataflow/dispatch/test.ql b/cpp/ql/test/library-tests/dataflow/dispatch/test.ql
new file mode 100644
index 00000000000..de16d6da1ef
--- /dev/null
+++ b/cpp/ql/test/library-tests/dataflow/dispatch/test.ql
@@ -0,0 +1,22 @@
+import cpp
+import utils.test.InlineExpectationsTest
+import semmle.code.cpp.ir.dataflow.internal.DataFlowDispatch
+import semmle.code.cpp.ir.dataflow.internal.DataFlowPrivate
+
+module ResolveDispatchTest implements TestSig {
+ string getARelevantTag() { result = "target" }
+
+ predicate hasActualResult(Location location, string element, string tag, string value) {
+ exists(DataFlowCall call, SourceCallable callable, MemberFunction mf |
+ mf = callable.asSourceCallable() and
+ not mf.isCompilerGenerated() and
+ callable = viableCallable(call) and
+ location = call.getLocation() and
+ element = call.toString() and
+ tag = "target" and
+ value = callable.getLocation().getStartLine().toString()
+ )
+ }
+}
+
+import MakeTest
diff --git a/cpp/ql/test/library-tests/dataflow/taint-tests/localTaint.expected b/cpp/ql/test/library-tests/dataflow/taint-tests/localTaint.expected
index e19f34eb217..76eaebb52cd 100644
--- a/cpp/ql/test/library-tests/dataflow/taint-tests/localTaint.expected
+++ b/cpp/ql/test/library-tests/dataflow/taint-tests/localTaint.expected
@@ -7767,6 +7767,10 @@ WARNING: module 'TaintTracking' has been deprecated and may be removed in future
| taint.cpp:830:20:830:34 | call to indirect_source | taint.cpp:832:23:832:24 | in | |
| taint.cpp:831:15:831:17 | out | taint.cpp:832:18:832:20 | out | |
| taint.cpp:831:15:831:17 | out | taint.cpp:833:8:833:10 | out | |
+| taint.cpp:841:21:841:35 | call to indirect_source | taint.cpp:842:11:842:12 | fp | |
+| taint.cpp:841:21:841:35 | call to indirect_source | taint.cpp:843:16:843:17 | fp | |
+| taint.cpp:842:11:842:12 | ref arg fp | taint.cpp:843:16:843:17 | fp | |
+| taint.cpp:842:15:842:16 | | taint.cpp:842:11:842:12 | ref arg fp | TAINT |
| thread.cpp:10:27:10:27 | s | thread.cpp:10:27:10:27 | s | |
| thread.cpp:10:27:10:27 | s | thread.cpp:11:8:11:8 | s | |
| thread.cpp:14:26:14:26 | s | thread.cpp:15:8:15:8 | s | |
diff --git a/cpp/ql/test/library-tests/dataflow/taint-tests/taint.cpp b/cpp/ql/test/library-tests/dataflow/taint-tests/taint.cpp
index 74bcf26ea7d..0c09665de1c 100644
--- a/cpp/ql/test/library-tests/dataflow/taint-tests/taint.cpp
+++ b/cpp/ql/test/library-tests/dataflow/taint-tests/taint.cpp
@@ -831,4 +831,15 @@ void test_write_to_const_ptr_ptr() {
const char* out;
take_const_ptr(out, in);
sink(out); // $ SPURIOUS: ast
+}
+
+void indirect_sink(FILE *fp);
+int fprintf(FILE *fp, const char *format, ...);
+
+int f7(void)
+{
+ FILE* fp = (FILE*)indirect_source();
+ fprintf(fp, "");
+ indirect_sink(fp); // $ ir MISSING: ast
+ return 0;
}
\ No newline at end of file
diff --git a/cpp/ql/test/library-tests/dataflow/taint-tests/taint.ql b/cpp/ql/test/library-tests/dataflow/taint-tests/taint.ql
index f5f483cdf1b..3bde6ca03f0 100644
--- a/cpp/ql/test/library-tests/dataflow/taint-tests/taint.ql
+++ b/cpp/ql/test/library-tests/dataflow/taint-tests/taint.ql
@@ -117,6 +117,11 @@ module IRTest {
call.getTarget().getName() = "sink" and
[sink.asExpr(), sink.asIndirectExpr()] = call.getAnArgument()
)
+ or
+ exists(FunctionCall call |
+ call.getTarget().getName() = "indirect_sink" and
+ sink.asIndirectExpr() = call.getAnArgument()
+ )
}
predicate isBarrier(DataFlow::Node barrier) {
diff --git a/cpp/ql/test/library-tests/dataflow/taint-tests/test_mad-signatures.expected b/cpp/ql/test/library-tests/dataflow/taint-tests/test_mad-signatures.expected
index 239ed2ec607..f5342ea7694 100644
--- a/cpp/ql/test/library-tests/dataflow/taint-tests/test_mad-signatures.expected
+++ b/cpp/ql/test/library-tests/dataflow/taint-tests/test_mad-signatures.expected
@@ -17670,6 +17670,55 @@ signatureMatches
| taint.cpp:822:6:822:19 | take_const_ptr | (unsigned long *,const char *) | | set_cert_ex | 1 |
| taint.cpp:822:6:822:19 | take_const_ptr | (unsigned long *,const char *) | | set_name_ex | 1 |
| taint.cpp:822:6:822:19 | take_const_ptr | (uv_pipe_t *,const char *) | | uv_pipe_bind | 1 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | _IO_default_uflow | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | _IO_feof | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | _IO_ferror | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | _IO_file_close_mmap | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | _IO_file_underflow_mmap | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | _IO_ftell | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | _IO_getc | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | _IO_getwc | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | _IO_new_file_underflow | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | _IO_peekc_locked | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | _IO_str_count | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | _IO_str_underflow | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | _IO_sungetc | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | _IO_sungetwc | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | _IO_wdefault_uflow | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | _IO_wfile_underflow | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | _IO_wfile_underflow_mmap | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | _IO_wstr_count | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | _IO_wstr_underflow | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | __fbufsize | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | __feof_unlocked | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | __ferror_unlocked | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | __fileno | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | __flbf | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | __fopen_maybe_mmap | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | __fpending | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | __ftello | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | __fwriting | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | __getc_unlocked | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | __getwc_unlocked | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | __uflow | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | __underflow | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | __wuflow | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | __wunderflow | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | feof_unlocked | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | ferror_unlocked | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | fgetc_unlocked | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | fgetgrent | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | fgetpwent | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | fgetsgent | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | fgetspent | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | getc_unlocked | 0 |
+| taint.cpp:836:6:836:18 | indirect_sink | (FILE *) | | getmntent | 0 |
+| taint.cpp:837:5:837:11 | fprintf | (CURLSH *,CURLSHoption,...) | | curl_share_setopt | 2 |
+| taint.cpp:837:5:837:11 | fprintf | (Jim_Interp *,const char *,...) | | Jim_SetResultFormatted | 1 |
+| taint.cpp:837:5:837:11 | fprintf | (Jim_Interp *,const char *,...) | | Jim_SetResultFormatted | 2 |
+| taint.cpp:837:5:837:11 | fprintf | (char **,const char *,...) | | ___asprintf | 1 |
+| taint.cpp:837:5:837:11 | fprintf | (char **,const char *,...) | | ___asprintf | 2 |
+| taint.cpp:837:5:837:11 | fprintf | (curl_httppost **,curl_httppost **,...) | | curl_formadd | 2 |
| thread.cpp:4:6:4:9 | sink | (int) | | ASN1_STRING_type_new | 0 |
| thread.cpp:4:6:4:9 | sink | (int) | | ASN1_tag2bit | 0 |
| thread.cpp:4:6:4:9 | sink | (int) | | ASN1_tag2str | 0 |
@@ -47191,6 +47240,10 @@ getParameterTypeName
| taint.cpp:817:6:817:27 | write_to_const_ptr_ptr | 1 | const char ** |
| taint.cpp:822:6:822:19 | take_const_ptr | 0 | const char * |
| taint.cpp:822:6:822:19 | take_const_ptr | 1 | const char * |
+| taint.cpp:836:6:836:18 | indirect_sink | 0 | FILE * |
+| taint.cpp:837:5:837:11 | fprintf | 0 | FILE * |
+| taint.cpp:837:5:837:11 | fprintf | 1 | const char * |
+| taint.cpp:837:5:837:11 | fprintf | 2 | ... |
| thread.cpp:4:6:4:9 | sink | 0 | int |
| thread.cpp:6:8:6:8 | operator= | 0 | S && |
| thread.cpp:6:8:6:8 | operator= | 0 | const S & |
diff --git a/csharp/ql/lib/change-notes/2025-08-18-implicit-reads-at-sinks.md b/csharp/ql/lib/change-notes/2025-08-18-implicit-reads-at-sinks.md
new file mode 100644
index 00000000000..d66e982e6ae
--- /dev/null
+++ b/csharp/ql/lib/change-notes/2025-08-18-implicit-reads-at-sinks.md
@@ -0,0 +1,4 @@
+---
+category: minorAnalysis
+---
+* The default taint tracking configuration now allows implicit reads from collections at sinks and in additional flow steps. This increases flow coverage for many taint tracking queries and helps reduce false negatives.
diff --git a/csharp/ql/lib/semmle/code/csharp/dataflow/internal/TaintTrackingPrivate.qll b/csharp/ql/lib/semmle/code/csharp/dataflow/internal/TaintTrackingPrivate.qll
index b7681994e2c..3146720efe8 100644
--- a/csharp/ql/lib/semmle/code/csharp/dataflow/internal/TaintTrackingPrivate.qll
+++ b/csharp/ql/lib/semmle/code/csharp/dataflow/internal/TaintTrackingPrivate.qll
@@ -29,7 +29,10 @@ predicate defaultTaintSanitizer(DataFlow::Node node) {
* of `c` at sinks and inputs to additional taint steps.
*/
bindingset[node]
-predicate defaultImplicitTaintRead(DataFlow::Node node, DataFlow::ContentSet c) { none() }
+predicate defaultImplicitTaintRead(DataFlow::Node node, DataFlow::ContentSet c) {
+ exists(node) and
+ c.isElement()
+}
private class LocalTaintExprStepConfiguration extends ControlFlowReachabilityConfiguration {
LocalTaintExprStepConfiguration() { this = "LocalTaintExprStepConfiguration" }
diff --git a/csharp/ql/src/Security Features/CWE-090/LDAPInjection.qhelp b/csharp/ql/src/Security Features/CWE-090/LDAPInjection.qhelp
index 04f01720ce6..4af37eadfd7 100644
--- a/csharp/ql/src/Security Features/CWE-090/LDAPInjection.qhelp
+++ b/csharp/ql/src/Security Features/CWE-090/LDAPInjection.qhelp
@@ -12,7 +12,7 @@ is likely to be able to run malicious LDAP queries.
If user input must be included in an LDAP query, it should be escaped to
avoid a malicious user providing special characters that change the meaning
of the query. If possible, use an existing library, such as the AntiXSS
-library.
+library. One may also make their own encoder filter `LdapEncode` following RFC 4515 standards.
@@ -35,7 +35,6 @@ the query cannot be changed by a malicious user.