Files
Open_Ultra_SAST/benchmarks/measurements/2026-09-22-php-query-cost-identified.json
T
Marius CiepluchandClaude Opus 5 9910dd18b9 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
2026-09-22 09:53:52 +02:00

122 lines
5.2 KiB
JSON

{
"schema_version": 1,
"experiment": "php-query-cost-identified-v1",
"date": "2026-09-22",
"task": "pre-push-safety-net 13.2",
"claim_scope": "What makes a PHP arbitration batch unaffordable. One graph, built once, three controlled sweeps plus the driver's own log.",
"the_answer": "The cost is the number of CALL NODES a family's sink names match in this repository, not the number of requests in the batch, not the number of sink names declared, and 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.",
"the_evidence": {
"batch_size_is_exonerated": {
"sweep": [
{
"requests": 25,
"seconds": 71.4,
"answered": true,
"rc": 0,
"per_request": 2.86
},
{
"requests": 50,
"seconds": 76.0,
"answered": true,
"rc": 0,
"per_request": 1.52
},
{
"requests": 100,
"seconds": 155.4,
"answered": true,
"rc": 0,
"per_request": 1.55
},
{
"requests": 200,
"seconds": 166.7,
"answered": true,
"rc": 0,
"per_request": 0.83
},
{
"requests": 400,
"seconds": 169.7,
"answered": true,
"rc": 0,
"per_request": 0.42
}
],
"reading": "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": {
"sweep": [
{
"callDepth": "0",
"parameterSources": "false",
"seconds": 150.2,
"answered": true,
"rc": 0
},
{
"callDepth": "3",
"parameterSources": "false",
"seconds": 156.9,
"answered": true,
"rc": 0
},
{
"callDepth": "0",
"parameterSources": "true",
"seconds": 226.0,
"answered": true,
"rc": 0
},
{
"callDepth": "3",
"parameterSources": "true",
"seconds": 221.3,
"answered": true,
"rc": 0
}
],
"reading": "At 200 requests the full production shape, call depth 3 with parameter sources, costs 221 s against a 150 s baseline. Not the difference between 170 s and a 1,200 s timeout."
},
"the_family_is_the_variable": {
"sweep": [
{
"family": "injection_sql",
"sinks_declared": 6,
"seconds": 146.8,
"answered": true
},
{
"family": "output_encoding",
"sinks_declared": 7,
"seconds": 704.2,
"answered": true
},
{
"family": "path",
"sinks_declared": 16,
"seconds": 193.8,
"answered": true
}
],
"reading": "Same graph, same 100 requests, same query. SQL injection costs 147 s with six declared sinks; path costs 194 s with sixteen; output encoding costs 704 s with seven. The declared count does not predict it and the request count does not predict it. What separates output encoding is that `echo` and `print` match thousands of calls where `$wpdb->get_var` matches a handful."
},
"the_driver_named_its_own_expensive_step": [
"cpg batch taint: 2435 request(s) in 45.5s, answered (evidence, no dataflow)",
"cpg batch taint: 65 request(s) in 21.6s, answered",
"cpg batch taint: 315 request(s) in 1200.1s, unanswered (arbitration, all families)",
"cpg batch taint: split abandoned at part 1, no time left",
"cpg batch dominance: 13 request(s) in 20.6s, answered"
]
},
"why_it_stayed_unidentified_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. Nine hypotheses were eliminated that way, and the tenth was not a hypothesis anyone had stated.",
"why_splitting_did_not_rescue_it": "The driver splits a failed batch into eight parts BY REQUEST COUNT. Each part still carries the same everywhere-sinks, so each part is expensive in proportion to its share and the whole is no cheaper. Splitting by request count cannot help when the cost is per matched sink.",
"fix_direction": "Budget a batch by the sink nodes it implies rather than by the requests it holds. The count is already computed: taint.sc memoises its candidate sink list per sink set, and the evidence pass reports the in-scope sink list per request before any dataflow runs. A driver that reads it can size batches, and can report a family whose sinks are everywhere as expensive rather than as failed.",
"limits": [
"One subject and one language. The effect should be proportional to how common a family's sinks are in a codebase, which is a property of the code.",
"The sweeps use file-scoped requests; production also sends function-scoped ones, which were not separately timed.",
"`echo` as an output-encoding sink is correct for PHP. The finding is about cost, not about whether the fact is right."
]
}