Skip to content

Heap buffer overflow in property hooks (zho_build_properties_ex) via re-entrant getter during var_export() #20479

Closed

Description

@vi3tL0u1s

Description

The following code:

<?php
#[AllowDynamicProperties]
class ByRef {
    private $_virtualByRef = 'virtualByRef';
    public $virtualByRef {
        &get {
          return $this->_virtualByRef;
        }
        set {
          $this->_virtualByRef = $value;
        }
    }
    public $virtualSetOnly {
        set {
        }
    }
    public function __construct() {
        $this->undef = 'dynamic';
        $this->dynamic = 'dynamic';
    }
}
#[AllowDynamicProperties]
class ByVal extends ByRef {
    private $_virtualByVal = 'virtualByVal';
    public $virtualByVal {
        get {
          return $this->_virtualByVal;
        }
        set {
          $this->_virtualByVal = $value;
        }
    }
    public $backed = 'backed' {
        get {
          return $this->backed;
        }
        set {
          $this->backed = $value;
        }
    }
    public string $backedUninixialized {
        get {
            $this->_this = $this->_thisx= $this;
            $a[0] =//////
            var_export($this);
            return $this->backedUninitialized;
        }
        set {
            $this->backedUninitialized = $value;
        }
    }
}
function testByVal($object) {
    foreach ($object as $prop => $value) {
        $object->{$prop}!= strtoupper($value);
    }
    var_dump($object);
}

testByVal(new ByVal);
testByVal(new ByRef);

Resulted in this output:

=================================================================
==2914108==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x61200002f580 at pc 0x558dee39b4fe bp 0x7ffd74ab0f50 sp 0x7ffd74ab0f48
WRITE of size 8 at 0x61200002f580 thread T0
    #0 0x558dee39b4fd in _zend_hash_append_ex /path/to/php-src/Zend/zend_hash.h:1641:2
    #1 0x558dee39afa6 in _zend_hash_append /path/to/php-src/Zend/zend_hash.h:1659:9
    #2 0x558dee39a060 in zho_build_properties_ex /path/to/php-src/Zend/zend_property_hooks.c:124:16
    #3 0x558dee39921c in zend_hooked_object_build_properties /path/to/php-src/Zend/zend_property_hooks.c:141:9
    #4 0x558dee3539e6 in zend_std_get_properties_for /path/to/php-src/Zend/zend_object_handlers.c:2508:12
    #5 0x558dee353e67 in zend_get_properties_for /path/to/php-src/Zend/zend_object_handlers.c:2546:9
    #6 0x558dedad55ce in php_var_export_ex /path/to/php-src/ext/standard/var.c:603:11
    #7 0x558dedad7b58 in zif_var_export /path/to/php-src/ext/standard/var.c:707:23
    #8 0x558dee140bd1 in ZEND_DO_ICALL_SPEC_RETVAL_USED_HANDLER /path/to/php-src/Zend/zend_vm_execute.h:1421:2
    #9 0x558dedfabf42 in execute_ex /path/to/php-src/Zend/zend_vm_execute.h:116212:12
    #10 0x558dedf86571 in zend_call_function /path/to/php-src/Zend/zend_execute_API.c:1014:3
    #11 0x558dedf88782 in zend_call_known_function /path/to/php-src/Zend/zend_execute_API.c:1108:23
    #12 0x558dee3546ea in zend_call_known_instance_method /path/to/php-src/Zend/zend_API.h:862:2
    #13 0x558dee33a7fb in zend_call_known_instance_method_with_0_params /path/to/php-src/Zend/zend_API.h:868:2
    #14 0x558dee340bfe in zend_call_get_hook /path/to/php-src/Zend/zend_object_handlers.c:733:2
    #15 0x558dee33e731 in zend_std_read_property /path/to/php-src/Zend/zend_object_handlers.c:847:8
    #16 0x558dede7242b in zend_read_property_ex /path/to/php-src/Zend/zend_API.c:5122:10
    #17 0x558dee39cd5e in zho_declared_it_fetch_current /path/to/php-src/Zend/zend_property_hooks.c:179:17
    #18 0x558dee39c7b8 in zho_it_fetch_current /path/to/php-src/Zend/zend_property_hooks.c:262:4
    #19 0x558dee39bc4c in zho_it_valid /path/to/php-src/Zend/zend_property_hooks.c:288:2
    #20 0x558dee1f80d4 in zend_fe_fetch_object_helper_SPEC /path/to/php-src/Zend/zend_vm_execute.h:3102:8
    #21 0x558dee0c44e9 in ZEND_FE_FETCH_R_SPEC_VAR_HANDLER /path/to/php-src/Zend/zend_vm_execute.h:23604:3
    #22 0x558dedfabf42 in execute_ex /path/to/php-src/Zend/zend_vm_execute.h:116212:12
    #23 0x558dedfac847 in zend_execute /path/to/php-src/Zend/zend_vm_execute.h:121924:2
    #24 0x558dee3dffd0 in zend_execute_script /path/to/php-src/Zend/zend.c:1975:3
    #25 0x558dedbe07ab in php_execute_script_ex /path/to/php-src/main/main.c:2640:13
    #26 0x558dedbe0ca8 in php_execute_script /path/to/php-src/main/main.c:2680:9
    #27 0x558dee3e7ed2 in do_cli /path/to/php-src/sapi/cli/php_cli.c:951:5
    #28 0x558dee3e4e2c in main /path/to/php-src/sapi/cli/php_cli.c:1362:18
    #29 0x7fdf152f7d8f  (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f) (BuildId: 4f7b0c955c3d81d7cac1501a2498b69d1d82bfe7)
    #30 0x7fdf152f7e3f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x29e3f) (BuildId: 4f7b0c955c3d81d7cac1501a2498b69d1d82bfe7)
    #31 0x558dec803374 in _start (/path/to/php-src/sapi/cli/php+0x603374) (BuildId: 7e30eed09f929fbd58cc0b481c8117cf438919d5)

0x61200002f580 is located 0 bytes to the right of 320-byte region [0x61200002f440,0x61200002f580)
allocated by thread T0 here:
    #0 0x558dec8861be in malloc (/path/to/php-src/sapi/cli/php+0x6861be) (BuildId: 7e30eed09f929fbd58cc0b481c8117cf438919d5)
    #1 0x558dede2f143 in __zend_malloc /path/to/php-src/Zend/zend_alloc.c:3543:14
    #2 0x558dede2ead0 in _emalloc /path/to/php-src/Zend/zend_alloc.c:2780:10
    #3 0x558dee23a1ff in zend_hash_real_init_mixed_ex /path/to/php-src/Zend/zend_hash.c:176:10
    #4 0x558dee23a037 in zend_hash_real_init_mixed /path/to/php-src/Zend/zend_hash.c:342:2
    #5 0x558dee3993b4 in zho_build_properties_ex /path/to/php-src/Zend/zend_property_hooks.c:61:2
    #6 0x558dee39921c in zend_hooked_object_build_properties /path/to/php-src/Zend/zend_property_hooks.c:141:9
    #7 0x558dee3539e6 in zend_std_get_properties_for /path/to/php-src/Zend/zend_object_handlers.c:2508:12
    #8 0x558dee353e67 in zend_get_properties_for /path/to/php-src/Zend/zend_object_handlers.c:2546:9
    #9 0x558dedad55ce in php_var_export_ex /path/to/php-src/ext/standard/var.c:603:11
    #10 0x558dedad7b58 in zif_var_export /path/to/php-src/ext/standard/var.c:707:23
    #11 0x558dee140bd1 in ZEND_DO_ICALL_SPEC_RETVAL_USED_HANDLER /path/to/php-src/Zend/zend_vm_execute.h:1421:2
    #12 0x558dedfabf42 in execute_ex /path/to/php-src/Zend/zend_vm_execute.h:116212:12
    #13 0x558dedf86571 in zend_call_function /path/to/php-src/Zend/zend_execute_API.c:1014:3
    #14 0x558dedf88782 in zend_call_known_function /path/to/php-src/Zend/zend_execute_API.c:1108:23
    #15 0x558dee3546ea in zend_call_known_instance_method /path/to/php-src/Zend/zend_API.h:862:2
    #16 0x558dee33a7fb in zend_call_known_instance_method_with_0_params /path/to/php-src/Zend/zend_API.h:868:2
    #17 0x558dee340bfe in zend_call_get_hook /path/to/php-src/Zend/zend_object_handlers.c:733:2
    #18 0x558dee33e731 in zend_std_read_property /path/to/php-src/Zend/zend_object_handlers.c:847:8
    #19 0x558dede7242b in zend_read_property_ex /path/to/php-src/Zend/zend_API.c:5122:10
    #20 0x558dee39cd5e in zho_declared_it_fetch_current /path/to/php-src/Zend/zend_property_hooks.c:179:17
    #21 0x558dee39c7b8 in zho_it_fetch_current /path/to/php-src/Zend/zend_property_hooks.c:262:4
    #22 0x558dee39bc4c in zho_it_valid /path/to/php-src/Zend/zend_property_hooks.c:288:2
    #23 0x558dee1f80d4 in zend_fe_fetch_object_helper_SPEC /path/to/php-src/Zend/zend_vm_execute.h:3102:8
    #24 0x558dee0c44e9 in ZEND_FE_FETCH_R_SPEC_VAR_HANDLER /path/to/php-src/Zend/zend_vm_execute.h:23604:3
    #25 0x558dedfabf42 in execute_ex /path/to/php-src/Zend/zend_vm_execute.h:116212:12
    #26 0x558dedfac847 in zend_execute /path/to/php-src/Zend/zend_vm_execute.h:121924:2
    #27 0x558dee3dffd0 in zend_execute_script /path/to/php-src/Zend/zend.c:1975:3
    #28 0x558dedbe07ab in php_execute_script_ex /path/to/php-src/main/main.c:2640:13
    #29 0x558dedbe0ca8 in php_execute_script /path/to/php-src/main/main.c:2680:9

