diff --git a/.github/workflows/real-time-benchmark.yml b/.github/workflows/real-time-benchmark.yml index b017a1b67c07..559eba5fd0b0 100644 --- a/.github/workflows/real-time-benchmark.yml +++ b/.github/workflows/real-time-benchmark.yml @@ -16,6 +16,15 @@ on: options: - "0" - "1" + binary_layout_strategy: + description: 'How the binary layout is generated' + required: false + default: "" + type: choice + options: + - "" + - "bolt_align" + - "bolt" collect_extended_perf_stats: description: 'Whether to collect extended perf stats as artifacts' required: true @@ -58,6 +67,7 @@ jobs: BASELINE_COMMIT: "d5f6e56610c729710073350af318c4ea1b292cfe" ID: "master" JIT: "1" + BINARY_LAYOUT_STRATEGY: "" COLLECT_EXTENDED_PERF_STATS: "0" DEBUG_ENVIRONMENT: "0" RUN_MICRO_BENCH: "0" @@ -90,6 +100,7 @@ jobs: echo "ID=benchmarked" >> $GITHUB_ENV echo "JIT=${{ inputs.jit }}" >> $GITHUB_ENV + echo "BINARY_LAYOUT_STRATEGY=${{ inputs.binary_layout_strategy }}" >> $GITHUB_ENV echo "COLLECT_EXTENDED_PERF_STATS=${{ inputs.collect_extended_perf_stats }}" >> $GITHUB_ENV echo "DEBUG_ENVIRONMENT=${{ inputs.debug_environment }}" >> $GITHUB_ENV echo "RUN_MICRO_BENCH=${{ inputs.run_micro_bench }}" >> $GITHUB_ENV @@ -231,6 +242,7 @@ jobs: cp ./php-version-benchmarks/config/infra/aws/x86_64-metal.ini.dist ./php-version-benchmarks/config/infra/aws/x86_64-metal.ini sed -i "s|INFRA_DOCKER_REGISTRY=public.ecr.aws/abcdefgh|INFRA_DOCKER_REGISTRY=${{ secrets.PHP_VERSION_BENCHMARK_DOCKER_REGISTRY }}|g" ./php-version-benchmarks/config/infra/aws/x86_64-metal.ini sed -i "s|INFRA_WORKSPACE=|INFRA_WORKSPACE=$WORKSPACE|g" ./php-version-benchmarks/config/infra/aws/x86_64-metal.ini + sed -i "s/INFRA_BINARY_LAYOUT_STRATEGY=/INFRA_BINARY_LAYOUT_STRATEGY=${{ env.BINARY_LAYOUT_STRATEGY }}/g" ./php-version-benchmarks/config/infra/aws/x86_64-metal.ini sed -i "s/INFRA_COLLECT_EXTENDED_PERF_STATS=0/INFRA_COLLECT_EXTENDED_PERF_STATS=${{ env.COLLECT_EXTENDED_PERF_STATS }}/g" ./php-version-benchmarks/config/infra/aws/x86_64-metal.ini sed -i "s/INFRA_DEBUG_ENVIRONMENT=0/INFRA_DEBUG_ENVIRONMENT=${{ env.DEBUG_ENVIRONMENT }}/g" ./php-version-benchmarks/config/infra/aws/x86_64-metal.ini diff --git a/NEWS b/NEWS index d7c338421cf4..d0510be26579 100644 --- a/NEWS +++ b/NEWS @@ -20,6 +20,10 @@ PHP NEWS . Fixed bug GH-23418 (Use-after-free when looking up mounted directories). (Weilin Du) +- Standard: + . Fixed an out-of-bounds read when following a redirect response with an + empty Location header. (iliaal) + 27 Aug 2026, PHP 8.6.0beta2 diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c index 575b54b11a24..02de1a543da9 100644 --- a/Zend/zend_alloc.c +++ b/Zend/zend_alloc.c @@ -1430,6 +1430,12 @@ static zend_always_inline void zend_mm_free_small(zend_mm_heap *heap, void *ptr, #endif p = (zend_mm_free_slot*)ptr; +#if ZEND_MM_HEAP_PROTECTION + /* Catch the most common double-free pattern for free. */ + if (UNEXPECTED(p == heap->free_slot[bin_num])) { + zend_mm_panic("zend_mm_heap corrupted (double free)"); + } +#endif zend_mm_set_next_free_slot(heap, bin_num, p, heap->free_slot[bin_num]); heap->free_slot[bin_num] = p; } diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c index 9301536458a6..266d5001b81f 100644 --- a/ext/standard/http_fopen_wrapper.c +++ b/ext/standard/http_fopen_wrapper.c @@ -1054,7 +1054,7 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, char *new_path = NULL; - if (strlen(header_info.location) < 8 || + if (header_info.location_len < 8 || (strncasecmp(header_info.location, "http://", sizeof("http://")-1) && strncasecmp(header_info.location, "https://", sizeof("https://")-1) && strncasecmp(header_info.location, "ftp://", sizeof("ftp://")-1) && @@ -1062,7 +1062,7 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, { char *loc_path = NULL; if (*header_info.location != '/') { - if (*(header_info.location+1) != '\0' && resource->path) { + if (header_info.location_len > 0 && resource->path) { char *s = strrchr(ZSTR_VAL(resource->path), '/'); if (!s) { s = ZSTR_VAL(resource->path); diff --git a/ext/standard/tests/http/http_empty_location_redirect.phpt b/ext/standard/tests/http/http_empty_location_redirect.phpt new file mode 100644 index 000000000000..a7f99bf1e249 --- /dev/null +++ b/ext/standard/tests/http/http_empty_location_redirect.phpt @@ -0,0 +1,36 @@ +--TEST-- +Empty Location header must not over-read when building the redirect target +--FILE-- + ['follow_location' => 1]]); +echo @file_get_contents("http://{{ ADDR }}/a/b", false, $ctx), "\n"; +CODE; + +include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--EXPECT-- +uri=/ diff --git a/sapi/phpdbg/phpdbg_watch.c b/sapi/phpdbg/phpdbg_watch.c index 1e67d2c57670..a2e39e15a6ca 100644 --- a/sapi/phpdbg/phpdbg_watch.c +++ b/sapi/phpdbg/phpdbg_watch.c @@ -847,8 +847,36 @@ bool phpdbg_try_re_adding_watch_element(zval *parent, phpdbg_watch_element *elem return true; } +/* watch_recreation is keyed by element->str, so a parent and its child are stored + * under distinct keys and the deduplication in phpdbg_queue_element_for_recreation() + * cannot notice that they belong to the same chain. As phpdbg_free_watch_element_tree() + * frees the whole chain, any other entry referencing a member of it must be dropped + * first, or the chain gets freed twice. The buckets are only nulled out as the hash is + * being iterated over by the callers; they clean it right after. */ +static void phpdbg_forget_queued_watch_element(phpdbg_watch_element *element) { + zval *zv = zend_hash_find(&PHPDBG_G(watch_recreation), element->str); + if (zv && Z_PTR_P(zv) == element) { + Z_PTR_P(zv) = NULL; + } +} + +static void phpdbg_dequeue_watch_element_tree(phpdbg_watch_element *element) { + phpdbg_watch_element *cur; + + for (cur = element->parent; cur; cur = cur->parent) { + phpdbg_forget_queued_watch_element(cur); + } + for (cur = element->child; cur; cur = cur->child) { + phpdbg_forget_queued_watch_element(cur); + } + phpdbg_forget_queued_watch_element(element); +} + void phpdbg_automatic_dequeue_free(phpdbg_watch_element *element) { phpdbg_watch_element *child = element; + + phpdbg_dequeue_watch_element_tree(element); + while (child->child && !(child->flags & PHPDBG_WATCH_RECURSIVE_ROOT)) { child = child->child; } @@ -863,6 +891,10 @@ void phpdbg_dequeue_elements_for_recreation(void) { phpdbg_watch_element *element; ZEND_HASH_MAP_FOREACH_PTR(&PHPDBG_G(watch_recreation), element) { + if (!element) { + /* freed along with an already dequeued element of the same chain */ + continue; + } ZEND_ASSERT(element->flags & (PHPDBG_WATCH_IMPLICIT | PHPDBG_WATCH_RECURSIVE_ROOT | PHPDBG_WATCH_SIMPLE)); if (element->parent || zend_hash_index_find(&PHPDBG_G(watch_free), (zend_ulong)(uintptr_t) element->parent_container)) { zval _zv, *zv = &_zv; @@ -1641,7 +1673,9 @@ void phpdbg_destroy_watchpoints(void) { /* unconditionally free all remaining elements to avoid memory leaks */ ZEND_HASH_MAP_FOREACH_PTR(&PHPDBG_G(watch_recreation), element) { - phpdbg_automatic_dequeue_free(element); + if (element) { + phpdbg_automatic_dequeue_free(element); + } } ZEND_HASH_FOREACH_END(); /* upon fatal errors etc. (i.e. CG(unclean_shutdown) == 1), some watchpoints may still be active. Ensure memory is not watched anymore for next run. Do not care about memory freeing here, shutdown is unclean and near anyway. */ @@ -1669,7 +1703,9 @@ void phpdbg_release_watch_elements(void) { uint32_t guard; ZEND_HASH_MAP_FOREACH_PTR(&PHPDBG_G(watch_recreation), element) { - phpdbg_automatic_dequeue_free(element); + if (element) { + phpdbg_automatic_dequeue_free(element); + } } ZEND_HASH_FOREACH_END(); zend_hash_clean(&PHPDBG_G(watch_recreation));