fix(cpg): a query that could not be asked is not an empty answer

contributor-scan 5.7, and it was the wrong question.

**The 117k-line pmpro CPG does not load.** Importing it throws in a
ForkJoinParallelCpgPassWithAccumulator during overlay application, so every
query against it returns nothing. It is not a call-graph question at all.

Isolated by running the ARBITER against two prebuilt CPGs rather than
writing a sixth ad-hoc Scala probe, four of which had already failed on my
own mistakes:

  small wpdb CPG   get_order_by_code       -> 1 model_entailed
  pmpro CPG        getMemberOrderByCode    -> 0

Both have their source and sink in ONE function, both were asked with
parameter sources and depth 3, same arbiter and same spec. The CPG is the
only difference.

That clears the facts, which is worth stating: `$wpdb` sinks work, the
parameter-source rule works, and a PHP parameter reaching `$wpdb->get_var`
through a concat is entailed. Whether php2cpg resolves `new X()` and
`$this->method()` is still unmeasured -- it cannot be measured on a CPG
that will not load -- and is recorded as 5.10 along with finding the size
boundary.

**And the defect underneath it, which is the fourth quiet failure found on
this one checkout.** `query_batch` returned `{}` for a failure and `{}` for
a genuine empty answer, so a caller could not tell "could not ask" from
"nothing found". Every query on this scan failed at load and it reported
500 regions examined, no findings, and no degradation: a vulnerable plugin,
clean.

A failed batch now returns None, unparseable output counts as a failure
rather than an empty answer, and the driver records a `query_failed`
degradation naming the kind and the request count.

This is the failure mode the rung ladder structurally cannot catch. Every
other honesty mechanism here labels a VERDICT -- suspicion,
regions_unjudged, regions_unasked, stated family limits -- and when no
verdict is produced there is nothing to label.

