Thanks to visit codestin.com
Credit goes to github.com

Skip to content

FFICasterTest::testCastNonTrailingCharPointer relies on undefined behaviors #57387

Closed
@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