Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Side effects after zend_fetch_property_address() / get_property_ptr_ptr() #15938

Open
arnaud-lb opened this issue Sep 17, 2024 · 3 comments · May be fixed by #15961
Open

Side effects after zend_fetch_property_address() / get_property_ptr_ptr() #15938

arnaud-lb opened this issue Sep 17, 2024 · 3 comments · May be fixed by #15961

Comments

@arnaud-lb
Copy link
Member

arnaud-lb commented Sep 17, 2024

Description

In some cases it is possible to modify the object or release it after acquiring the address of a property with zend_fetch_property_address() / get_property_ptr_ptr(), leading to corruptions.

Here we remove the b key from the zend_object.properties hashtable, and then write a zval to the slot. This leaks the zval:

<?php

#[AllowDynamicProperties]
class C {
    public $a;
}

$obj = new C();
$obj->b = str_repeat('a', 10);
$obj->b .= new class {
    function __toString() {
        global $obj;
        unset($obj->b);
        return str_repeat('c', 10);
    }
};
ASAN

=================================================================
==376525==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 48 byte(s) in 1 object(s) allocated from:
    #0 0x69d0a0 in realloc (sapi/cli/php+0x69d0a0) (BuildId: 8e08199ed0daa8fb60d4f6bd5b7af95330f99943)
    #1 0x1ee6e9b in __zend_realloc Zend/zend_alloc.c:3299:6
    #2 0x1ee6749 in _erealloc Zend/zend_alloc.c:2758:10
    #3 0x2cf4354 in zend_string_extend Zend/zend_string.h:273:25
    #4 0x2cf2d7f in concat_function Zend/zend_operators.c:2082:17
    #5 0x28ed31c in zend_binary_op Zend/zend_execute.c:1651:9
    #6 0x246fcf4 in ZEND_ASSIGN_OBJ_OP_SPEC_CV_CONST_HANDLER Zend/zend_vm_execute.h:42460:7
    #7 0x2248b78 in execute_ex Zend/zend_vm_execute.h:58486:7
    #8 0x224a0cd in zend_execute Zend/zend_vm_execute.h:64138:2
    #9 0x2db1e2b in zend_execute_script Zend/zend.c:1928:3
    #10 0x18e97f1 in php_execute_script_ex main/main.c:2578:13
    #11 0x18ea108 in php_execute_script main/main.c:2618:9
    #12 0x2dbf5c0 in do_cli sapi/cli/php_cli.c:935:5
    #13 0x2dbc16f in main sapi/cli/php_cli.c:1309:18
    #14 0x7f067e183087 in __libc_start_call_main (/lib64/libc.so.6+0x2a087) (BuildId: 77c77fee058b19c6f001cf2cb0371ce3b8341211)
    #15 0x7f067e18314a in __libc_start_main@GLIBC_2.2.5 (/lib64/libc.so.6+0x2a14a) (BuildId: 77c77fee058b19c6f001cf2cb0371ce3b8341211)
    #16 0x601fa4 in _start (sapi/cli/php+0x601fa4) (BuildId: 8e08199ed0daa8fb60d4f6bd5b7af95330f99943)

SUMMARY: AddressSanitizer: 48 byte(s) leaked in 1 allocation(s).

Here the zend_object.properties ht is resized, so the address points to a freed block:

<?php

#[AllowDynamicProperties]
class C {
    public $a;
}

$obj = new C();
$obj->b = '';

$obj->b .= new class {
    function __toString() {
        global $obj;
        for ($i = 0; $i < 8; $i++) {
            $obj->{$i} = 0;
        }
        return 'str';
    }
};
ASAN
=================================================================
==376076==ERROR: AddressSanitizer: heap-use-after-free on address 0x51200001b229 at pc 0x000002cf3def bp 0x7ffd70f61e80 sp 0x7ffd70f61e78
READ of size 1 at 0x51200001b229 thread T0
    #0 0x2cf3dee in i_zval_ptr_dtor Zend/zend_variables.h:42:6
    #1 0x2cf0f30 in concat_function Zend/zend_operators.c:2031:5
    #2 0x28ed31c in zend_binary_op Zend/zend_execute.c:1651:9
    #3 0x246fcf4 in ZEND_ASSIGN_OBJ_OP_SPEC_CV_CONST_HANDLER Zend/zend_vm_execute.h:42460:7
    #4 0x2248b78 in execute_ex Zend/zend_vm_execute.h:58486:7
    #5 0x224a0cd in zend_execute Zend/zend_vm_execute.h:64138:2
    #6 0x2db1e2b in zend_execute_script Zend/zend.c:1928:3
    #7 0x18e97f1 in php_execute_script_ex main/main.c:2578:13
    #8 0x18ea108 in php_execute_script main/main.c:2618:9
    #9 0x2dbf5c0 in do_cli sapi/cli/php_cli.c:935:5
    #10 0x2dbc16f in main sapi/cli/php_cli.c:1309:18
    #11 0x7fa120b46087 in __libc_start_call_main (/lib64/libc.so.6+0x2a087) (BuildId: 77c77fee058b19c6f001cf2cb0371ce3b8341211)
    #12 0x7fa120b4614a in __libc_start_main@GLIBC_2.2.5 (/lib64/libc.so.6+0x2a14a) (BuildId: 77c77fee058b19c6f001cf2cb0371ce3b8341211)
    #13 0x601fa4 in _start (sapi/cli/php+0x601fa4) (BuildId: 8e08199ed0daa8fb60d4f6bd5b7af95330f99943)

