Skip to content

Heap buffer overflow in zval_undefined_cv() via invalid CV dimension unset during destructor #20403

Open

Description

@vi3tL0u1s

Description

The following code:

class test
{
    static x$instances;

    public function __construct($id) {
        $this->_id = $id;
    }

    function __destruct() {
        unset(self::$instances[$x]->x);
    }
}
$test = new test(1);
x ^ new test(1);

Resulted in this output:

=================================================================
==354402==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000003838 at pc 0x55c9627dd661 bp 0x7fffd13e6510 sp 0x7fffd13e6508
READ of size 8 at 0x602000003838 thread T0
    #0 0x55c9627dd660 in zval_undefined_cv /path/to/php-src/Zend/zend_execute.c:280:21
    #1 0x55c9627dc190 in _zval_undefined_op1 /path/to/php-src/Zend/zend_execute.c:288:9
    #2 0x55c9628019e4 in zend_fetch_dimension_address /path/to/php-src/Zend/zend_execute.c:3000:5
    #3 0x55c9628025b3 in zend_fetch_dimension_address_UNSET /path/to/php-src/Zend/zend_execute.c:3054:2
    #4 0x55c9626f2d8c in ZEND_FETCH_DIM_UNSET_SPEC_VAR_CV_HANDLER /path/to/php-src/Zend/zend_vm_execute.h:32041:2
    #5 0x55c9625abf42 in execute_ex /path/to/php-src/Zend/zend_vm_execute.h:116212:12
    #6 0x55c962586571 in zend_call_function /path/to/php-src/Zend/zend_execute_API.c:1014:3
    #7 0x55c962588782 in zend_call_known_function /path/to/php-src/Zend/zend_execute_API.c:1108:23
    #8 0x55c96295ba5a in zend_call_known_instance_method /path/to/php-src/Zend/zend_API.h:862:2
    #9 0x55c9629592bb in zend_call_known_instance_method_with_0_params /path/to/php-src/Zend/zend_API.h:868:2
    #10 0x55c962958cb1 in zend_objects_destroy_object /path/to/php-src/Zend/zend_objects.c:172:3
    #11 0x55c962956812 in zend_objects_store_del /path/to/php-src/Zend/zend_objects_API.c:181:4
    #12 0x55c9629c01d6 in rc_dtor_func /path/to/php-src/Zend/zend_variables.c:57:2
    #13 0x55c9629c02c4 in i_zval_ptr_dtor /path/to/php-src/Zend/zend_variables.h:45:4
    #14 0x55c9629c0214 in zval_ptr_dtor /path/to/php-src/Zend/zend_variables.c:84:2
    #15 0x55c96284c759 in _zend_hash_del_el_ex /path/to/php-src/Zend/zend_hash.c:1493:3
    #16 0x55c96284b82a in _zend_hash_del_el /path/to/php-src/Zend/zend_hash.c:1520:2
    #17 0x55c96285478a in zend_hash_reverse_apply /path/to/php-src/Zend/zend_hash.c:2236:5
    #18 0x55c96257b410 in shutdown_destructors /path/to/php-src/Zend/zend_execute_API.c:262:4
    #19 0x55c9629d931e in zend_call_destructors /path/to/php-src/Zend/zend.c:1336:3
    #20 0x55c9621da74a in php_request_shutdown /path/to/php-src/main/main.c:1980:3
    #21 0x55c9629ea00b in do_cli /path/to/php-src/sapi/cli/php_cli.c:1158:3
    #22 0x55c9629e4e2c in main /path/to/php-src/sapi/cli/php_cli.c:1362:18
    #23 0x7f4533006d8f  (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f) (BuildId: 4f7b0c955c3d81d7cac1501a2498b69d1d82bfe7)
    #24 0x7f4533006e3f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x29e3f) (BuildId: 4f7b0c955c3d81d7cac1501a2498b69d1d82bfe7)
    #25 0x55c960e03374 in _start (/path/to/php-src/sapi/cli/php+0x603374) (BuildId: 7e30eed09f929fbd58cc0b481c8117cf438919d5)

0x602000003838 is located 0 bytes to the right of 8-byte region [0x602000003830,0x602000003838)
allocated by thread T0 here:
    #0 0x55c960e865e6 in __interceptor_realloc (/path/to/php-src/sapi/cli/php+0x6865e6) (BuildId: 7e30eed09f929fbd58cc0b481c8117cf438919d5)
    #1 0x55c96242f29b in __zend_realloc /path/to/php-src/Zend/zend_alloc.c:3562:6
    #2 0x55c96242eccc in _erealloc /path/to/php-src/Zend/zend_alloc.c:2801:10
    #3 0x55c962969a6f in pass_two /path/to/php-src/Zend/zend_opcode.c:1069:36
    #4 0x55c9624f9437 in zend_compile_func_decl_ex /path/to/php-src/Zend/zend_compile.c:8721:2
    #5 0x55c9624ef79c in zend_compile_func_decl /path/to/php-src/Zend/zend_compile.c:8743:9
    #6 0x55c9624f232a in zend_compile_stmt /path/to/php-src/Zend/zend_compile.c:11853:4
    #7 0x55c96250ed91 in zend_compile_stmt_list /path/to/php-src/Zend/zend_compile.c:7143:3
    #8 0x55c9624f2200 in zend_compile_stmt /path/to/php-src/Zend/zend_compile.c:11800:4
    #9 0x55c9624f0a42 in zend_compile_class_decl /path/to/php-src/Zend/zend_compile.c:9448:2
    #10 0x55c9624ef61b in zend_compile_top_stmt /path/to/php-src/Zend/zend_compile.c:11775:3
    #11 0x55c9624ef443 in zend_compile_top_stmt /path/to/php-src/Zend/zend_compile.c:11764:4
    #12 0x55c9628e1dbf in zend_compile /path/to/php-src/Zend/zend_language_scanner.l:621:3
    #13 0x55c9628e17e7 in compile_file /path/to/php-src/Zend/zend_language_scanner.l:661:14
    #14 0x55c961bc7d37 in phar_compile_file /path/to/php-src/ext/phar/phar.c:3236:9
    #15 0x55c9629dff39 in zend_execute_script /path/to/php-src/Zend/zend.c:1968:28
    #16 0x55c9621e07ab in php_execute_script_ex /path/to/php-src/main/main.c:2640:13
    #17 0x55c9621e0ca8 in php_execute_script /path/to/php-src/main/main.c:2680:9
    #18 0x55c9629e7ed2 in do_cli /path/to/php-src/sapi/cli/php_cli.c:951:5
    #19 0x55c9629e4e2c in main /path/to/php-src/sapi/cli/php_cli.c:1362:18
    #20 0x7f4533006d8f  (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f) (BuildId: 4f7b0c955c3d81d7cac1501a2498b69d1d82bfe7)

SUMMARY: AddressSanitizer: heap-buffer-overflow /path/to/php-src/Zend/zend_execute.c:280:21 in zval_undefined_cv
Shadow bytes around the buggy address:
  0x0c047fff86b0: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fd
  0x0c047fff86c0: fa fa fd fd fa fa fd fd fa fa 00 04 fa fa 00 05
  0x0c047fff86d0: fa fa 00 05 fa fa fd fd fa fa fd fd fa fa fd fd
  0x0c047fff86e0: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fa
  0x0c047fff86f0: fa fa 00 00 fa fa 04 fa fa fa 00 fa fa fa fd fa
=>0x0c047fff8700: fa fa 04 fa fa fa 00[fa]fa fa fd fa fa fa fd fa
  0x0c047fff8710: fa fa fd fd fa fa 00 00 fa fa fa fa fa fa fa fa
  0x0c047fff8720: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8730: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8740: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8750: 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
==354402==ABORTING

Commit:

d3c5163c25c0bc82b72af2f1b09e9d85d92c3668

Build configuration:

CC="clang" CXX="clang++" CFLAGS="-fsanitize=address -g -O0" CXXFLAGS="-fsanitize=address -g -O0" ./configure --enable-debug --enable-address-sanitizer --disable-shared --with-pic

PHP Version

PHP 8.6.0-dev (cli) (built: Nov  6 2025 19:53:33) (NTS DEBUG)
Copyright (c) The PHP Group
Zend Engine v4.6.0-dev, Copyright (c) Zend Technologies
    with Zend OPcache v8.6.0-dev, Copyright (c), by Zend Technologies

Operating System

Ubuntu 22.04

Activity

  1. arnaud-lb commented on Nov 6, 2025

    @arnaud-lb
    Member

    Smaller reproducer:

    class test
    {
        static x $instances;
    }
    
    $test = new test;
    unset($test::$instances[$x]->x);

    This branch is entered:

    php-src/Zend/zend_execute.c

    Lines 2999 to 3000 in 7815ab9

    if (type != BP_VAR_W && UNEXPECTED(Z_TYPE_P(container) == IS_UNDEF)) {
    ZVAL_UNDEFINED_OP1();

    But OP1 is not a CV, so we get invalid memory accesses when trying to fetch a CV name.

    Interestingly, the following code will trigger "Warning: Undefined variable $foo", but this looks incorrect:

    unset($foo[$x]->x);
  2. arnaud-lb commented on Nov 6, 2025

    @arnaud-lb
    Member

    Possible fix would be to evaluate ZVAL_UNDEFINED_OP1() only when type is BP_VAR_R|BP_VAR_RW:

    diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c
    index d411dcbc3b9..81373fa6d13 100644
    --- a/Zend/zend_execute.c
    +++ b/Zend/zend_execute.c
    @@ -2996,7 +2996,7 @@ static zend_always_inline void zend_fetch_dimension_address(zval *result, zval *
                    }
            } else {
                    if (EXPECTED(Z_TYPE_P(container) <= IS_FALSE)) {
    -                       if (type != BP_VAR_W && UNEXPECTED(Z_TYPE_P(container) == IS_UNDEF)) {
    +                       if ((type == BP_VAR_R || type == BP_VAR_RW) && UNEXPECTED(Z_TYPE_P(container) == IS_UNDEF)) {
                                    ZVAL_UNDEFINED_OP1();
                            }
                            if (type != BP_VAR_UNSET) {

    Rationale:

    • The warning should only be triggered in R/RW modes
    • I believe that in these modes, Z_TYPE_P(container) == IS_UNDEF is only possible when OP1 is a CV. In other modes, an exception would have been thrown, or container would be NULL, not UNDEF.
  3. iluuu1994 commented on Nov 6, 2025

    @iluuu1994
    Member

    I think that's just another duplicate of #19160, which I should have merged long ago. I wanted to check the logic again but didn't get around to it.

  4. Truta446 commented on Oct 27, 2026

    @Truta446

    This still reproduces on PHP-8.4 and PHP-8.5 (heap-buffer-overflow in zval_undefined_cv() with the smaller reproducer above); master and PHP-8.6 are fixed by 8b4ef3a (GH-19160), which was only merged into master. I opened #24788 to backport it to PHP-8.4, with a regression test.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions