条件跳转或移动依赖于未初始化的值(出现在php_pcre_match_impl中)
我在PHP-C扩展中创建一个简单函数,我使用的是从源码编译的PHP 8.3,开启opcache和 pcre.jit。我把ZEND_DONT_UNLOAD_MODULES设置为1,USE_ZEND_ALLOC设置为0,以便Valgrind使用。
在我的扩展中,我直接调用php_pcre_match_impl
#include <php.h>
#include <zend_interfaces.h>
#include <ext/session/php_session.h>
#include <ext/pcre/php_pcre.h>
#include "swat.h"
PHP_FUNCTION(run) {
zval matched, matches;
ZEND_PARSE_PARAMETERS_NONE();
ZVAL_UNDEF(&matched);
array_init(&matches);
zend_string *pattern = zend_string_init(ZEND_STRL("/\\/page-[0-9]+/"), 0);
zend_string *subject = zend_string_init(ZEND_STRL("backend/product"), 0); // Valgrind stack trace error
pcre_cache_entry *pce = pcre_get_compiled_regex_cache(pattern);
if (!pce) {
zend_string_release(pattern);
zend_string_release(subject);
RETURN_FALSE;
}
php_pcre_match_impl(pce, subject, &matched, &matches, 1, 0, 0, 0);
zend_string_release(pattern);
zend_string_release(subject);
zval_ptr_dtor(&matched);
zval_ptr_dtor(&matches);
RETURN_TRUE;
}
在使用Valgrind运行PHP时:
valgrind --leak-check=full php -S 0.0.0.0:8686 index.php
我看到如下错误:
==11== Invalid read of size 16
==11== at 0x8B43C56: ???
==11== by 0x8850AF7: ???
==11== by 0x8850AF7: ???
==11== by 0x8850B06: ???
==11== by 0x84BF75F: ???
==11== by 0x8850AF7: ???
==11== Address 0x8850aff is 31 bytes inside a block of size 40 alloc'd
==11== at 0x4846828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==11== by 0x89B4E5: __zend_malloc (zend_alloc.c:3142)
==11== by 0x899FBA: _malloc_custom (zend_alloc.c:2494)
==11== by 0x89A104: _emalloc (zend_alloc.c:2613)
==11== by 0x87003FB: zend_string_alloc (zend_string.h:174)
==11== by 0x870046E: zend_string_init (zend_string.h:196)
==11== by 0x87005CB: zif_run (swat.c:20)
==11== by 0x923C92: ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER (zend_vm_execute.h:1275)
==11== by 0x9A257B: execute_ex (zend_vm_execute.h:57273)
==11== by 0x9A7E45: zend_execute (zend_vm_execute.h:61665)
==11== by 0x8DE991: zend_execute_scripts (zend.c:1895)
==11== by 0xA7551F: php_cli_server_dispatch_router (php_cli_server.c:2286)
==11== by 0xA756FF: php_cli_server_dispatch (php_cli_server.c:2326)
==11== by 0xA762DD: php_cli_server_recv_event_read_request (php_cli_server.c:2667)
==11== by 0xA766D9: php_cli_server_do_event_for_each_fd_callback (php_cli_server.c:2754)
==11== by 0xA71C73: php_cli_server_poller_iter_on_active (php_cli_server.c:932)
==11== by 0xA76773: php_cli_server_do_event_for_each_fd (php_cli_server.c:2774)
==11== by 0xA7681F: php_cli_server_do_event_loop (php_cli_server.c:2786)
==11== by 0xA76C84: do_cli_server (php_cli_server.c:2918)
==11== by 0xA6C1D9: main (php_cli.c:1344)
==11==
==11== Conditional jump or move depends on uninitialised value(s)
==11== at 0x8B43CFA: ???
==11== by 0x8850AF7: ???
==11== by 0x8850AF7: ???
==11== by 0x8850B06: ???
==11== by 0x84BF75F: ???
==11== by 0x8850AF7: ???
==11==
预期:
该函数应该在没有内存错误的情况下运行。
实际:
Valgrind报告了无效读取和未初始化值的使用。
我怀疑问题可能与我如何使用php_pcre_match_impl或 zvals的初始化有关,但不确定到底哪里出了问题。
直接在扩展中调用php_pcre_match_impl是否安全,还是我遗漏了必要的初始化步骤?
解决方案
恐怕除了不使用JIT之外,几乎没有可做到的办法。在Valgrind的 Qt用户FAQ中也有提及那里使用的是pcre2进行正则表达式匹配 https://valgrind.org/docs/manual/faq.html#faq.pcre2_qt。
问题在于pcre2的 JIT编译代码出于优化目的故意使用未定义行为(UB)。你可以从错误信息 Address 0x8850aff is 31 bytes inside a block of size 40 alloc'd 看出。这意味着有40字节的分配,但pcre2的 JIT编码从字节31到字节47进行了16字节的读取。那包含了超出40字节分配结束的位置的字节40到 47。这就是未定义行为。更糟的是,这发生在JIT编译的代码中。这意味着pcre2已经把一些机器码装载到内存并正在执行它。Valgrind无法知道那来自哪里——没有DWARF调试信息,且该内存地址范围也不对应任何映射到内存中的ELF二进制文件。结果在调用栈中只能看到问号。
这种对UB的优化使用相当常见。C标准库在与SIMD指令配合时常会采用它。在这些情况下,Valgrind会用它自有的str/mem函数版本来替代,以避免产生错误。该优化利用了几乎所有编译器和libc实现都使用16字节默认分配粒度这一事实。这意味着在本例中有一次 malloc 调用请求40字节,而malloc很可能分配了48字节。这也并非ISO C或 ISO C++标准所规定。
曾尝试对总分配大小进行formalise的信息,https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0901r5.html。那份论文未被接受。
至于修复问题,我相信PHP有类似Qt的机制,你可以在php.ini中设置类似 export PCRE_JIT=0 或 pcre.jit = 0 的选项。环境变量似乎更适合,因为它可以更容易地限定在你的Valgrind测试中。我不是PHP开发者,所以无法确认这种做法是否有效。
如果你能确认这是一个可行的解决方案,我会更新Valgrind的 FAQ。