Skip to content

FFICasterTest::testCastNonTrailingCharPointer relies on undefined behaviors #57387

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
arnaud-lb opened this issue Jun 13, 2024 · 2 comments
Closed

Comments

@arnaud-lb
Copy link
Contributor

arnaud-lb commented Jun 13, 2024

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)

@arnaud-lb arnaud-lb added the Bug label Jun 13, 2024
@arnaud-lb arnaud-lb changed the title testCastNonTrailingCharPointer relies on undefined behaviors FFICasterTest::testCastNonTrailingCharPointer relies on undefined behaviors Jun 13, 2024
@alexandre-daubois
Copy link
Member

I'll have a look at a fix with all your hints and details. Thank you Arnaud for this exhaustive bug report!

fabpot added a commit that referenced this issue Jun 22, 2024
This PR was merged into the 6.4 branch.

Discussion
----------

[VarDumper] Fix FFI caster test

| Q             | A
| ------------- | ---
| Branch?       | 6.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Issues        | Fix #57387
| License       | MIT

Fixes the invalid memory writing thanks to Arnaud's hints. Not easy to understand exactly what's the point of `testCastNonTrailingCharPointer`, so I hope the test is still valid like this.

Commits
-------

8594b2f [VarDumper] Fix FFI caster test
@fabpot fabpot closed this as completed Jun 22, 2024
@SerafimArts
Copy link
Contributor

SerafimArts commented Dec 11, 2024

This test was originally written to make sure that even when outputting non-C-strings with "garbage data" at the end, the var-dumper would not crash and at least do something, continuing to work correctly in most cases (That's why there was a comment informing that the test might not be stable).

However, it seems to me that the problem is initially that the var-dumper tries to output char* as a string. I suggested introducing an ENV variable to solve this problem (something like VAR_DUMPER_ALLOW_UNSAFE_STRINGS), then this test would not be needed. However, I didn't receive any feedback (approval or rejection) from the Symfony maintainers, so I didn't take on the task. Maybe it makes sense to add this functionality and disable dump char* by default?

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

No branches or pull requests

6 participants