Skip to content

fix(compiler): keep typed property is-a check for unknown object values - #156

Open
alwaysLinger wants to merge 1 commit into
swoole:masterfrom
alwaysLinger:fix/object-property-unknown-class-assign-check
Open

alwaysLinger wants to merge 1 commit into
swoole:masterfrom
alwaysLinger:fix/object-property-unknown-class-assign-check

Conversation

@alwaysLinger

Copy link
Copy Markdown

Observed failure

Assigning a value that is statically typed only as object to a property declared with a specific class compiles without a diagnostic and runs without a runtime check: a wrong-class object is stored in the typed property slot. ZendPHP rejects the same assignment with a TypeError at runtime.

Because AOT object properties are fixed-layout C++ slots, the stored object is later read as the declared class, so every subsequent typed access operates on the wrong object layout.

<?php
class Foo { }
class Bar { }

class Holder
{
    public Foo $p;
}

function assign(object $o, Holder $h): void
{
    $h->p = $o;
}

function main(): void
{
    $h = new Holder();
    assign(new Bar(), $h);
    var_dump($h->p);
}

ZendPHP 8.4:

Fatal error: Uncaught TypeError: Cannot assign Bar to property Holder::$p of type Foo

TypePHP master (65d2ea7):

object(Bar)#2 (1) {
  ["y"]=>
  int(2)
}

Reproduction

Save the file above as repro.php and compare:

php repro.php                                     # ZendPHP: TypeError
php vendor/bin/tpc.php repro.php -O2 --run        # TypePHP: silent object(Bar)

Right values typed mixed or produced by std::any() already generate the runtime is-a check and fail with the expected TypeError; object is the only static type that silently bypasses it.

Root cause

assertCanAssignObjectProperty() defers to wrapObjectPropertyAssignTypeCheck() when the right side's class is statically unknown (the TODO: ... a runtime check is required branch). The deferred check is then skipped:

  1. wrapObjectPropertyAssignTypeCheck() returns early when canAssignStaticTypeToObjectProperty($def, $rightType) is true;
  2. canAssignStaticTypeToObjectProperty() compares storage types only (default => $rightType === $def->type) and never consults $def->class;
  3. a property declared as Foo is stored as Type::OBJECT, and an object parameter is detected as Type::OBJECT, so Type::OBJECT === Type::OBJECT holds and the runtime check is dropped.

The generated C++ for the repro is a bare attribute write with no check:

h.attr(get_persistent_prop(...), php::AttrMode::Update) = o;

while the same assignment with a mixed right value correctly generates:

if (UNEXPECTED(!((tmp.isObject() && php::instanceOf(tmp, ...Foo...))))) {
    php::throwExceptionEx(zend_ce_type_error, 0,
        "Holder::$p must be of type Foo, %s given", tmp.typeStr());
}

Fix

src/Parser/PropertyAccessTrait.php (+9/-2): in wrapObjectPropertyAssignTypeCheck(), do not take the static-assignable early return when the right side is Type::OBJECT with a statically unknown class and the property declares a specific class. Those writes fall through to the existing runtime is-a check generation used for Type::VAR right values.

canAssignStaticTypeToObjectProperty() itself is unchanged: its other two callers (assertCanAssignObjectProperty() and the compound-assignment path in AssignOpTrait) rely on the storage-type verdict to raise compile-time fatals, and changing it globally would reject valid programs whose object value happens to match at runtime. Assignments that are statically provable (subclass to base, known compatible classes) still compile with no runtime check.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant