Skip to content

PHP: global and static statements do not poison the receiver type table, minting stale-type edges #13

Description

@filipechagas

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

  • $svc = new Alpha(); global $svc; $svc->run(); yields no edge
  • $svc = new Alpha(); static $svc; $svc->run(); yields no edge
  • global $other; / static $other; naming an unrelated variable leaves $svc's binding intact and the correct edge is still emitted
  • Multi-name forms (global $a, $svc; / static $a = 1, $svc;) poison every listed name
  • Existing poison cases from PHP: typed locals and typed params resolve, with scope poisoning #4 (conflicting new, non-new reassignment, augmented assignment, closure/arrow params, foreach, list destructuring) unchanged
  • Full test suite green

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingready-for-agentFully specified, ready for an AFK agent

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions