measure(php): the query cost is the matched sink count, and the blocker is identified

Open since September and re-measured nine times. The cost of a taint batch is the number of CALL
NODES a family sink names match in the repository. Not the request count, not the declared sink
count, not any request parameter. reachableByFlows is asked per matched sink, and PHP
soutput-encoding sinks are echo and print, which a WordPress plugin writes thousands of times.

Three controlled sweeps on one graph, built once.

  Batch size exonerated: 400 requests answer in 170 s, and cost per request FALLS with size because
  roughly 70 s is fixed JVM start and graph load.

  Request parameters exonerated: at 200 requests the full production shape, call depth 3 with
  parameter sources, costs 221 s against a 150 s baseline.

  The family is the variable: at the same 100 requests, SQL injection 147 s with six declared sinks,
  path 194 s with sixteen, output encoding 704 s with seven.

And the driver named its own expensive step once it was asked to: 2,435 evidence requests in 45.5 s,
then 315 arbitration requests unanswered at 1,200 s, then a split abandoned for want of time.

It survived since September because every reproduction outside the driver carried the SQL sinks --
the family the declared target belongs to, and the one family that is cheap on this subject. Nine
hypotheses were eliminated by re-measuring the cheap case.

Splitting never rescued it because a failed batch is split by REQUEST COUNT and each part carries the
same everywhere-sinks. contributor-scan 7.7 carries the fix: budget a batch by the sink nodes it
implies, using the count the evidence pass already computes, and report a family whose sinks are
everywhere as expensive rather than as failed.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-22 09:53:52 +02:00
co-authored by Claude Opus 5
parent 4a5bf0aaf9
commit 9910dd18b9
3 changed files with 136 additions and 1 deletions
+6 -1
View File
@@ -465,7 +465,12 @@ Preserve all completed task evidence below.
- _The scale limit, quantified._ Roughly one to two seconds per question. 25 and 100 regions complete cleanly; 500 regions asks 2,526 questions, needs more than the 1,800-second ceiling and is killed. Nothing pathological, and the retry's fast failure is still unexplained.
- _What is NOT the problem, each measured rather than assumed:_ the malformed-CPG bug (fixed in 4.0.625, verified on the real trigger file); the dataflow overlay (42 s on the full 637-file graph); the engine version (4.0.625 and 4.0.631 indistinguishable); and the bundled Stripe, Braintree and Twocheckout SDKs under `includes/lib`, which the vendored list does not cover — that was the hypothesis and the overlay costs 42 s with them and 37 s without, so it is refuted.
- **The remaining real gap is bookkeeping, not detection:** `questions_completed` is zero in every PHP run including the ones that find things, with `cross_partition_semantics_unresolved` and `partition_file_census_incomplete` present throughout. Findings and question outcomes disagree in public, and the admission policy reads the outcomes. Group 12 raised completion for JavaScript and Python and PHP was never measured. Evidence: `benchmarks/measurements/2026-09-20-php-detection-corrected.json`.
- **Blocker still unidentified; investigation paused 2026-09-19 to return to vulnerability work.** Six hypotheses were measured and eliminated: batch size (17,700 requests answer in 44 s with a 2.6 MB response), dataflow cost (50 execution requests in 24 s), call depth 3 with parameter sources (24.7 s for 20), argument length (a 16 KB hook table against a 128 KB cap), hook callback resolution (1 s difference with 233 hooks), and build or sharding (79 s, no shards). Preparation measures 273.5 s. Every reproduction of the failing 22,125-request batch outside the push path answers in under a minute, and the difference from the production invocation is not known. The exception has never been read, because the backend logged a stack trace's tail and the corrected extractor reports its head, which on this engine is JVM startup noise. Evidence: `benchmarks/measurements/2026-09-19-php-blocker-open.json`.
- **IDENTIFIED 2026-09-22.** The cost is the number of CALL NODES a family's sink names match in the repository. Not the request count, not the declared sink count, not any request parameter. `reachableByFlows` is asked per matched sink, and PHP's output-encoding sinks are `echo` and `print`, which a WordPress plugin writes thousands of times.
- _Three controlled sweeps on one graph, built once._ Batch size is exonerated: 400 requests answer in 170 s and cost per request FALLS with size, because roughly 70 s is fixed JVM start and graph load. Request parameters are exonerated: at 200 requests the full production shape, call depth 3 with parameter sources, costs 221 s against a 150 s baseline. The family is the variable: at the same 100 requests, SQL injection costs 147 s with six declared sinks, path costs 194 s with sixteen, and output encoding costs 704 s with seven.
- _And the driver named its own expensive step, once it was asked to._ `taint: 2435 request(s) in 45.5s, answered` for the evidence pass, then `taint: 315 request(s) in 1200.1s, unanswered` for arbitration across all families, then a split abandoned for want of time.
- _Why it survived since September:_ every reproduction outside the driver carried the SQL sinks, because that is the family the declared target belongs to. The one family that is cheap on this subject was the one being re-measured, and nine hypotheses were eliminated that way.
- _Why splitting never rescued it:_ a failed batch is split into eight parts BY REQUEST COUNT, and each part carries the same everywhere-sinks, so the whole is no cheaper. Splitting by request count cannot help when the cost is per matched sink. Evidence: `benchmarks/measurements/2026-09-22-php-query-cost-identified.json`.
- **Blocker identified; the fix is contributor-scan 7.7.** Six hypotheses were measured and eliminated: batch size (17,700 requests answer in 44 s with a 2.6 MB response), dataflow cost (50 execution requests in 24 s), call depth 3 with parameter sources (24.7 s for 20), argument length (a 16 KB hook table against a 128 KB cap), hook callback resolution (1 s difference with 233 hooks), and build or sharding (79 s, no shards). Preparation measures 273.5 s. Every reproduction of the failing 22,125-request batch outside the push path answers in under a minute, and the difference from the production invocation is not known. The exception has never been read, because the backend logged a stack trace's tail and the corrected extractor reports its head, which on this engine is JVM startup noise. Evidence: `benchmarks/measurements/2026-09-19-php-blocker-open.json`.
- _Boundary: Evaluation harness, full-run integration_
- _Depends: 13.1_
- _Requirements: 6.2, 6.3, 8.1, 8.2_