Skip to content

FFICasterTest::testCastNonTrailingCharPointer relies on undefined behaviors #57387

@arnaud-lb

Description

@arnaud-lb

Symfony version(s) affected

7.2

Description

I believe that the test FFICasterTest::testCastNonTrailingCharPointer
relies on undefined behavior, and will break depending on what is allocated just after \FFI::cdef()->new('char['.$actualLength.']').

The assignment $pointer[$actualLength] = "\x01"; writes 1 byte just after the memory block allocated by \FFI::cdef()->new('char['.$actualLength.']'). Valgrind detects this invalid write when running with USE_ZEND_ALLOC=0 valgrind ./phpunit ...:

==460210== Invalid write of size 1
==460210==    at 0x809ACB: zend_ffi_zval_to_cdata (ffi.c:808)
==460210==    by 0x809ACB: zend_ffi_cdata_write_dim (ffi.c:1476)
==460210==    by 0xDC899D: zend_assign_to_object_dim (zend_execute.c:1532)
==460210==    by 0xEA1E40: ZEND_ASSIGN_DIM_SPEC_CV_CONST_OP_DATA_CONST_HANDLER (zend_vm_execute.h:42809)
==460210==    by 0xEEDDBE: execute_ex (zend_vm_execute.h:61830)
==460210==    by 0xEF10E9: zend_execute (zend_vm_execute.h:62776)
==460210==    by 0xD7966D: zend_execute_script (zend.c:1906)
==460210==    by 0xCA4799: php_execute_script_ex (main.c:2516)
==460210==    by 0xCA48BC: php_execute_script (main.c:2556)
==460210==    by 0x10132F6: do_cli (php_cli.c:956)
==460210==    by 0x10142CD: main (php_cli.c:1330)
==460210==  Address 0x1e30ddec is 0 bytes after a block of size 12 alloc'd
==460210==    at 0x484282F: malloc (vg_replace_malloc.c:446)
==460210==    by 0xD1FFA1: __zend_malloc (zend_alloc.c:3268)
==460210==    by 0xD1BF43: _emalloc (zend_alloc.c:2765)
==460210==    by 0x816E73: zim_FFI_new (ffi.c:3862)
==460210==    by 0xED7A69: ZEND_DO_FCALL_SPEC_RETVAL_USED_HANDLER (zend_vm_execute.h:1988)
==460210==    by 0xED7A69: execute_ex (zend_vm_execute.h:57414)
==460210==    by 0xEF10E9: zend_execute (zend_vm_execute.h:62776)
==460210==    by 0xD7966D: zend_execute_script (zend.c:1906)
==460210==    by 0xCA4799: php_execute_script_ex (main.c:2516)
==460210==    by 0xCA48BC: php_execute_script (main.c:2556)
==460210==    by 0x10132F6: do_cli (php_cli.c:956)
==460210==    by 0x10142CD: main (php_cli.c:1330)

Additionally, FFICaster::castFFIStringValue() treats char* pointers a NUL-terminated C strings, but there is no guarantee that a NUL byte will be found near that address. As a result, in the test, the function will dump any data that happens to exist after $string. In theory this may also crash if this causes the function to reach an unreadable memory region.

How to reproduce

php ./phpunit src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php  --filter=testCastNonTrailingCharPointer

Commit 25360ef24951f1c6b83f8bf85fbdcaff4a1a40e1 in php-src changes the memory layout slightly, which causes the test output to change:

1) Symfony\Component\VarDumper\Tests\Caster\FFICasterTest::testCastNonTrailingCharPointer
Failed asserting that string matches format description.
--- Expected
+++ Actual
@@ @@
 FFI\CData<char*> size 8 align 8 {
-  cdata: "Hello World!%s"
+  cdata: b"Hello World!\x011RK`\x1EÐþ\x16\x7F\x00"
 }

