Skip to content

SIGSEGV when trying to modify HTTP headers of a PHP stream context #19583

Closed as not planned

Description

@KaseyJenkins

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:81

Notice 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)
#endif

php_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

#endif

php.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

  1. DanielEScherzer commented on Aug 25, 2025

    @DanielEScherzer
    Member

    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

  2. DanielEScherzer commented on Aug 25, 2025

    @DanielEScherzer
    Member

    Can you please try with:

    • alpha2 and no opcache
    • alpha2 with opcache

    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?

  3. KaseyJenkins commented on Aug 25, 2025

    @KaseyJenkins
    ContributorAuthor

    Hi, @DanielEScherzer!
    Thank you for taking a look!

    There appears to be no difference as to whether opcache is on or off when it comes to alpha2.

    Thanks.

  4. DanielEScherzer commented on Aug 25, 2025

    @DanielEScherzer
    Member

    Okay. You wrote

    The following commit (Make OPcache non-optional) ... seems to cause a SIGSEGV when trying to modify HTTP headers of a PHP stream context.

    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?

  5. TimWolla commented on Aug 25, 2025

    @TimWolla
    Member

    Given that stream contexts are mentioned, #19158 seems relevant.

  6. TimWolla commented on Aug 25, 2025

    @TimWolla
    Member

    @KaseyJenkins Can you run in valgrind or build with ASAN? (set USE_ZEND_ALLOC=0 when doing so).

  7. KaseyJenkins commented on Aug 25, 2025

    @KaseyJenkins
    ContributorAuthor

    Yes, exactly that - I tested it against the commit right before it and everything worked (when it comes to beta1 no options were added to php.ini re opcache, i.e. php.ini does not contain any options related to opcache).

    When it comes to alpha2 - opcache.enable=1 was placed before extension=fopen_inject.so.

  8. DanielEScherzer commented on Aug 25, 2025

    @DanielEScherzer
    Member

    Yes, exactly that - I tested it against the commit right before it and everything worked (when it comes to beta1 no options were added to php.ini re opcache, i.e. php.ini does not contain any options related to opcache).

    When it comes to alpha2 - opcache.enable=1 was placed before extension=fopen_inject.so.

    For alpha2 you also need to make sure that the extension is installed, I don't think opcache.enable is enough - can you confirm that php -m includes opcache?

  9. KaseyJenkins commented on Aug 25, 2025

    @KaseyJenkins
    ContributorAuthor

    Yes, @DanielEScherzer , you are right, after having enabled opcache with zend_extension=opcache.so in alpha2 the described aberrant behaviour seems to be back (i.e. PHP crashes).

  10. DanielEScherzer commented on Aug 25, 2025

    @DanielEScherzer
    Member

    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

  11. KaseyJenkins commented on Aug 25, 2025

    @KaseyJenkins
    ContributorAuthor

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

  12. TimWolla commented on Aug 25, 2025

    @TimWolla
    Member

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

  13. KaseyJenkins commented on Aug 26, 2025

    @KaseyJenkins
    ContributorAuthor

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

  14. bwoebi commented on Aug 26, 2025

    @bwoebi
    Member

    @KaseyJenkins What you want are calls to SEPARATE_ARRAY(), to create a copy, which you can alter.

  15. KaseyJenkins commented on Aug 27, 2025

    @KaseyJenkins
    ContributorAuthor

    Thank you, @bwoebi!
    I assume it's safe to apply SEPARATE_ARRAY() to such an array (which is IS_ARRAY_IMMUTABLE), i.e. there is no need for ZVAL_DUP() and consequently php_stream_context_set_option?

    Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to 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