Repository navigation
SIGSEGV when trying to modify HTTP headers of a PHP stream context #19583
Description
Description
The following commit (Make OPcache non-optional) (which belongs to PHP 8.5.0-alpha3) seems to cause a SIGSEGV when trying to modify HTTP headers of a PHP stream context.
The objective is to insert a custom header into the HTTP headers of a PHP stream context.
What the extension does:
- Retrieves the 4th argument passed to
fopen(a stream context resource). - Fetches the internal C structure (
php_stream_context) for the stream context resource. - Gets the 'header' option (array) from the 'http' section of the stream context.
- Copies the last element of the header array and replaces it with a custom header.
If fopen_array.php (listed below) contains only one call to fopen, it usually succeeds; however, the next request typically causes a SIGSEGV (there is unfortunately no useful backtrace available in such a case).
If fopen_array.php (listed below) contains more than one call to fopen (your mileage may vary; modify the file to add more calls if needed), the first call usually succeeds. However, one of the subsequent calls may fail, resulting in the following backtrace:
Thread 3 "httpd" received signal SIGABRT, Aborted.
[Switching to Thread 0x7ffff1fff640 (LWP 1813271)]
__pthread_kill_implementation (no_tid=0, signo=6, threadid=140737253471808) at ./nptl/pthread_kill.c:44
44 ./nptl/pthread_kill.c: No such file or directory.
(gdb) bt
#0 __pthread_kill_implementation (no_tid=0, signo=6, threadid=140737253471808)
at ./nptl/pthread_kill.c:44
#1 __pthread_kill_internal (signo=6, threadid=140737253471808) at ./nptl/pthread_kill.c:78
#2 __GI___pthread_kill (threadid=140737253471808, signo=signo@entry=6) at ./nptl/pthread_kill.c:89
#3 0x00007ffff7c42476 in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4 0x00007ffff7c287f3 in __GI_abort () at ./stdlib/abort.c:79
#5 0x00007ffff4f7ef55 in zend_mm_panic (
message=message@entry=0x7ffff60c3a42 "zend_mm_heap corrupted")
at /php-src/Zend/zend_alloc.c:408
#6 0x00007ffff4f7f502 in zend_mm_realloc_heap (copy_size=136, use_copy_size=true,
size=<optimized out>, ptr=0x408be898, heap=0x7ffff6800040)
at /php-src/Zend/zend_alloc.c:1717
#7 _erealloc2 (ptr=0x408be898, size=<optimized out>, copy_size=136)
at /php-src/Zend/zend_alloc.c:2796
#8 0x00007ffff548b3e8 in zend_hash_packed_grow (ht=0x408be860)
at /php-src/Zend/zend_hash.c:317
#9 0x00007ffff548e8e3 in _zend_hash_index_add_or_update_i (flag=18, pData=0x408be910, h=8,
ht=0x408be860)
at /php-src/Zend/zend_hash.c:1140
#10 zend_hash_next_index_insert (ht=0x408be860, pData=0x408be910)
at /php-src/Zend/zend_hash.c:1225
#11 0x00007ffff7ba532f in add_next_index_zval (value=<optimized out>, arg=0x7ffff68812e0)
at /php-8.5.0-dev/include/php/Zend/zend_API.h:632
#12 zif_fopen (execute_data=0x7ffff68140c0, return_value=0x7ffff1ffc5f0)
at /_ext/_fopen_inject_ext/fopen_inject.c:34
#13 0x00007ffff547c824 in ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER ()
at /php-src/Zend/zend_vm_execute.h:1344
#14 execute_ex (ex=0x1bab13)
at /php-src/Zend/zend_vm_execute.h:58925
#15 0x00007ffff5475e69 in zend_execute (op_array=op_array@entry=0x7ffff687c000,
return_value=return_value@entry=0x0)
at /php-src/Zend/zend_vm_execute.h:64393
#16 0x00007ffff54e2c72 in zend_execute_script (type=type@entry=8, retval=retval@entry=0x0,
file_handle=file_handle@entry=0x7ffff1ffea50)
at /php-src/Zend/zend.c:1943
#17 0x00007ffff536fed3 in php_execute_script_ex (primary_file=<optimized out>,
retval=<optimized out>)
at /php-src/main/main.c:2594
#18 0x00007ffff54e4cd8 in php_handler (r=<optimized out>)
at /php-src/sapi/apache2handler/sapi_apache2.c:718
#19 0x00005555555b7c78 in ap_run_handler (r=r@entry=0x7fffe4002c20) at config.c:169
#20 0x00005555555b8276 in ap_invoke_handler (r=r@entry=0x7fffe4002c20) at config.c:443
#21 0x00005555555cf8b8 in ap_process_async_request (r=r@entry=0x7fffe4002c20) at http_request.c:452
#22 0x00005555555cb953 in ap_process_http_async_connection (c=0x7fff70000f90) at http_core.c:155
#23 ap_process_http_connection (c=0x7fff70000f90) at http_core.c:246
#24 0x00005555555c1cc8 in ap_run_process_connection (c=c@entry=0x7fff70000f90) at connection.c:42
#25 0x00005555555dc42f in process_socket (thd=thd@entry=0x555555925a08, p=<optimized out>,
sock=0x7fff70000ce0, cs=<optimized out>, my_child_num=my_child_num@entry=0,
my_thread_num=my_thread_num@entry=0) at event.c:1098
#26 0x00005555555dd26b in worker_thread (thd=0x555555925a08, dummy=<optimized out>) at event.c:2252
#27 0x00007ffff7c94ac3 in start_thread (arg=<optimized out>) at ./nptl/pthread_create.c:442
#28 0x00007ffff7d26850 in clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:81Notice the difference between the working and failing Z_ARRVAL_P(header) representations in GDB:
working:
(gdb) p *(*(header)).value.arr
$1 = {gc = {refcount = 2, u = {type_info = 7}}, u = {v = {flags = 20 '\024', _unused = 0 '\000',
nIteratorsCount = 0 '\000', _unused2 = 0 '\000'}, flags = 20}, nTableMask = 4294967294, {
arHash = 0x7ffff38690a8, arData = 0x7ffff38690a8, arPacked = 0x7ffff38690a8}, nNumUsed = 3,
nNumOfElements = 3, nTableSize = 8, nInternalPointer = 0, nNextFreeElement = 3,
pDestructor = 0x7ffff41508b0 <zval_ptr_dtor>}non-working:
(gdb) p *(*(header)).value.arr
$1 = {gc = {refcount = 2, u = {type_info = 71}}, u = {v = {flags = 20 '\024', _unused = 0 '\000',
nIteratorsCount = 0 '\000', _unused2 = 0 '\000'}, flags = 20}, nTableMask = 4294967294, {
arHash = 0x408be8a0, arData = 0x408be8a0, arPacked = 0x408be8a0}, nNumUsed = 3,
nNumOfElements = 3, nTableSize = 8, nInternalPointer = 0, nNextFreeElement = 3, pDestructor = 0x0}non-working version seems to have type_info = 71, no pDestructor and the arPacked's address looks a bit off compared to the working version.
Below is a minimal PHP extension to reproduce the issue.
The testing PHP setup is configured to be used as a module with the Apache web server.
config.m4
PHP_ARG_ENABLE(fopen_inject, whether to enable fopen_inject support, [ --enable-fopen_inject Enable fopen_inject support])
if test "$PHP_FOPEN_INJECT" = "yes"; then
PHP_NEW_EXTENSION(fopen_inject, fopen_inject.c, $ext_shared)
fi
fopen_inject.c
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "php.h"
static void (*orig_fopen_handler)(INTERNAL_FUNCTION_PARAMETERS) = NULL;
// Our replacement for fopen
PHP_FUNCTION(fopen)
{
php_printf("[fopen_inject] fopen called\n");
// Retrieves the 4th argument passed to fopen (a stream context resource).
zval* context = ZEND_CALL_ARG(EG(current_execute_data), 3 + 1);
if (Z_TYPE_P(context) == IS_RESOURCE) {
// Fetches the internal C structure (`php_stream_context`) for the stream context resource.
php_stream_context *ctx = zend_fetch_resource_ex(context, "Stream-Context", php_le_stream_context());
if (ctx) {
zval* header = php_stream_context_get_option(ctx, "http", "header");
if (Z_TYPE_P(header) == IS_ARRAY) {
php_printf("IS_ARRAY, type_info: %d\n", GC_TYPE_INFO(Z_ARRVAL_P(header))); /* p *(*(header)).value.arr */
zval *tmpheader = NULL;
unsigned int i = 0;
const char *tag = "tracestate: rojo=00f067aa0ba902b7";
ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(header), tmpheader) {
if (Z_TYPE_P(tmpheader) == IS_STRING) {
/* copy the last element && replace it with the custom header */
if (i == Z_ARRVAL_P(header)->nNumOfElements - 1) {
Z_TRY_ADDREF_P(tmpheader);
add_next_index_zval(header, tmpheader);
add_index_stringl(header, i, tag, strlen(tag));
goto exit;
}
}
++i;
} ZEND_HASH_FOREACH_END();
}
}
}
exit:
// Call the original handler
if (orig_fopen_handler) {
orig_fopen_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU);
return;
}
RETURN_FALSE;
}
PHP_MINIT_FUNCTION(fopen_inject)
{
zend_function *func = zend_hash_str_find_ptr(CG(function_table), "fopen", sizeof("fopen")-1);
if (func && func->type == ZEND_INTERNAL_FUNCTION) {
orig_fopen_handler = func->internal_function.handler;
func->internal_function.handler = ZEND_FN(fopen);
}
return SUCCESS;
}
zend_module_entry fopen_inject_module_entry = {
STANDARD_MODULE_HEADER,
"fopen_inject",
NULL,
PHP_MINIT(fopen_inject),
NULL,
NULL,
NULL,
NULL,
"0.1",
STANDARD_MODULE_PROPERTIES
};
#ifdef COMPILE_DL_FOPEN_INJECT
ZEND_GET_MODULE(fopen_inject)
#endifphp_fopen_inject.h
#ifndef PHP_FOPEN_INJECT_H
#define PHP_FOPEN_INJECT_H
extern zend_module_entry fopen_inject_module_entry;
#define phpext_fopen_inject_ptr &fopen_inject_module_entry
#endifphp.ini
extension=fopen_inject.so
config.nice
#! /bin/sh
#
# Created by configure
'./configure' \
'--prefix=/YOUR_PATH/php-8.5.0-dev' \
'--with-config-file-path=/YOUR_PATH/php-8.5.0-dev/etc' \
'--with-apxs2=/YOUR_PATH/httpd-dev-2.4.65/bin/apxs' \
"$@"fopen_array.php
<?php
$apachePort = 8083;
$opts = array(
'http'=>array(
'method'=>"GET",
'header'=> array("Accept-language: en",
"Cookie: foo=bar")
)
);
$context = stream_context_create($opts);
fopen('http://localhost:' . $apachePort . '/stream/print_headers.php', "r", false, $context);
// fopen('http://localhost:' . $apachePort . '/stream/print_headers.php', "r", false, $context);
// fopen('http://localhost:' . $apachePort . '/stream/print_headers.php', "r", false, $context);
// fopen('http://localhost:' . $apachePort . '/stream/print_headers.php', "r", false, $context);
// fopen('http://localhost:' . $apachePort . '/stream/print_headers.php', "r", false, $context);
// fopen('http://localhost:' . $apachePort . '/stream/print_headers.php', "r", false, $context);
// fopen('http://localhost:' . $apachePort . '/stream/print_headers.php', "r", false, $context);
// fopen('http://localhost:' . $apachePort . '/stream/print_headers.php', "r", false, $context);
// echo fread(fopen('http://localhost:' . $apachePort . '/stream/print_headers.php', "r", false, $context), 9001);
// echo "<br /><br />Test 2<br /><br />";
// echo fread(fopen('http://localhost:' . $apachePort . '/stream/print_headers.php', "r", false, $context), 9001);print_headers.php
<?php
// Get all HTTP headers
$headers = getallheaders();
// Print each header
foreach ($headers as $name => $value) {
echo "$name: $value<br>";
}
?>PHP Version
PHP 8.5.0-alpha3 - PHP 8.5.0-beta1
Operating System
Ubuntu 22.04
Activity
DanielEScherzer commentedon Aug 25, 2025
PHP 8.5.0alpha3 was problematic and skipped, it should not be used for anything (see https://scherzer.dev/Blog/20250801-no-alpha-3 for some details).
But I see you reported that this is also with beta1, thanks for including
DanielEScherzer commentedon Aug 25, 2025
Can you please try with:
to allow us to confirm if this is just caused by the fact that opcache is now always enabled, or caused by opcache, and only revealed now that it is always available?
KaseyJenkins commentedon Aug 25, 2025
Hi, @DanielEScherzer!
Thank you for taking a look!
There appears to be no difference as to whether
opcacheis on or off when it comes toalpha2.Thanks.
DanielEScherzer commentedon Aug 25, 2025
Okay. You wrote
how did you narrow it down to that commit? Did you test against the commit right before it and things worked? (Both with and without opcache?)
Can you also confirm that you are testing with opcache enabled before your custom extension?
TimWolla commentedon Aug 25, 2025
Given that stream contexts are mentioned, #19158 seems relevant.
TimWolla commentedon Aug 25, 2025
@KaseyJenkins Can you run in valgrind or build with ASAN? (set
USE_ZEND_ALLOC=0when doing so).KaseyJenkins commentedon Aug 25, 2025
Yes, exactly that - I tested it against the commit right before it and everything worked (when it comes to
beta1no options were added tophp.inireopcache, i.e.php.inidoes not contain any options related toopcache).When it comes to
alpha2-opcache.enable=1was placed beforeextension=fopen_inject.so.DanielEScherzer commentedon Aug 25, 2025
For alpha2 you also need to make sure that the extension is installed, I don't think
opcache.enableis enough - can you confirm thatphp -mincludes opcache?KaseyJenkins commentedon Aug 25, 2025
Yes, @DanielEScherzer , you are right, after having enabled
opcachewithzend_extension=opcache.soinalpha2the described aberrant behaviour seems to be back (i.e. PHP crashes).DanielEScherzer commentedon Aug 25, 2025
Okay, so this is unrelated to #18961. Did it work with PHP 8.4? PHP 8.5.0alpha1? If it worked with 8.4 but not alpha1 bisecting will be a lot trickier
KaseyJenkins commentedon Aug 25, 2025
I will run additional tests involving PHP 8.4 and PHP 8.5.0alpha1 and get back to you (there could be a slight delay of a few hours though :)
TimWolla commentedon Aug 25, 2025
@KaseyJenkins When running with OPcache the array literal is sitting in SHM and immutable. The call to
add_next_index_zval()is trying to modify it. Generally speaking you must never modify an array that you do not own (e.g. refcount > 1, immutable, …). Try running with-d opcache.protect_memory=1.This is not a bug in PHP.
KaseyJenkins commentedon Aug 26, 2025
@TimWolla, thank you for taking a look!
If one wanted to achieve something similar with OPcache, what would your advice be regarding the original logic in the code?
Thanks!
bwoebi commentedon Aug 26, 2025
@KaseyJenkins What you want are calls to SEPARATE_ARRAY(), to create a copy, which you can alter.
KaseyJenkins commentedon Aug 27, 2025
Thank you, @bwoebi!
I assume it's safe to apply
SEPARATE_ARRAY()to such an array (which isIS_ARRAY_IMMUTABLE), i.e. there is no need forZVAL_DUP()and consequentlyphp_stream_context_set_option?Thanks!