SUMMARY: AddressSanitizer: heap-buffer-overflow /path/to/php-src/Zend/zend_hash.h:1641:2 in _zend_hash_append_ex
Shadow bytes around the buggy address:
  0x0c247fffde60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c247fffde70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c247fffde80: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
  0x0c247fffde90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c247fffdea0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c247fffdeb0:[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c247fffdec0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c247fffded0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c247fffdee0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c247fffdef0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c247fffdf00: 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
==2914108==ABORTING

Commit:

9cd367362da5442861f30d3b41e967d641b90cbd

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 14 2025 16:07:53) (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. devnexen commented on Nov 14, 2025

    @devnexen
    Member

    Reproduced with USE_ZEND_ALLOC=0, a bug in zend mm happens instead if set as 1.

  2. ndossche commented on Nov 14, 2025

    @ndossche
    Member

    Reduced:

    <?php
    #[AllowDynamicProperties]
    class Trigger {
        public $a = 'x';
        public $b = 'x';
        public $c = 'x';
        public $d = 'x';
        public $e = 'x';
        public $f = 'x';
        public string $trigger {
          get {
            return 'trigger';
          }
        }
    }
    
    $obj = new Trigger();
    // Add 2 dynamic props
    $obj->g = $obj->h = 'x';
    var_export($obj);
  3. ndossche commented on Nov 15, 2025

    @ndossche
    Member

    I think the logic error is that zho_build_properties_ex computes too few number of required buckets. It does so by looking at the number of elements in the properties hash table; however this won't include the entry for trigger because it doesn't have a backing value.
    EDIT: There might also be an issue in how UNDEF properties may be handled, because the property table building code skips those but the hooks code adds INDIRECTs to those.

  4. ndossche commented on Nov 15, 2025

    @ndossche
    Member

    Assuming the semantics are right, we'd either need to compute the number of buckets precisely a priori, or we can just do this:

    diff --git a/Zend/zend_property_hooks.c b/Zend/zend_property_hooks.c
    index 01a8afb1693..fc50a9671e7 100644
    --- a/Zend/zend_property_hooks.c
    +++ b/Zend/zend_property_hooks.c
    @@ -121,7 +121,7 @@ static zend_array *zho_build_properties_ex(zend_object *zobj, bool check_access,
     			if (Z_TYPE_P(prop_value) == IS_INDIRECT) {
     				continue;
     			}
    -			zval *tmp = _zend_hash_append(properties, prop_name, prop_value);
    +			zval *tmp = zend_hash_add_new(properties, prop_name, prop_value);
     			Z_TRY_ADDREF_P(tmp);
     		} ZEND_HASH_FOREACH_END();
     	}
    

    Using add instead of append is slower but less error-prone.

    My gut feeling also tells me we need to do this (see EDIT in previous comment):

    diff --git a/Zend/zend_property_hooks.c b/Zend/zend_property_hooks.c
    index 01a8afb1693..f7abebd7342 100644
    --- a/Zend/zend_property_hooks.c
    +++ b/Zend/zend_property_hooks.c
    @@ -100,7 +100,8 @@ static zend_array *zho_build_properties_ex(zend_object *zobj, bool check_access,
     				zend_hash_update_ptr(properties, property_name, prop_info);
     			} else {
     				if (UNEXPECTED(Z_TYPE_P(OBJ_PROP(zobj, prop_info->offset)) == IS_UNDEF)) {
    -					HT_FLAGS(properties) |= HASH_FLAG_HAS_EMPTY_IND;
    +					// HT_FLAGS(properties) |= HASH_FLAG_HAS_EMPTY_IND;
    +					goto skip_property;
     				}
     				zval *tmp = zend_hash_lookup(properties, property_name);
     				ZVAL_INDIRECT(tmp, OBJ_PROP(zobj, prop_info->offset));
    

    cc @iluuu1994

  5. iluuu1994 commented on Jan 21, 2026

    @iluuu1994
    Member

    Sorry for the slight delay. 😅 The first part looks ok to me. The indirect part is actually important because it maintains the correct order for child properties where offset changes (recall GH-17870). I didn't quite understand what you meant by "property table building code skips those", I'm not sure if you're referring to if (Z_TYPE_P(prop_value) == IS_INDIRECT) { continue; } just below, but the intention here is to skip declared properties and only add dynamic properties, which don't have an INDIRECT indirection. Do you want to create a PR? Edit: Nevermind, I'll create one for you. Edit 2: #20988

  6. ndossche commented on Jan 21, 2026

    @ndossche
    Member

    The indirect part is actually important because it maintains the correct order for child properties where offset changes (recall #17870).

    Oh of course. I remember. That clears up the confusion I had about this.

    I didn't quite understand what you meant by "property table building code skips those", I'm not sure if you're referring to if (Z_TYPE_P(prop_value) == IS_INDIRECT) { continue; } just below, but the intention here is to skip declared properties and only add dynamic properties

    Yeah I got confused about this because I didn't remember the true purpose of what I cited previously, so I confused the two.

  7. added a commit that references this issue on Jan 21, 2026
    d9cbc31
  8. iluuu1994 commented on Jan 21, 2026

    @iluuu1994
    Member

    Perfect, thanks for the feedback and patch. 🙂

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