0x51200001b229 is located 105 bytes inside of 320-byte region [0x51200001b1c0,0x51200001b300)
freed by thread T0 here:
    #0 0x69cb5a in free (sapi/cli/php+0x69cb5a) (BuildId: 8e08199ed0daa8fb60d4f6bd5b7af95330f99943)
    #1 0x1edd9b3 in __zend_free Zend/zend_alloc.c:3308:2
    #2 0x1ee653e in _efree Zend/zend_alloc.c:2747:3
    #3 0x2a26794 in zend_hash_do_resize Zend/zend_hash.c:1321:3
    #4 0x29e335a in _zend_hash_add_or_update_i Zend/zend_hash.c:873:2
    #5 0x29e1da3 in zend_hash_add_new Zend/zend_hash.c:1009:9
    #6 0x2c4b340 in zend_std_write_property Zend/zend_object_handlers.c:1179:19
    #7 0x242b99e in ZEND_ASSIGN_OBJ_SPEC_CV_CV_OP_DATA_CONST_HANDLER Zend/zend_vm_execute.h:52801:10
    #8 0x2248b78 in execute_ex Zend/zend_vm_execute.h:58486:7
    #9 0x21f3e66 in zend_call_function Zend/zend_execute_API.c:996:3
    #10 0x21f85bd in zend_call_known_function Zend/zend_execute_API.c:1090:23
    #11 0x2c72e04 in zend_call_known_instance_method Zend/zend_API.h:860:2
    #12 0x2c349db in zend_call_known_instance_method_with_0_params Zend/zend_API.h:866:2
    #13 0x2c6f76b in zend_std_cast_object_tostring Zend/zend_object_handlers.c:2322:5
    #14 0x2cccd7b in __zval_get_string_func Zend/zend_operators.c:1032:8
    #15 0x2ccc226 in zval_get_string_func Zend/zend_operators.c:1053:9
    #16 0x2cf0c0a in concat_function Zend/zend_operators.c:2014:17
    #17 0x28ed31c in zend_binary_op Zend/zend_execute.c:1651:9
    #18 0x246fcf4 in ZEND_ASSIGN_OBJ_OP_SPEC_CV_CONST_HANDLER Zend/zend_vm_execute.h:42460:7
    #19 0x2248b78 in execute_ex Zend/zend_vm_execute.h:58486:7
    #20 0x224a0cd in zend_execute Zend/zend_vm_execute.h:64138:2
    #21 0x2db1e2b in zend_execute_script Zend/zend.c:1928:3
    #22 0x18e97f1 in php_execute_script_ex main/main.c:2578:13
    #23 0x18ea108 in php_execute_script main/main.c:2618:9
    #24 0x2dbf5c0 in do_cli sapi/cli/php_cli.c:935:5
    #25 0x2dbc16f in main sapi/cli/php_cli.c:1309:18
    #26 0x7fa120b46087 in __libc_start_call_main (/lib64/libc.so.6+0x2a087) (BuildId: 77c77fee058b19c6f001cf2cb0371ce3b8341211)
    #27 0x7fa120b4614a in __libc_start_main@GLIBC_2.2.5 (/lib64/libc.so.6+0x2a14a) (BuildId: 77c77fee058b19c6f001cf2cb0371ce3b8341211)
    #28 0x601fa4 in _start (sapi/cli/php+0x601fa4) (BuildId: 8e08199ed0daa8fb60d4f6bd5b7af95330f99943)

previously allocated by thread T0 here:
    #0 0x69cdf3 in malloc (sapi/cli/php+0x69cdf3) (BuildId: 8e08199ed0daa8fb60d4f6bd5b7af95330f99943)
    #1 0x1ee6d33 in __zend_malloc Zend/zend_alloc.c:3280:14
    #2 0x1ee6336 in _emalloc Zend/zend_alloc.c:2737:10
    #3 0x29c4062 in zend_hash_real_init_mixed_ex Zend/zend_hash.c:177:10
    #4 0x29c3cb8 in zend_hash_real_init_mixed Zend/zend_hash.c:343:2
    #5 0x2c300ce in rebuild_object_properties_internal Zend/zend_object_handlers.c:74:4
    #6 0x2c33042 in zend_std_get_properties_ex Zend/zend_object_handlers.h:282:10
    #7 0x2c32f54 in zend_std_get_properties Zend/zend_object_handlers.c:136:9
    #8 0x2c4b32a in zend_std_write_property Zend/zend_object_handlers.c:1179:37
    #9 0x23ff6f1 in ZEND_ASSIGN_OBJ_SPEC_CV_CONST_OP_DATA_CONST_HANDLER Zend/zend_vm_execute.h:43350:10
    #10 0x2248b78 in execute_ex Zend/zend_vm_execute.h:58486:7
    #11 0x224a0cd in zend_execute Zend/zend_vm_execute.h:64138:2
    #12 0x2db1e2b in zend_execute_script Zend/zend.c:1928:3
    #13 0x18e97f1 in php_execute_script_ex main/main.c:2578:13
    #14 0x18ea108 in php_execute_script main/main.c:2618:9
    #15 0x2dbf5c0 in do_cli sapi/cli/php_cli.c:935:5
    #16 0x2dbc16f in main sapi/cli/php_cli.c:1309:18
    #17 0x7fa120b46087 in __libc_start_call_main (/lib64/libc.so.6+0x2a087) (BuildId: 77c77fee058b19c6f001cf2cb0371ce3b8341211)
    #18 0x7fa120b4614a in __libc_start_main@GLIBC_2.2.5 (/lib64/libc.so.6+0x2a14a) (BuildId: 77c77fee058b19c6f001cf2cb0371ce3b8341211)
    #19 0x601fa4 in _start (sapi/cli/php+0x601fa4) (BuildId: 8e08199ed0daa8fb60d4f6bd5b7af95330f99943)

SUMMARY: AddressSanitizer: heap-use-after-free Zend/zend_variables.h:42:6 in i_zval_ptr_dtor
Shadow bytes around the buggy address:
  0x51200001af80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x51200001b000: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
  0x51200001b080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x51200001b100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x51200001b180: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd fd
=>0x51200001b200: fd fd fd fd fd[fd]fd fd fd fd fd fd fd fd fd fd
  0x51200001b280: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x51200001b300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x51200001b380: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x51200001b400: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x51200001b480: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==376076==ABORTING

Here the object is released, so the address points to a freed block:

<?php

class C {
    public $a;
}

$obj = new C();
$obj->a = '';
$obj->a .= new class {
    function __toString() {
        global $obj;
        $obj = null;
        return 'str';
    }
};
ASAN
=================================================================
==376392==ERROR: AddressSanitizer: heap-use-after-free on address 0x50600001cd31 at pc 0x000002cf3def bp 0x7ffe0bf73260 sp 0x7ffe0bf73258
READ of size 1 at 0x50600001cd31 thread T0
    #0 0x2cf3dee in i_zval_ptr_dtor Zend/zend_variables.h:42:6
    #1 0x2cf0f30 in concat_function Zend/zend_operators.c:2031:5
    #2 0x28ed31c in zend_binary_op Zend/zend_execute.c:1651:9
    #3 0x246fcf4 in ZEND_ASSIGN_OBJ_OP_SPEC_CV_CONST_HANDLER Zend/zend_vm_execute.h:42460:7
    #4 0x2248b78 in execute_ex Zend/zend_vm_execute.h:58486:7
    #5 0x224a0cd in zend_execute Zend/zend_vm_execute.h:64138:2
    #6 0x2db1e2b in zend_execute_script Zend/zend.c:1928:3
    #7 0x18e97f1 in php_execute_script_ex main/main.c:2578:13
    #8 0x18ea108 in php_execute_script main/main.c:2618:9
    #9 0x2dbf5c0 in do_cli sapi/cli/php_cli.c:935:5
    #10 0x2dbc16f in main sapi/cli/php_cli.c:1309:18
    #11 0x7f36a037e087 in __libc_start_call_main (/lib64/libc.so.6+0x2a087) (BuildId: 77c77fee058b19c6f001cf2cb0371ce3b8341211)
    #12 0x7f36a037e14a in __libc_start_main@GLIBC_2.2.5 (/lib64/libc.so.6+0x2a14a) (BuildId: 77c77fee058b19c6f001cf2cb0371ce3b8341211)
    #13 0x601fa4 in _start (sapi/cli/php+0x601fa4) (BuildId: 8e08199ed0daa8fb60d4f6bd5b7af95330f99943)

0x50600001cd31 is located 49 bytes inside of 56-byte region [0x50600001cd00,0x50600001cd38)
freed by thread T0 here:
    #0 0x69cb5a in free (sapi/cli/php+0x69cb5a) (BuildId: 8e08199ed0daa8fb60d4f6bd5b7af95330f99943)
    #1 0x1edd9b3 in __zend_free Zend/zend_alloc.c:3308:2
    #2 0x1ee653e in _efree Zend/zend_alloc.c:2747:3
    #3 0x2c79eda in zend_objects_store_del Zend/zend_objects_API.c:198:3
    #4 0x2d70b92 in rc_dtor_func Zend/zend_variables.c:57:2
    #5 0x28dca40 in zend_assign_to_variable Zend/zend_execute.h:178:4
    #6 0x22ffaa7 in ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER Zend/zend_vm_execute.h:44465:11
    #7 0x2248b78 in execute_ex Zend/zend_vm_execute.h:58486:7
    #8 0x21f3e66 in zend_call_function Zend/zend_execute_API.c:996:3
    #9 0x21f85bd in zend_call_known_function Zend/zend_execute_API.c:1090:23
    #10 0x2c72e04 in zend_call_known_instance_method Zend/zend_API.h:860:2
    #11 0x2c349db in zend_call_known_instance_method_with_0_params Zend/zend_API.h:866:2
    #12 0x2c6f76b in zend_std_cast_object_tostring Zend/zend_object_handlers.c:2322:5
    #13 0x2cccd7b in __zval_get_string_func Zend/zend_operators.c:1032:8
    #14 0x2ccc226 in zval_get_string_func Zend/zend_operators.c:1053:9
    #15 0x2cf0c0a in concat_function Zend/zend_operators.c:2014:17
    #16 0x28ed31c in zend_binary_op Zend/zend_execute.c:1651:9
    #17 0x246fcf4 in ZEND_ASSIGN_OBJ_OP_SPEC_CV_CONST_HANDLER Zend/zend_vm_execute.h:42460:7
    #18 0x2248b78 in execute_ex Zend/zend_vm_execute.h:58486:7
    #19 0x224a0cd in zend_execute Zend/zend_vm_execute.h:64138:2
    #20 0x2db1e2b in zend_execute_script Zend/zend.c:1928:3
    #21 0x18e97f1 in php_execute_script_ex main/main.c:2578:13
    #22 0x18ea108 in php_execute_script main/main.c:2618:9
    #23 0x2dbf5c0 in do_cli sapi/cli/php_cli.c:935:5
    #24 0x2dbc16f in main sapi/cli/php_cli.c:1309:18
    #25 0x7f36a037e087 in __libc_start_call_main (/lib64/libc.so.6+0x2a087) (BuildId: 77c77fee058b19c6f001cf2cb0371ce3b8341211)
    #26 0x7f36a037e14a in __libc_start_main@GLIBC_2.2.5 (/lib64/libc.so.6+0x2a14a) (BuildId: 77c77fee058b19c6f001cf2cb0371ce3b8341211)
    #27 0x601fa4 in _start (sapi/cli/php+0x601fa4) (BuildId: 8e08199ed0daa8fb60d4f6bd5b7af95330f99943)

