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

@iliaaliliaal 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.

@ndosschendossche 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
iliaalforce-pushed the fix/gh-21687-enum-array-walk-assertion branch from 56b2b9f to 1cbea32CompareApril 9, 2026 16:58
@iliaal
iliaal requested a review from bukka as a code ownerApril 9, 2026 16:58
@iliaaliliaal changed the title Fix GH-21687: assertion failure in zend_enum_fetch_case_name after array_walkFix GH-21687: array_walk creates references to readonly propertiesApr 9, 2026
@iliaal

Copy link
Copy Markdown
ContributorAuthor

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

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

Copy link
Copy Markdown
ContributorAuthor

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 ndosscheApril 9, 2026 18:02
Comment threadext/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:

<?phpclass Foo {
publicreadonlyint$prop;
publicfunction__construct() {
$this->prop = 1;
}
}
$foo = newFoo;
$ref =& $foo->prop;

Copy link
Copy Markdown
ContributorAuthor

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 {
publicreadonlyint$prop;
publicfunction__construct() { $this->prop = 1; }
}
$foo = newFoo;
foreach ($fooas &$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

@ndosschendossche 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 iluuu1994April 9, 2026 20:49

@iluuu1994iluuu1994 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 {
publicfunction__construct(
public private(set) int$a = 1,
privateint$b = 1,
) {}
}
$c = newC;
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 {
publicfunction__construct(
publicreadonlyint$a = 1,
) {}
}
$c = newC;
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.
ClosesphpGH-21687
@iliaal
iliaalforce-pushed the fix/gh-21687-enum-array-walk-assertion branch from 1cbea32 to c2c6730CompareMay 3, 2026 18:59
@iliaal

Copy link
Copy Markdown
ContributorAuthor

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 iluuu1994May 3, 2026 20:06
@Girgias

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

Copy link
Copy Markdown
ContributorAuthor

I guess if that goes through this is not needed then

Sign up for freeto 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

@iliaal@ndossche@Girgias@iluuu1994