Repository navigation
Description
A configuration that raises accessPathLimit() above the language default (5) finds longer flows on CodeQL 2.26.2. It no longer does on 2.27.4, and the result is the same as with the default. Lower values still work.
CodeQL CLI release binaries 2.26.2 and 2.27.4 on macOS 26.6 (arm64), with the codeql/java-all and codeql/dataflow packs bundled with each.
Each program NestedN (N = 3 to 7) passes a parameter through N nested java.util.Lists and back into Runtime.exec. Nested6:
public class Nested6 {
void handle(String input) throws java.io.IOException {
java.util.List c0 = new java.util.ArrayList(); c0.add(input);
java.util.List c1 = new java.util.ArrayList(); c1.add(c0);
java.util.List c2 = new java.util.ArrayList(); c2.add(c1);
java.util.List c3 = new java.util.ArrayList(); c3.add(c2);
java.util.List c4 = new java.util.ArrayList(); c4.add(c3);
java.util.List c5 = new java.util.ArrayList(); c5.add(c4);
java.util.List cur = c5;
cur = (java.util.List) cur.get(0);
cur = (java.util.List) cur.get(0);
cur = (java.util.List) cur.get(0);
cur = (java.util.List) cur.get(0);
cur = (java.util.List) cur.get(0);
Runtime.getRuntime().exec((String) cur.get(0));
}
}The query only varies accessPathLimit:
import java
import semmle.code.java.dataflow.TaintTracking
module C implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node n) { n.asParameter().getName() = "input" }
predicate isSink(DataFlow::Node n) {
exists(MethodCall mc | mc.getMethod().hasName("exec") and n.asExpr() = mc.getArgument(0))
}
int accessPathLimit() { result = 8 } // varied below
}
module F = TaintTracking::Global<C>;
from DataFlow::Node s, DataFlow::Node t where F::flow(s, t)
select t, s.getEnclosingCallable().getDeclaringType().getName()Programs found (codeql database create --build-mode=none, then codeql query run):
accessPathLimit() |
2.26.2 (java-all 9.2.2, dataflow 2.1.10) | 2.27.4 (java-all 9.3.4, dataflow 2.1.17) |
|---|---|---|
| default (5) | Nested3-5 | Nested3-5 |
| 3 | Nested3 | Nested3 |
| 4 | Nested3-4 | Nested3-4 |
| 6 | Nested3-6 | Nested3-5 |
| 8 | Nested3-7 | Nested3-5 |
| 10, 20 | Nested3-7 | Nested3-5 |
I ran only 2.26.2 and 2.27.4. From the sources, the change below is not in 2.26.3 or 2.26.4, and it is in 2.27.0.
Where it seems to come from. #22392 ("Track access path lengths in stages 3 and 4") gave the cached access path fronts a length, in shared/dataflow/codeql/dataflow/internal/DataFlowImplCommon.qll:
TFrontHead(Content c, int length) { length in [1 .. accessPathLimit()] }
TApproxFrontHead(ContentApprox c, int length) { length in [1 .. accessPathLimit()] }MakeImplCommon does private import Lang, so this accessPathLimit() is the language default, not the configuration's. DataFlowImpl.qll then uses Config::accessPathLimit(), for example len in [2 .. Config::accessPathLimit()]. A configured limit above the default cannot be represented by these fronts, so the longer paths are pruned.
As far as I can tell, the cap is a side effect rather than a decision. #22392 is about performance and does not mention the per-configuration
limit, and the fronts are cached per language, so they cannot see the configuration. Configurations in this repository only lower the limit (the
reason ConfigSig::accessPathLimit was added in #15867), so the existing tests would not notice the change.
Expected. As before 2.27.0, a configured accessPathLimit() is honoured in both directions. If capping it at the language default is intended, for example to keep the cached fronts small, the ConfigSig::accessPathLimit documentation should say so.
Activity
sam-robson commentedon Nov 6, 2026
Thanks for the report @yuan2li . We'll look into this and get back to you.