From de39a14cc740ce11e271d6f0569718b4c9dc06ac Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Tue, 16 Jun 2026 18:17:09 -0400 Subject: [PATCH 1/3] Fix SOAP client failing to follow a scheme-less redirect Location The HTTP redirect handler inherited scheme, host and port from the newly parsed Location URI itself instead of the previous request URI, so a scheme-less (relative or absolute-path) Location left the host NULL and the retry aborted with "Unable to parse URL". Read the inherited components and the path base from the request URI, restoring the behaviour the URI-parser refactor changed. Closes GH-22341 --- ext/soap/php_http.c | 10 ++-- ext/soap/tests/bugs/relative_redirect.phpt | 49 +++++++++++++++++++ .../tests/bugs/relative_redirect_path.phpt | 49 +++++++++++++++++++ 3 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 ext/soap/tests/bugs/relative_redirect.phpt create mode 100644 ext/soap/tests/bugs/relative_redirect_path.phpt diff --git a/ext/soap/php_http.c b/ext/soap/php_http.c index 944b033363c6..c7f3732f2987 100644 --- a/ext/soap/php_http.c +++ b/ext/soap/php_http.c @@ -1162,12 +1162,12 @@ int make_http_soap_request( zend_string_release_ex(http_headers, 0); zend_string_release_ex(http_body, 0); if (new_uri->scheme == NULL && new_uri->path != NULL) { - new_uri->scheme = new_uri->scheme ? zend_string_copy(new_uri->scheme) : NULL; - new_uri->host = new_uri->host ? zend_string_copy(new_uri->host) : NULL; - new_uri->port = new_uri->port; + new_uri->scheme = uri->scheme ? zend_string_copy(uri->scheme) : NULL; + new_uri->host = uri->host ? zend_string_copy(uri->host) : NULL; + new_uri->port = uri->port; if (new_uri->path && ZSTR_VAL(new_uri->path)[0] != '/') { - if (new_uri->path) { - char *t = ZSTR_VAL(new_uri->path); + if (uri->path) { + char *t = ZSTR_VAL(uri->path); char *p = strrchr(t, '/'); if (p) { zend_string *s = zend_string_alloc((p - t) + ZSTR_LEN(new_uri->path) + 2, 0); diff --git a/ext/soap/tests/bugs/relative_redirect.phpt b/ext/soap/tests/bugs/relative_redirect.phpt new file mode 100644 index 000000000000..774e7cbd98d7 --- /dev/null +++ b/ext/soap/tests/bugs/relative_redirect.phpt @@ -0,0 +1,49 @@ +--TEST-- +SOAP client follows a redirect with a scheme-less (relative) Location +--EXTENSIONS-- +soap +--SKIPIF-- + +--FILE-- +', + '', + 'ok', + ''; +} else { + http_response_code(302); + header("Location: /redirected"); +} +PHP; + +php_cli_server_start($code, null, $args); + +$client = new SoapClient(null, [ + 'location' => 'http://' . PHP_CLI_SERVER_ADDRESS . '/start', + 'uri' => 'test-uri', +]); + +try { + $client->__soapCall("foo", []); + echo "redirect followed\n"; +} catch (SoapFault $e) { + echo "SoapFault: " . $e->getMessage() . "\n"; +} +?> +--EXPECT-- +redirect followed diff --git a/ext/soap/tests/bugs/relative_redirect_path.phpt b/ext/soap/tests/bugs/relative_redirect_path.phpt new file mode 100644 index 000000000000..09d4c857cc92 --- /dev/null +++ b/ext/soap/tests/bugs/relative_redirect_path.phpt @@ -0,0 +1,49 @@ +--TEST-- +SOAP client follows a redirect with a relative Location resolved against the request path +--EXTENSIONS-- +soap +--SKIPIF-- + +--FILE-- +', + '', + 'ok', + ''; +} else { + http_response_code(302); + header("Location: redirected"); +} +PHP; + +php_cli_server_start($code, null, $args); + +$client = new SoapClient(null, [ + 'location' => 'http://' . PHP_CLI_SERVER_ADDRESS . '/svc/start', + 'uri' => 'test-uri', +]); + +try { + $client->__soapCall("foo", []); + echo "redirect followed\n"; +} catch (SoapFault $e) { + echo "SoapFault: " . $e->getMessage() . "\n"; +} +?> +--EXPECT-- +redirect followed From 5ce795bec1988052596c77a27dddbe4770e7487c Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 20 Jun 2026 19:01:46 -0400 Subject: [PATCH 2/3] Fix GH-20964: fseek() on php://memory with PHP_INT_MIN causes undefined behavior (#21433) Negate after casting to unsigned instead of before, avoiding signed integer overflow when offset is ZEND_LONG_MIN. The same pattern existed in the pdo_sqlite and sqlite3 stream seek handlers. Closes GH-20964 Closes GH-20927 --- ext/pdo_sqlite/pdo_sqlite.c | 4 ++-- ext/pdo_sqlite/tests/gh20964.phpt | 23 +++++++++++++++++++++++ ext/sqlite3/sqlite3.c | 4 ++-- ext/sqlite3/tests/gh20964.phpt | 24 ++++++++++++++++++++++++ main/streams/memory.c | 4 ++-- tests/basic/gh20964.phpt | 22 ++++++++++++++++++++++ 6 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 ext/pdo_sqlite/tests/gh20964.phpt create mode 100644 ext/sqlite3/tests/gh20964.phpt create mode 100644 tests/basic/gh20964.phpt diff --git a/ext/pdo_sqlite/pdo_sqlite.c b/ext/pdo_sqlite/pdo_sqlite.c index 0e7e02df46a4..2da9329e2a04 100644 --- a/ext/pdo_sqlite/pdo_sqlite.c +++ b/ext/pdo_sqlite/pdo_sqlite.c @@ -203,7 +203,7 @@ static int php_pdosqlite3_stream_seek(php_stream *stream, zend_off_t offset, int switch(whence) { case SEEK_CUR: if (offset < 0) { - if (sqlite3_stream->position < (size_t)(-offset)) { + if (sqlite3_stream->position < -(size_t)offset) { sqlite3_stream->position = 0; *newoffs = -1; return -1; @@ -241,7 +241,7 @@ static int php_pdosqlite3_stream_seek(php_stream *stream, zend_off_t offset, int sqlite3_stream->position = sqlite3_stream->size; *newoffs = -1; return -1; - } else if (sqlite3_stream->size < (size_t)(-offset)) { + } else if (sqlite3_stream->size < -(size_t)offset) { sqlite3_stream->position = 0; *newoffs = -1; return -1; diff --git a/ext/pdo_sqlite/tests/gh20964.phpt b/ext/pdo_sqlite/tests/gh20964.phpt new file mode 100644 index 000000000000..65070cb1838a --- /dev/null +++ b/ext/pdo_sqlite/tests/gh20964.phpt @@ -0,0 +1,23 @@ +--TEST-- +GH-20964 (fseek() on PDO SQLite blob stream with PHP_INT_MIN causes undefined behavior) +--EXTENSIONS-- +pdo_sqlite +--FILE-- +exec('CREATE TABLE test (id INTEGER PRIMARY KEY, data BLOB)'); +$db->exec("INSERT INTO test (id, data) VALUES (1, 'hello')"); + +$stream = $db->openBlob('test', 'data', 1); + +var_dump(fseek($stream, PHP_INT_MIN, SEEK_END)); + +rewind($stream); +var_dump(fseek($stream, PHP_INT_MIN, SEEK_CUR)); + +fclose($stream); +?> +--EXPECT-- +int(-1) +int(-1) diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c index bef568a62dfd..67ff850cadd0 100644 --- a/ext/sqlite3/sqlite3.c +++ b/ext/sqlite3/sqlite3.c @@ -1158,7 +1158,7 @@ static int php_sqlite3_stream_seek(php_stream *stream, zend_off_t offset, int wh switch(whence) { case SEEK_CUR: if (offset < 0) { - if (sqlite3_stream->position < (size_t)(-offset)) { + if (sqlite3_stream->position < -(size_t)offset) { sqlite3_stream->position = 0; *newoffs = -1; return -1; @@ -1199,7 +1199,7 @@ static int php_sqlite3_stream_seek(php_stream *stream, zend_off_t offset, int wh sqlite3_stream->position = sqlite3_stream->size; *newoffs = -1; return -1; - } else if (sqlite3_stream->size < (size_t)(-offset)) { + } else if (sqlite3_stream->size < -(size_t)offset) { sqlite3_stream->position = 0; *newoffs = -1; return -1; diff --git a/ext/sqlite3/tests/gh20964.phpt b/ext/sqlite3/tests/gh20964.phpt new file mode 100644 index 000000000000..852940ba983e --- /dev/null +++ b/ext/sqlite3/tests/gh20964.phpt @@ -0,0 +1,24 @@ +--TEST-- +GH-20964 (fseek() on SQLite3 blob stream with PHP_INT_MIN causes undefined behavior) +--EXTENSIONS-- +sqlite3 +--FILE-- +exec('CREATE TABLE test (id INTEGER PRIMARY KEY, data BLOB)'); +$db->exec("INSERT INTO test (id, data) VALUES (1, 'hello')"); + +$stream = $db->openBlob('test', 'data', 1); + +var_dump(fseek($stream, PHP_INT_MIN, SEEK_END)); + +rewind($stream); +var_dump(fseek($stream, PHP_INT_MIN, SEEK_CUR)); + +fclose($stream); +$db->close(); +?> +--EXPECT-- +int(-1) +int(-1) diff --git a/main/streams/memory.c b/main/streams/memory.c index af54c46dd9ad..1cc1886e609d 100644 --- a/main/streams/memory.c +++ b/main/streams/memory.c @@ -126,7 +126,7 @@ static int php_stream_memory_seek(php_stream *stream, zend_off_t offset, int whe switch(whence) { case SEEK_CUR: if (offset < 0) { - if (ms->fpos < (size_t)(-offset)) { + if (ms->fpos < -(size_t)offset) { ms->fpos = 0; *newoffs = -1; return -1; @@ -163,7 +163,7 @@ static int php_stream_memory_seek(php_stream *stream, zend_off_t offset, int whe stream->eof = 0; stream->fatal_error = 0; return 0; - } else if (ZSTR_LEN(ms->data) < (size_t)(-offset)) { + } else if (ZSTR_LEN(ms->data) < -(size_t)offset) { ms->fpos = 0; *newoffs = -1; return -1; diff --git a/tests/basic/gh20964.phpt b/tests/basic/gh20964.phpt new file mode 100644 index 000000000000..2a97164c7924 --- /dev/null +++ b/tests/basic/gh20964.phpt @@ -0,0 +1,22 @@ +--TEST-- +GH-20964 (fseek() on php://memory with PHP_INT_MIN causes undefined behavior) +--FILE-- + +--EXPECT-- +int(-1) +bool(false) +int(-1) +bool(false) From 19f595fd9ea9974e649c43819ffb08a47fc3fd77 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 8 Jun 2026 08:43:42 -0400 Subject: [PATCH 3/3] Fix GH-22158: JIT observer dispatch through wrong run_time_cache slot For a megamorphic call the tracing JIT built the observer handler pointer with the ir_PHI_2 operands reversed relative to the ir_MERGE_WITH predecessor order, so a user function read the internal_function_extension slot instead of op_array_extension. When an extension reserves an op_array handle before the observer registers the two indices differ, that slot holds 0, and the JIT calls a NULL begin handler. The default-off zend_test.observer.reserve_op_array_handle INI forces that index mismatch so the regression test can reproduce the crash. Fixes GH-22158 Closes GH-22258 --- NEWS | 3 +++ ext/opcache/jit/zend_jit_ir.c | 2 +- ext/opcache/tests/jit/gh22158.phpt | 33 ++++++++++++++++++++++++++++++ ext/zend_test/observer.c | 5 +++++ ext/zend_test/php_test.h | 1 + 5 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 ext/opcache/tests/jit/gh22158.phpt diff --git a/NEWS b/NEWS index df93cb5b77b6..9a1b6d35d1f7 100644 --- a/NEWS +++ b/NEWS @@ -58,6 +58,9 @@ PHP NEWS - Opcache: . Fixed bug GH-20469 (Unsafe inheritance cache replay with reentrant autoloading). (Levi Morrison) + . Fixed bug GH-22158 (Tracing JIT dispatches the observer begin handler + through the wrong run_time_cache slot on megamorphic calls). (ptondereau, + iliaal) - OpenSSL: . Fixed bug GH-22187 (Memory corruption (zend_mm_heap corrupted) in diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index f99605ee4208..2cc6a01c4107 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -4815,7 +4815,7 @@ static struct jit_observer_fcall_is_unobserved_data jit_observer_fcall_is_unobse ir_ref observer_handler_user = ir_ADD_OFFSET(run_time_cache, zend_observer_fcall_op_array_extension * sizeof(void *)); ir_MERGE_WITH(if_internal_func_end); - *observer_handler = ir_PHI_2(IR_ADDR, observer_handler_internal, observer_handler_user); + *observer_handler = ir_PHI_2(IR_ADDR, observer_handler_user, observer_handler_internal); } // JIT: if (*observer_handler == ZEND_OBSERVER_NONE_OBSERVED) { diff --git a/ext/opcache/tests/jit/gh22158.phpt b/ext/opcache/tests/jit/gh22158.phpt new file mode 100644 index 000000000000..ce7f9bc55938 --- /dev/null +++ b/ext/opcache/tests/jit/gh22158.phpt @@ -0,0 +1,33 @@ +--TEST-- +GH-22158 (Tracing JIT dispatches observer begin handler through the wrong run_time_cache slot on megamorphic calls) +--EXTENSIONS-- +opcache +zend_test +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=tracing +opcache.jit_buffer_size=32M +opcache.jit_max_polymorphic_calls=0 +zend_test.observer.enabled=1 +zend_test.observer.observe_all=1 +zend_test.observer.show_output=0 +zend_test.observer.reserve_op_array_handle=1 +--FILE-- +f(); +} +echo $t, "\n"; +?> +--EXPECT-- +600000 diff --git a/ext/zend_test/observer.c b/ext/zend_test/observer.c index 0dfb62723bc4..a7ab0c51e53f 100644 --- a/ext/zend_test/observer.c +++ b/ext/zend_test/observer.c @@ -18,6 +18,7 @@ #include "php_test.h" #include "observer.h" #include "zend_observer.h" +#include "zend_extensions.h" #include "zend_smart_str.h" #include "ext/standard/php_var.h" #include "zend_generators.h" @@ -378,6 +379,7 @@ PHP_INI_BEGIN() STD_PHP_INI_BOOLEAN("zend_test.observer.fiber_switch", "0", PHP_INI_SYSTEM, OnUpdateBool, observer_fiber_switch, zend_zend_test_globals, zend_test_globals) STD_PHP_INI_BOOLEAN("zend_test.observer.fiber_destroy", "0", PHP_INI_SYSTEM, OnUpdateBool, observer_fiber_destroy, zend_zend_test_globals, zend_test_globals) STD_PHP_INI_BOOLEAN("zend_test.observer.execute_internal", "0", PHP_INI_SYSTEM, OnUpdateBool, observer_execute_internal, zend_zend_test_globals, zend_test_globals) + STD_PHP_INI_BOOLEAN("zend_test.observer.reserve_op_array_handle", "0", PHP_INI_SYSTEM, OnUpdateBool, observer_reserve_op_array_handle, zend_zend_test_globals, zend_test_globals) PHP_INI_END() void zend_test_observer_init(INIT_FUNC_ARGS) @@ -386,6 +388,9 @@ void zend_test_observer_init(INIT_FUNC_ARGS) if (type != MODULE_TEMPORARY) { REGISTER_INI_ENTRIES(); if (ZT_G(observer_enabled)) { + if (ZT_G(observer_reserve_op_array_handle)) { + zend_get_op_array_extension_handle("zend_test"); + } zend_observer_fcall_register(observer_fcall_init); } } else { diff --git a/ext/zend_test/php_test.h b/ext/zend_test/php_test.h index c1310db7bd70..f32c6338d5c6 100644 --- a/ext/zend_test/php_test.h +++ b/ext/zend_test/php_test.h @@ -51,6 +51,7 @@ ZEND_BEGIN_MODULE_GLOBALS(zend_test) int observer_fiber_switch; int observer_fiber_destroy; int observer_execute_internal; + int observer_reserve_op_array_handle; HashTable *global_weakmap; int replace_zend_execute_ex; int register_passes;