Description
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)