Skip to content

Fix GH-21687: array_walk creates references to readonly properties#21690

Open
iliaal wants to merge 1 commit into
php:masterfrom
iliaal:fix/gh-21687-enum-array-walk-assertion
Open

Fix GH-21687: array_walk creates references to readonly properties#21690
iliaal wants to merge 1 commit into
php:masterfrom
iliaal:fix/gh-21687-enum-array-walk-assertion

Conversation

@iliaal

@iliaal iliaal commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #21687

array_walk() wrapped object properties in IS_REFERENCE for the callback, corrupting readonly slots (enum singletons crashed on subsequent var_dump(); regular readonly properties were silently modifiable). By-ref callbacks now throw the same "Cannot acquire reference to readonly property" error as foreach; by-val callbacks copy the value into a temp so the slot stays intact.

@ndossche ndossche left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. You're missing the point that it corrupts the enum and shm.

@iliaal iliaal force-pushed the fix/gh-21687-enum-array-walk-assertion branch from 56b2b9f to 1cbea32 Compare April 9, 2026 16:58
@iliaal iliaal requested a review from bukka as a code owner April 9, 2026 16:58
@iliaal iliaal changed the title Fix GH-21687: assertion failure in zend_enum_fetch_case_name after array_walk Fix GH-21687: array_walk creates references to readonly properties Apr 9, 2026
@iliaal

iliaal commented Apr 9, 2026

Copy link
Copy Markdown
Contributor Author

Reworked. The fix is now in array_walk: check ZEND_ACC_READONLY before creating the reference, same check foreach already does. Enum properties are no longer corrupted, and regular readonly properties can no longer be silently modified through the callback.

@ndossche

ndossche commented Apr 9, 2026

Copy link
Copy Markdown
Member

Just from looking at the code I think that you can violate the type of a typed property, e.g. put a string where an int is expected.

@iliaal

iliaal commented Apr 9, 2026

Copy link
Copy Markdown
Contributor Author

No. You're missing the point that it corrupts the enum and shm.

Was a little to quick fixing the symptom as opposed to the cause 🤦 Here goes attempt #2

@iliaal iliaal requested a review from ndossche April 9, 2026 18:02
Comment thread ext/standard/array.c Outdated
ZVAL_NEW_REF(zv, zv);
ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(zv), prop_info);
if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
zend_throw_error(NULL,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the canonical message is Cannot indirectly modify readonly property Foo::$prop

E.g. from:

<?php

class Foo {
    public readonly int $prop;
    public function __construct() {
        $this->prop = 1;
    }
}

$foo = new Foo;
$ref =& $foo->prop;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

foreach by-ref uses "Cannot acquire reference to readonly property", direct $ref =& $foo->prop uses "Cannot indirectly modify readonly property."

 class Foo {
      public readonly int $prop;
      public function __construct() { $this->prop = 1; }
  }
  $foo = new Foo;
  foreach ($foo as &$v) {}
  // Error: Cannot acquire reference to readonly property Foo::$prop

I matched the foreach wording since array_walk iterates properties and both are array related, but "Cannot indirectly modify" works too. @ndossche I can update the message, if you prefer, but there is a bit of inconsistency here

@ndossche ndossche left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok for me

@ndossche ndossche requested a review from iluuu1994 April 9, 2026 20:49

@iluuu1994 iluuu1994 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing the same handling for private and public private(set).

Edit: Thinking about it, the fact that this can access private properties at all is long-standing and probably shouldn't be changed without an RFC...

class C {
    public function __construct(
        public private(set) int $a = 1,
        private int $b = 1,
    ) {}
}

$c = new C;
array_walk($c, function(&$v) { $v++; });
var_dump($c);
object(C)#1 (2) {
  ["a"]=>
  int(2)
  ["b":"C":private]=>
  int(2)
}

But also, this breaks by-val array_walk() for readonly:

class C {
    public function __construct(
        public readonly int $a = 1,
    ) {}
}

$c = new C;
array_walk($c, function($v) { $v++; });
var_dump($c);
Fatal error: Uncaught Error: Cannot acquire reference to readonly property C::$a

It might be possible not converting each zv into a ref and replace this with an addref on the container. But I'm not sure. The key comment here from Nikita:

Ensure the value is a reference. Otherwise the location of the value may be freed.

When object properties are iterated, we need to make sure the object doesn't go away or we'll write to freed memory.

By-ref callbacks throw the same "Cannot acquire reference to readonly
property" error foreach uses. By-val callbacks copy the value into a
temp so MAKE_REF does not corrupt the slot.

Closes phpGH-21687
@iliaal iliaal force-pushed the fix/gh-21687-enum-array-walk-assertion branch from 1cbea32 to c2c6730 Compare May 3, 2026 18:59
@iliaal

iliaal commented May 3, 2026

Copy link
Copy Markdown
Contributor Author

Agree, private/public private(set) deferred to RFC. Updated PR, by-val now copies the value into a temp before MAKE_REF, slot stays intact.

@iliaal iliaal requested a review from iluuu1994 May 3, 2026 20:06
@Girgias

Girgias commented May 4, 2026

Copy link
Copy Markdown
Member

I'm planning on deprecating the iteration on objects as it is not consistent, see https://wiki.php.net/rfc/deprecations_php_8_6#passing_objects_for_array_parameter_of_array_walk_and_array_walk_recursive

@iliaal

iliaal commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

I guess if that goes through this is not needed then

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Assertion failure in zend_enum_fetch_case_name() when calling array_walk() on an enum case value after try/catch block

4 participants