/home/runner/work/symfony/symfony/src/Symfony/Component/VarDumper/Test/VarDumperTestTrait.php:52
/home/runner/work/symfony/symfony/src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php:219

(https://github.com/symfony/symfony/actions/runs/9497428991/job/26173977979)

Running any php version under valgrind reports an invalid write and read of size 1:

USE_ZEND_ALLOC=0 valgrind php ./phpunit src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php  --filter=testCastNonTrailingCharPointer
==462378== Invalid write of size 1
==462378==    at 0x138452F6: zend_ffi_zval_to_cdata (ffi.c:808)
==462378==    by 0x138452F6: zend_ffi_cdata_write_dim (ffi.c:1476)
==462378==    by 0x555FF2: zend_assign_to_object_dim (zend_execute.c:1534)
==462378==    by 0x5986EA: ZEND_ASSIGN_DIM_SPEC_CV_CV_OP_DATA_CONST_HANDLER (zend_vm_execute.h:51907)
==462378==    by 0x5A45F9: execute_ex (zend_vm_execute.h:57332)
==462378==    by 0x3458AF: ZEND_DO_FCALL_SPEC_OBSERVER_HANDLER (zend_vm_execute.h:2052)
==462378==    by 0x346A26: execute_ex.cold (zend_vm_execute.h:57256)
==462378==    by 0x3458AF: ZEND_DO_FCALL_SPEC_OBSERVER_HANDLER (zend_vm_execute.h:2052)
==462378==    by 0x346A26: execute_ex.cold (zend_vm_execute.h:57256)
==462378==    by 0x3458AF: ZEND_DO_FCALL_SPEC_OBSERVER_HANDLER (zend_vm_execute.h:2052)
==462378==    by 0x346A26: execute_ex.cold (zend_vm_execute.h:57256)
==462378==    by 0x3458AF: ZEND_DO_FCALL_SPEC_OBSERVER_HANDLER (zend_vm_execute.h:2052)
==462378==    by 0x346A26: execute_ex.cold (zend_vm_execute.h:57256)
==462378==  Address 0x1a4bbf4c is 0 bytes after a block of size 12 alloc'd
==462378==    at 0x484282F: malloc (vg_replace_malloc.c:446)
==462378==    by 0x5033C4: __zend_malloc (zend_alloc.c:3130)
==462378==    by 0x13842595: zim_FFI_new (ffi.c:3861)
==462378==    by 0x34594D: ZEND_DO_FCALL_SPEC_OBSERVER_HANDLER (zend_vm_execute.h:2086)
==462378==    by 0x346A26: execute_ex.cold (zend_vm_execute.h:57256)
==462378==    by 0x3458AF: ZEND_DO_FCALL_SPEC_OBSERVER_HANDLER (zend_vm_execute.h:2052)
==462378==    by 0x346A26: execute_ex.cold (zend_vm_execute.h:57256)
==462378==    by 0x3458AF: ZEND_DO_FCALL_SPEC_OBSERVER_HANDLER (zend_vm_execute.h:2052)
==462378==    by 0x346A26: execute_ex.cold (zend_vm_execute.h:57256)
==462378==    by 0x3458AF: ZEND_DO_FCALL_SPEC_OBSERVER_HANDLER (zend_vm_execute.h:2052)
==462378==    by 0x346A26: execute_ex.cold (zend_vm_execute.h:57256)
==462378==    by 0x3458AF: ZEND_DO_FCALL_SPEC_OBSERVER_HANDLER (zend_vm_execute.h:2052)
==462378== 
==462378== Invalid read of size 1
==462378==    at 0x1383BB40: zend_ffi_cdata_to_zval (ffi.c:590)
==462378==    by 0x1383BB40: zend_ffi_cdata_read_dim (ffi.c:1423)
==462378==    by 0x557BDD: UnknownInlinedFun (zend_execute.c:2845)
==462378==    by 0x557BDD: zend_fetch_dimension_address_read_R_slow (zend_execute.c:2884)
==462378==    by 0x582B29: ZEND_FETCH_DIM_R_SPEC_CV_CV_HANDLER (zend_vm_execute.h:50876)
==462378==    by 0x5A6B29: execute_ex (zend_vm_execute.h:61391)
==462378==    by 0x3458AF: ZEND_DO_FCALL_SPEC_OBSERVER_HANDLER (zend_vm_execute.h:2052)
==462378==    by 0x346A26: execute_ex.cold (zend_vm_execute.h:57256)
==462378==    by 0x3458AF: ZEND_DO_FCALL_SPEC_OBSERVER_HANDLER (zend_vm_execute.h:2052)
==462378==    by 0x346A26: execute_ex.cold (zend_vm_execute.h:57256)
==462378==    by 0x3458AF: ZEND_DO_FCALL_SPEC_OBSERVER_HANDLER (zend_vm_execute.h:2052)
==462378==    by 0x346A26: execute_ex.cold (zend_vm_execute.h:57256)
==462378==    by 0x3458AF: ZEND_DO_FCALL_SPEC_OBSERVER_HANDLER (zend_vm_execute.h:2052)
==462378==    by 0x346A26: execute_ex.cold (zend_vm_execute.h:57256)
==462378==  Address 0x1a4bbf4c is 0 bytes after a block of size 12 alloc'd
==462378==    at 0x484282F: malloc (vg_replace_malloc.c:446)
==462378==    by 0x5033C4: __zend_malloc (zend_alloc.c:3130)
==462378==    by 0x13842595: zim_FFI_new (ffi.c:3861)
==462378==    by 0x34594D: ZEND_DO_FCALL_SPEC_OBSERVER_HANDLER (zend_vm_execute.h:2086)
==462378==    by 0x346A26: execute_ex.cold (zend_vm_execute.h:57256)
==462378==    by 0x3458AF: ZEND_DO_FCALL_SPEC_OBSERVER_HANDLER (zend_vm_execute.h:2052)
==462378==    by 0x346A26: execute_ex.cold (zend_vm_execute.h:57256)
==462378==    by 0x3458AF: ZEND_DO_FCALL_SPEC_OBSERVER_HANDLER (zend_vm_execute.h:2052)
==462378==    by 0x346A26: execute_ex.cold (zend_vm_execute.h:57256)
==462378==    by 0x3458AF: ZEND_DO_FCALL_SPEC_OBSERVER_HANDLER (zend_vm_execute.h:2052)
==462378==    by 0x346A26: execute_ex.cold (zend_vm_execute.h:57256)
==462378==    by 0x3458AF: ZEND_DO_FCALL_SPEC_OBSERVER_HANDLER (zend_vm_execute.h:2052)
==462378== 

Possible Solution

Allocate more memory to make room for the $pointer[$actualLength] = "\x01";. However, this assignment is not strictly necessary as the null byte after $actualMessage is not copied by \FFI::memcpy($pointer, $actualMessage, $actualLength).

There is no way to prevent FFICaster::castFFIStringValue() from dumping unrelated data, but if that's ok, it may be possible to prevent it from reaching unreadable memory and crashing (providing that char* points to a readable memory address) by not stepping over page boundaries. E.g. don't dump past $addr | (4096-1).

It may be possible to find the size of the memory block if its allocated by zend_mm, and if $addr points to the beginning of the block, by using the zend_mm API. E.g. use is_zend_mm() && is_zend_ptr($addr) to find if $addr points to the beginning of a block allocated by zend_mm, and _zend_mm_block_size() to find its size. However I do not necessarily recommend this.

Additional Context

The issue was detected in #57379, and reported by @alexandre-daubois in php/php-src#14546 (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

      pFad - Phonifier reborn

      Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

      Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


      Alternative Proxies:

      Alternative Proxy

      pFad Proxy

      pFad v3 Proxy

      pFad v4 Proxy