previously allocated by thread T0 here:
    #0 0x69cdf3 in malloc (sapi/cli/php+0x69cdf3) (BuildId: 8e08199ed0daa8fb60d4f6bd5b7af95330f99943)
    #1 0x1ee6d33 in __zend_malloc Zend/zend_alloc.c:3280:14
    #2 0x1ee6336 in _emalloc Zend/zend_alloc.c:2737:10
    #3 0x2c7fba6 in zend_objects_new Zend/zend_objects.c:210:24
    #4 0x1f229ef in _object_and_properties_init Zend/zend_API.c:1823:22
    #5 0x1f231b0 in object_init_ex Zend/zend_API.c:1846:9
    #6 0x25278f7 in ZEND_NEW_SPEC_CONST_UNUSED_HANDLER Zend/zend_vm_execute.h:10895:6
    #7 0x2248b78 in execute_ex Zend/zend_vm_execute.h:58486:7
    #8 0x224a0cd in zend_execute Zend/zend_vm_execute.h:64138:2
    #9 0x2db1e2b in zend_execute_script Zend/zend.c:1928:3
    #10 0x18e97f1 in php_execute_script_ex main/main.c:2578:13
    #11 0x18ea108 in php_execute_script main/main.c:2618:9
    #12 0x2dbf5c0 in do_cli sapi/cli/php_cli.c:935:5
    #13 0x2dbc16f in main sapi/cli/php_cli.c:1309:18
    #14 0x7f36a037e087 in __libc_start_call_main (/lib64/libc.so.6+0x2a087) (BuildId: 77c77fee058b19c6f001cf2cb0371ce3b8341211)
    #15 0x7f36a037e14a in __libc_start_main@GLIBC_2.2.5 (/lib64/libc.so.6+0x2a14a) (BuildId: 77c77fee058b19c6f001cf2cb0371ce3b8341211)
    #16 0x601fa4 in _start (sapi/cli/php+0x601fa4) (BuildId: 8e08199ed0daa8fb60d4f6bd5b7af95330f99943)

SUMMARY: AddressSanitizer: heap-use-after-free Zend/zend_variables.h:42:6 in i_zval_ptr_dtor
Shadow bytes around the buggy address:
  0x50600001ca80: 00 00 00 00 fa fa fa fa 00 00 00 00 00 00 00 00
  0x50600001cb00: fa fa fa fa 00 00 00 00 00 00 00 00 fa fa fa fa
  0x50600001cb80: 00 00 00 00 00 00 00 00 fa fa fa fa 00 00 00 00
  0x50600001cc00: 00 00 00 00 fa fa fa fa fd fd fd fd fd fd fd fd
  0x50600001cc80: fa fa fa fa 00 00 00 00 00 00 00 00 fa fa fa fa
=>0x50600001cd00: fd fd fd fd fd fd[fd]fa fa fa fa fa fa fa fa fa
  0x50600001cd80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50600001ce00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50600001ce80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50600001cf00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50600001cf80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==376392==ABORTING

Using ReflectionClass::resetAsLazy*() in __toString() in these cases will also cause similar issues.

This issue exists with ASSIGN_OBJ_OP, ASSIGN_OBJ_REF, maybe ASSIGN_(PRE|POST)_(INC|DEC). I think we are aware of similar issues, but I'm not sure we were aware of these cases particularly.

The 3rd case may be resolved with a addref()/delref() of the object like we do in

// Increase refcount to prevent object from being released in __toString()

But the two others are trickier. If conversion of the value is the only way to trigger a side effect after acquisition of the address, maybe we can ensure this occurs before that. However we don't always know the required conversion before fetching the property. Or can we prevent any operation on the object, with a flag or by setting zend_object.handlers to error-only handlers temporarily?

PHP Version

master

Operating System

No response

@nielsdos
Copy link
Member

@arnaud-lb see also discussion of #13754

@iluuu1994
Copy link
Member

Indeed related, but cannot be solved in the same way as #13754 as no error handlers are involved. The same problem would generally exist for $foo->bar->baz = qux();, but we solve this by evaluating qux() before $foo->bar. It's conceivable to do the same here, i.e. perform a string coercion into a TMP, along with possible side-effects, and only then perform the offset/property fetch. This may affect performance though.

@iluuu1994
Copy link
Member

iluuu1994 commented Sep 19, 2024

Very simple PoC.

Details

diff --git a/Zend/tests/gh15938_001.phpt b/Zend/tests/gh15938_001.phpt
new file mode 100644
index 0000000000..c904c51b07
--- /dev/null
+++ b/Zend/tests/gh15938_001.phpt
@@ -0,0 +1,24 @@
+--TEST--
+GH-15938
+--FILE--
+<?php
+
+#[AllowDynamicProperties]
+class C {}
+
+$obj = new C();
+$obj->a = str_repeat('a', 10);
+$obj->a .= new class {
+    function __toString() {
+        global $obj;
+        unset($obj->a);
+        return str_repeat('c', 10);
+    }
+};
+
+var_dump($obj->a);
+
+?>
+--EXPECTF--
+Warning: Undefined property: C::$a in %s on line %d
+string(10) "cccccccccc"
diff --git a/Zend/tests/gh15938_002.phpt b/Zend/tests/gh15938_002.phpt
new file mode 100644
index 0000000000..3a98b9ae87
--- /dev/null
+++ b/Zend/tests/gh15938_002.phpt
@@ -0,0 +1,44 @@
+--TEST--
+GH-15938
+--FILE--
+<?php
+
+#[AllowDynamicProperties]
+class C {}
+
+$obj = new C();
+$obj->a = '';
+$obj->a .= new class {
+    function __toString() {
+        global $obj;
+        for ($i = 0; $i < 8; $i++) {
+            $obj->{$i} = 0;
+        }
+        return 'str';
+    }
+};
+
+var_dump($obj);
+
+?>
+--EXPECTF--
+object(C)#%d (9) {
+  ["a"]=>
+  string(3) "str"
+  ["0"]=>
+  int(0)
+  ["1"]=>
+  int(0)
+  ["2"]=>
+  int(0)
+  ["3"]=>
+  int(0)
+  ["4"]=>
+  int(0)
+  ["5"]=>
+  int(0)
+  ["6"]=>
+  int(0)
+  ["7"]=>
+  int(0)
+}
diff --git a/Zend/tests/gh15938_003.phpt b/Zend/tests/gh15938_003.phpt
new file mode 100644
index 0000000000..120697c1d8
--- /dev/null
+++ b/Zend/tests/gh15938_003.phpt
@@ -0,0 +1,25 @@
+--TEST--
+GH-15938
+--FILE--
+<?php
+
+class C {
+    public $a;
+}
+
+$obj = new C();
+$obj->a = '';
+$obj->a .= new class {
+    function __toString() {
+        global $obj;
+        $obj = null;
+        return 'str';
+    }
+};
+
+?>
+--EXPECTF--
+Fatal error: Uncaught Error: Attempt to assign property "a" on null in %s:%d
+Stack trace:
+#0 {main}
+  thrown in %s on line %d
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index 63787c902f..68ec9b9d43 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -3683,6 +3683,12 @@ static void zend_compile_compound_assign(znode *result, zend_ast *ast) /* {{{ */
 			offset = zend_delayed_compile_begin();
 			zend_delayed_compile_prop(result, var_ast, BP_VAR_RW);
 			zend_compile_expr(&expr_node, expr_ast);
+			/* If the expression may contain objects and cause side-effects, emit
+			 * a cast before compiling the lhs W fetches. */
+			if (opcode == ZEND_CONCAT && expr_ast->kind != ZEND_AST_ZVAL) {
+				zend_op *cast_opline = zend_emit_op_tmp(&expr_node, ZEND_CAST, &expr_node, NULL);
+				cast_opline->extended_value = IS_STRING;
+			}
 
 			opline = zend_delayed_compile_end(offset);
 			cache_slot = opline->extended_value;

The same is likely needed for DIM, STATIC_PROP and VAR. Each could at least leak memory, if not crash.

iluuu1994 added a commit to iluuu1994/php-src that referenced this issue Sep 19, 2024
iluuu1994 added a commit to iluuu1994/php-src that referenced this issue Sep 19, 2024
iluuu1994 added a commit to iluuu1994/php-src that referenced this issue Sep 19, 2024
Neither STATIC_PROP nor VAR are susceptible. Static vars cannot be unset.
Unsetting vars only sets them to IS_UNDEF, including dynamic properties
(`zend_hash_del_ind()` in the `ZEND_UNSET_VAR` handler).

Fixes phpGH-15938
iluuu1994 added a commit to iluuu1994/php-src that referenced this issue Sep 19, 2024
Neither STATIC_PROP nor VAR are susceptible. Static vars cannot be unset.
Unsetting vars only sets them to IS_UNDEF, including dynamic properties
(`zend_hash_del_ind()` in the `ZEND_UNSET_VAR` handler).

Fixes phpGH-15938
@iluuu1994 iluuu1994 linked a pull request Sep 19, 2024 that will close this issue
iluuu1994 added a commit to iluuu1994/php-src that referenced this issue Sep 19, 2024
Neither STATIC_PROP nor VAR are susceptible. Static vars cannot be unset.
Unsetting vars only sets them to IS_UNDEF, including dynamic properties
(`zend_hash_del_ind()` in the `ZEND_UNSET_VAR` handler).

Fixes phpGH-15938
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants