Parent
Spec: #1 (upstream: Graphify-Labs#1682). Found by the red-team session against 2e68ec6. Repro scripts: /tmp/rt6/ (harness.py, wave1.py, wave2.py, wave3.py).
Problem
#4's scope poisoning covers non-new reassignment, conflicting new, augmented assignment, closure and arrow-fn params, foreach targets and list destructuring. It does not cover the two PHP statements that rebind a local name to different storage, so the resolver keeps a stale type binding and mints a wrong INFERRED edge.
Exploit A — global
// bootstrap.php
namespace App;
$svc = new Beta();
// app/Runner.php
namespace App;
class Runner {
public function go(): void {
$svc = new Alpha();
global $svc; // $svc is now an alias of the GLOBAL; the local is discarded
$svc->run();
}
}
Actual:
app/Runner.php:.go() -> app/Alpha.php:.run() INFERRED 0.8
Expected: refuse. At runtime $svc is the global (Beta), never Alpha.
Repro: /tmp/rt6/wave2.py::B3_global_real (and wave1.py::A2_global without a global assignment present).
Exploit B — function-static
public function go(): void {
$svc = new Alpha();
static $svc; // rebinds to the function-static slot (initially null)
$svc->run();
}
Actual: go() -> Alpha.php:.run() INFERRED 0.8. Expected: refuse.
Repro: /tmp/rt6/wave2.py::B4_static_stmt.
Controls (poisoning that DOES work)
From /tmp/rt6/wave3.py:
$svc = new Alpha(); $svc = new Beta(); $svc->run(); → no edge (F2_control_conflicting_new)
$svc = new Alpha(); $svc = $x; $svc->run(); → no edge (F2_control_non_new_reassign)
$svc = new Alpha(); global $other; $svc->run(); → edge to Alpha::run correctly minted (F2_global_unrelated_name)
That last control matters for the fix: the defect is name-targeted, not statement-targeted. A global/static statement naming an unrelated variable must keep the binding intact.
Realism
global $db; / global $config; is idiomatic in the pre-7.4 and pre-PSR-4 codebases that issue Graphify-Labs#1682 explicitly targets, and static $conn; memoization inside a method is a common connection/cache pattern. Both appear in exactly the legacy PHP where typed locals coexist with global state.
What to build
Poison every name declared by a global or static variable statement inside the method body, in the same pass that already poisons foreach targets and closure params — i.e. name-targeted: remove that specific name from the method's receiver-type table, leaving other bindings untouched.
Both statement node types need a probe on the pinned grammar (tree-sitter-php 0.24.1) to confirm the child shape carrying the variable names; the existing poison sites are the natural template.
Acceptance criteria
Parent
Spec: #1 (upstream: Graphify-Labs#1682). Found by the red-team session against
2e68ec6. Repro scripts:/tmp/rt6/(harness.py,wave1.py,wave2.py,wave3.py).Problem
#4's scope poisoning covers non-
newreassignment, conflictingnew, augmented assignment, closure and arrow-fn params,foreachtargets and list destructuring. It does not cover the two PHP statements that rebind a local name to different storage, so the resolver keeps a stale type binding and mints a wrongINFERREDedge.Exploit A —
globalActual:
Expected: refuse. At runtime
$svcis the global (Beta), neverAlpha.Repro:
/tmp/rt6/wave2.py::B3_global_real(andwave1.py::A2_globalwithout a global assignment present).Exploit B — function-
staticActual:
go() -> Alpha.php:.run()INFERRED 0.8. Expected: refuse.Repro:
/tmp/rt6/wave2.py::B4_static_stmt.Controls (poisoning that DOES work)
From
/tmp/rt6/wave3.py:$svc = new Alpha(); $svc = new Beta(); $svc->run();→ no edge (F2_control_conflicting_new)$svc = new Alpha(); $svc = $x; $svc->run();→ no edge (F2_control_non_new_reassign)$svc = new Alpha(); global $other; $svc->run();→ edge toAlpha::runcorrectly minted (F2_global_unrelated_name)That last control matters for the fix: the defect is name-targeted, not statement-targeted. A
global/staticstatement naming an unrelated variable must keep the binding intact.Realism
global $db;/global $config;is idiomatic in the pre-7.4 and pre-PSR-4 codebases that issue Graphify-Labs#1682 explicitly targets, andstatic $conn;memoization inside a method is a common connection/cache pattern. Both appear in exactly the legacy PHP where typed locals coexist with global state.What to build
Poison every name declared by a
globalorstaticvariable statement inside the method body, in the same pass that already poisonsforeachtargets and closure params — i.e. name-targeted: remove that specific name from the method's receiver-type table, leaving other bindings untouched.Both statement node types need a probe on the pinned grammar (tree-sitter-php 0.24.1) to confirm the child shape carrying the variable names; the existing poison sites are the natural template.
Acceptance criteria
$svc = new Alpha(); global $svc; $svc->run();yields no edge$svc = new Alpha(); static $svc; $svc->run();yields no edgeglobal $other;/static $other;naming an unrelated variable leaves$svc's binding intact and the correct edge is still emittedglobal $a, $svc;/static $a = 1, $svc;) poison every listed name