Gates byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
Marius Ciepluch
2026-09-09 17:15:51 +02:00
co-authored by Claude Opus 5
parent db273dde53
commit 21fdb554aa
8 changed files with 133 additions and 20 deletions
+22 -1
View File
@@ -476,7 +476,7 @@
- _Requirements: 8.1, 9.1_
- _Depends: 5.1_
- [ ] 5.7 Does the PHP call graph carry a flow through an object?
- [x] 5.7 Does the PHP call graph carry a flow through an object? — NOT a call-graph question
- **The WordPress checkout scans and does not find its CVE.** Both ends are inside the budget --
`pmpro_rest_api_get_order` at position 290 as a recognised entry point, `getMemberOrderByCode` at 472 --
and the taint query runs for 23 seconds. So the question is the middle.
@@ -490,6 +490,15 @@
something else, which is a finding worth having either way.
- Observable: a recorded answer naming which edges exist, and either the CVE found or a stated reason it
cannot be.
- **Answered 2026-09-09, and it was the wrong question.** The 117k-line pmpro CPG does not LOAD -- import
throws in a ForkJoinParallelCpgPassWithAccumulator during overlay application -- so every query against
it returns nothing. Isolated by running the ARBITER against two prebuilt CPGs rather than writing more
ad-hoc Scala: `get_order_by_code` on the small probe returns 1 `model_entailed` with parameter sources
and depth 3, while `getMemberOrderByCode` on pmpro, the identical shape, returns 0 under identical
settings. Same arbiter, same spec, same question; the CPG is the difference.
- That clears the facts: `$wpdb` sinks work, the parameter-source rule works, and a PHP parameter reaching
`$wpdb->get_var` through a concat is entailed. Whether php2cpg resolves `new X()` and `$this->method()`
remains **unmeasured**, because it cannot be measured on a CPG that will not load. Recorded as 5.10.
- _Requirements: 4.4, 8.1, 9.1_
- _Depends: 5.1_
@@ -523,6 +532,18 @@
- _Requirements: 8.1, 9.1_
- _Depends: 5.6_
- [ ] 5.10 A PHP CPG large enough to matter that actually loads
- `php2cpg` builds a 4.3MB CPG for a 637-file plugin with no errors, and importing it throws during
overlay application. Every query then returns nothing.
- Find the boundary: does it load at 100 files, 300, 600? Is one file responsible, or is it size? Joern's
own message suggests the frontend for large codebases, which is what the fallback already does, so the
remaining lever is whether overlays can be applied separately or the tree scoped.
- Until this is answered, PHP detection is demonstrated only on files small enough to build cleanly, and
no claim about WordPress can be made either way.
- Observable: the largest PHP tree that loads and queries, measured, with the failure boundary recorded.
- _Requirements: 4.2, 4.3, 9.1_
- _Depends: 5.7_
## Group 6 — Regression baselines
- [ ] 6.1 Repository baseline and delta
+27 -2
View File
@@ -43,6 +43,30 @@ definite 1, or unknown, with abstract transformers for `INT_ADD`, `INT_MULT`, `I
validated against concrete execution in its own tests. That is precisely the shape of analysis the C family
lacks, and it exists.
### The candidates, and why they are not equivalent
| | language | IR | analyses | licence | state |
| --- | --- | --- | --- | --- | --- |
| **angr** | **Python 3** (+ Rust) | VEX, lifting | CFG recovery, data-dependency, **value-set analysis**, symbolic execution | BSD-2-Clause | actively maintained, ~14.8k commits |
| **GhidraPAL** | Java | Ghidra P-Code | **three-valued-logic abstract interpreter**, transformers validated against concrete runs | see repo | research code, small |
| **Vine** | OCaml/C++ | own IL | BitBlaze static analysis | GPL-2.0 | dormant since 2009 |
**angr is the strongest candidate and Vine is not one.** Three things decide it:
* **It is Python.** This codebase is Python with `dependencies = []` in the core, and every heavyweight
capability so far has been reached across a subprocess boundary rather than imported. angr fits that
pattern as an extra, the way `harnessx` and the tree-sitter grammars already do.
* **Value-set analysis is the abstraction we are missing**, by name. "Can this index exceed this allocation"
is a VSA question, and VSA is abstract interpretation rather than SMT -- which is the choice
`model-grounded-detection` already made and gave its reasons for.
* **BSD-2-Clause**, against Vine's GPL-2.0. This project has a standing rule about never vendoring code
without a stated licence, and a GPL dependency is a decision about the whole tool rather than about one
analysis.
GhidraPAL stays interesting for a different reason: it is a worked example of the *domain*, not a platform.
Its TVL bit-vector and its validated transformers for `INT_ADD`, `INT_MULT` and the comparisons are the
thing to read before writing any abstract domain here, whether the platform underneath is angr or Ghidra.
It is also the honest answer to a decision this project already took. `model-grounded-detection` chose
abstract interpretation over SMT, and chose to build the model from code because execution cannot be
assumed. A bit-level abstract interpreter over P-Code is *still abstract interpretation* and still static —
@@ -53,8 +77,9 @@ what changes is the artifact: it needs a **built** library rather than its sourc
1. Whether the source-IR subtraction is safe, measured rather than assumed.
2. Whether the C arithmetic class is worth a second pipeline at all, or whether `c/memory` stays scoped to
string misuse and says so — which is where task 2.16 left it, deliberately.
3. If it is worth it: P-Code with a TVL domain, adapting GhidraPAL's transformers, versus writing one. Vine
is dormant since 2009 and is not the candidate; Ghidra is maintained and P-Code has tooling.
3. If it is worth it: **angr** first, on language, licence and the fact that value-set analysis is the
named abstraction -- with GhidraPAL read as the worked example of a bit-precise domain rather than as a
platform. Vine is dormant since 2009 and is not a candidate.
4. What a second pipeline costs in the one currency this project has been strict about — **not lines, but
the number of things that can silently return nothing.** This session found three separate quiet failures
in one PHP scan. A binary pipeline adds a build step, a lifter, and a domain, each of which can produce
+17 -6
View File
@@ -1,15 +1,26 @@
{
"feature_name": "ir-consolidation",
"created_at": "2026-09-09T17:00:00Z",
"updated_at": "2026-09-09T17:00:00Z",
"updated_at": "2026-09-09T17:30:00Z",
"language": "en",
"phase": "brief",
"approvals": {
"requirements": { "generated": false, "approved": false },
"design": { "generated": false, "approved": false },
"tasks": { "generated": false, "approved": false }
"requirements": {
"generated": false,
"approved": false
},
"design": {
"generated": false,
"approved": false
},
"tasks": {
"generated": false,
"approved": false
}
},
"ready_for_implementation": false,
"depends_on": ["contributor-scan"],
"approval_note": "2026-09-09 brief drafted after the maintainer raised codebase growth and suggested adopting an existing IR (Vine, or Rolf Rolles' work, or Ghidra's). The brief separates two questions with opposite answers. The SOURCE IR is already carried rather than about to be added: semantic/ is 3218 lines, 16% of the codebase, holding a hand-written IR and flat taint engine measured at 2.2% entailment against the CPG's 41.3%, still running as a proposer -- and Vine and P-Code are binary IRs that do not help, so the work there is subtraction not substitution. The MISSING abstraction is the C arithmetic-overflow class, where GhidraPAL's three-valued-logic abstract interpreter over Ghidra P-Code is exactly the right shape and exists; Vine is dormant since 2009 and is not the candidate. Constraint recorded: question 1 lands before question 2 starts, or the brief makes the growth problem worse. Not approved."
"depends_on": [
"contributor-scan"
],
"approval_note": "2026-09-09 brief drafted after the maintainer raised codebase growth and suggested adopting an existing IR (Vine, or Rolf Rolles' work, or Ghidra's). The brief separates two questions with opposite answers. The SOURCE IR is already carried rather than about to be added: semantic/ is 3218 lines, 16% of the codebase, holding a hand-written IR and flat taint engine measured at 2.2% entailment against the CPG's 41.3%, still running as a proposer -- and Vine and P-Code are binary IRs that do not help, so the work there is subtraction not substitution. The MISSING abstraction is the C arithmetic-overflow class, where GhidraPAL's three-valued-logic abstract interpreter over Ghidra P-Code is exactly the right shape and exists; Vine is dormant since 2009 and is not the candidate. Constraint recorded: question 1 lands before question 2 starts, or the brief makes the growth problem worse. Not approved. ; 2026-09-09 angr added as the strongest candidate for the binary-side question after the maintainer raised it: Python 3 so it fits the extras pattern this codebase already uses for heavyweight capabilities, BSD-2-Clause against Vine's GPL-2.0 which would be a decision about the whole tool, actively maintained, and it names VALUE-SET ANALYSIS as a capability -- which is precisely the abstraction c/memory lacks and is abstract interpretation rather than SMT, matching the architecture decision already taken. GhidraPAL stays as the worked example of a bit-precise TVL domain to read before writing one, not as a platform."
}