From 27c10ddce6302912a46d72a4dcd47c89f1209544 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Wed, 24 Jun 2026 07:34:59 -0400 Subject: [PATCH 1/3] soap: do not overwrite the parsed host on a protocol-relative redirect When a redirect Location is a protocol-relative reference (//host/path), php_url_parse() already fills new_url->host, but the scheme-less redirect handling overwrote it with a copy of the request host without releasing the parsed one. That leaks a zend_string per such redirect and pins the redirect back to the original host instead of the one the server named. Inherit host and port from the request URL only when new_url->host is NULL, mirroring the scheme guard directly above. Closes GH-22434 --- ext/soap/php_http.c | 6 ++- .../bugs/protocol_relative_redirect.phpt | 49 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 ext/soap/tests/bugs/protocol_relative_redirect.phpt diff --git a/ext/soap/php_http.c b/ext/soap/php_http.c index 0bec42afbc37..e9fd68007d26 100644 --- a/ext/soap/php_http.c +++ b/ext/soap/php_http.c @@ -1151,8 +1151,10 @@ int make_http_soap_request(zval *this_ptr, zend_string_release_ex(http_body, 0); if (new_url->scheme == NULL && new_url->path != NULL) { new_url->scheme = phpurl->scheme ? zend_string_copy(phpurl->scheme) : NULL; - new_url->host = phpurl->host ? zend_string_copy(phpurl->host) : NULL; - new_url->port = phpurl->port; + if (new_url->host == NULL) { + new_url->host = phpurl->host ? zend_string_copy(phpurl->host) : NULL; + new_url->port = phpurl->port; + } if (new_url->path && ZSTR_VAL(new_url->path)[0] != '/') { if (phpurl->path) { char *t = ZSTR_VAL(phpurl->path); diff --git a/ext/soap/tests/bugs/protocol_relative_redirect.phpt b/ext/soap/tests/bugs/protocol_relative_redirect.phpt new file mode 100644 index 000000000000..e8f30ca66872 --- /dev/null +++ b/ext/soap/tests/bugs/protocol_relative_redirect.phpt @@ -0,0 +1,49 @@ +--TEST-- +SOAP client follows a protocol-relative (//host/path) redirect Location without leaking the host +--EXTENSIONS-- +soap +--SKIPIF-- + +--FILE-- +', + '', + 'ok', + ''; +} else { + http_response_code(302); + header("Location: //" . $_SERVER["HTTP_HOST"] . "/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 From 67fd6431a79618ce50475ac2d1f3af9dbb7fa1c2 Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Wed, 24 Jun 2026 23:43:30 +0200 Subject: [PATCH 2/3] ci: add workflow_dispatch bisect for Symfony Console JIT hang The nightly COMMUNITY_asan job hangs every run in the Symfony Console OutputFormatter tests (test #672, testInlineStyle) under tracing JIT, exhausting GitHub's 6h ceiling. The hang is an x86-64 tracing-JIT regression present in 8.4/8.5/8.6 but not 8.3, and does not reproduce on ARM or under qemu emulation. Add a manually-triggered bisect that runs on a native x86-64 runner, building ZTS+JIT+ASAN (matching COMMUNITY_asan) and using a Console-suite hang as the bisect signal, to pinpoint the introducing commit. --- .github/workflows/jit-hang-bisect.yml | 141 ++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 .github/workflows/jit-hang-bisect.yml diff --git a/.github/workflows/jit-hang-bisect.yml b/.github/workflows/jit-hang-bisect.yml new file mode 100644 index 000000000000..2adda56727b1 --- /dev/null +++ b/.github/workflows/jit-hang-bisect.yml @@ -0,0 +1,141 @@ +name: JIT Hang Bisect +# Manually-triggered bisect for the Symfony Console OutputFormatter tracing-JIT +# hang seen in the nightly COMMUNITY_asan job. Runs on a native x86-64 runner +# (the bug does not reproduce on ARM or under qemu emulation). +on: + workflow_dispatch: + inputs: + good: + description: 'Known-GOOD commit/tag (bug absent)' + default: 'php-8.3.0' + bad: + description: 'Known-BAD commit/tag (hang)' + default: '27c10ddce6302912a46d72a4dcd47c89f1209544' + symfony_commit: + description: 'Symfony commit to test against (CI nightly pin)' + default: 'dce1b899ab0c0e4cef9159c083ef535387a8db12' + test_timeout: + description: 'Per-step test timeout in seconds (hang detector)' + default: '300' + use_asan: + description: 'Build with ASAN+UBSAN (matches COMMUNITY_asan exactly)' + default: 'true' + +permissions: + contents: read + +jobs: + bisect: + runs-on: ubuntu-24.04 + timeout-minutes: 350 + env: + SYMFONY_DIR: ${{ runner.temp }}/symfony + TEST_TIMEOUT: ${{ inputs.test_timeout }} + USE_ASAN: ${{ inputs.use_asan }} + # Mirror the COMMUNITY_asan runtime environment + USE_ZEND_ALLOC: '0' + USE_TRACKED_ALLOC: '1' + ASAN_OPTIONS: 'exitcode=139' + UBSAN_OPTIONS: 'print_stacktrace=1' + SYMFONY_DEPRECATIONS_HELPER: 'max[total]=999' + steps: + - name: Checkout php-src (full history) + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Ensure bisect endpoints & tags are present + run: | + git remote add upstream https://github.com/php/php-src.git || true + git fetch --tags --quiet upstream + git rev-parse "${{ inputs.good }}^{commit}" + git rev-parse "${{ inputs.bad }}^{commit}" + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential autoconf automake libtool bison re2c pkg-config ccache \ + libxml2-dev libsqlite3-dev libonig-dev libssl-dev zlib1g-dev \ + php-cli php-xml php-mbstring composer + + - name: Set up Symfony test bed (pinned commit) + run: | + git clone --no-checkout --filter=blob:none https://github.com/symfony/symfony.git "$SYMFONY_DIR" + git -C "$SYMFONY_DIR" fetch --depth=1 origin "${{ inputs.symfony_commit }}" + git -C "$SYMFONY_DIR" checkout "${{ inputs.symfony_commit }}" + ( cd "$SYMFONY_DIR" && composer install --no-progress --ignore-platform-req=php+ && php ./phpunit install ) + + - name: Write bisect predicate + run: | + cat > "${{ runner.temp }}/bisect_step.sh" <<'STEP' + #!/usr/bin/env bash + # exit 0=GOOD, 1=BAD(hang), 125=SKIP(untestable) + set -u + NCPU="$(nproc)" + WS="$PWD" + rev="$(git rev-parse --short HEAD)" + export CC="ccache gcc" CXX="ccache g++" + + make distclean >/dev/null 2>&1 + ./buildconf --force >/tmp/bc.log 2>&1 || { echo "[$rev] buildconf fail -> skip"; exit 125; } + + CONF=( --enable-debug --enable-zts --enable-opcache + --enable-mbstring --enable-tokenizer + --with-libxml --enable-dom --enable-xml --enable-xmlwriter --enable-xmlreader --enable-simplexml + --enable-ctype --enable-filter --enable-fileinfo --enable-phar --enable-posix --enable-pcntl + --disable-cgi ) + if [ "${USE_ASAN:-true}" = "true" ]; then + CONF+=( "CFLAGS=-fsanitize=undefined,address -fno-sanitize-recover -DZEND_TRACK_ARENA_ALLOC" + "LDFLAGS=-fsanitize=undefined,address" ) + fi + ./configure "${CONF[@]}" >/tmp/conf.log 2>&1 || { echo "[$rev] configure fail -> skip"; tail -5 /tmp/conf.log; exit 125; } + make -j"$NCPU" >/tmp/make.log 2>&1 || { echo "[$rev] make fail -> skip"; tail -15 /tmp/make.log; exit 125; } + + PHP_BIN="$WS/sapi/cli/php" + OC="$(find "$WS" -name opcache.so -path '*/modules/*' | head -1)" + [ -x "$PHP_BIN" ] && [ -n "$OC" ] || { echo "[$rev] no binary -> skip"; exit 125; } + JIT=( -dzend_extension="$OC" -dopcache.enable_cli=1 -dopcache.jit=tracing -dopcache.jit_buffer_size=1G + -dopcache.jit_hot_loop=1 -dopcache.jit_hot_func=1 -dopcache.jit_hot_return=1 + -dopcache.jit_hot_side_exit=1 -dmemory_limit=-1 ) + "$PHP_BIN" "${JIT[@]}" -r 'exit((opcache_get_status(false)["jit"]["enabled"]??false)?0:3);' \ + || { echo "[$rev] JIT not enabled -> skip"; exit 125; } + + cd "$SYMFONY_DIR" || { echo "[$rev] no symfony -> skip"; exit 125; } + echo "[$rev] running Console suite (timeout ${TEST_TIMEOUT}s)..." + timeout "${TEST_TIMEOUT}" "$PHP_BIN" "${JIT[@]}" ./phpunit src/Symfony/Component/Console \ + --exclude-group tty --exclude-group benchmark --exclude-group intl-data \ + --exclude-group transient --exclude-group skip >/tmp/test.log 2>&1 + rc=$? + case "$rc" in + 124) echo "[$rev] HANG (timeout) -> BAD"; exit 1 ;; + 0|1|2) echo "[$rev] completed rc=$rc -> GOOD"; exit 0 ;; + *) echo "[$rev] abnormal rc=$rc (asan abort / crash) -> SKIP"; tail -5 /tmp/test.log; exit 125 ;; + esac + STEP + chmod +x "${{ runner.temp }}/bisect_step.sh" + + - name: Run git bisect + run: | + git bisect reset >/dev/null 2>&1 || true + git bisect start + git bisect bad "${{ inputs.bad }}" + git bisect good "${{ inputs.good }}" + set +e + git bisect run "${{ runner.temp }}/bisect_step.sh" | tee "${{ runner.temp }}/bisect_out.txt" + set -e + + - name: Report first-bad commit + if: always() + run: | + { + echo "## JIT hang bisect result" + echo '```' + grep -E "is the first bad commit" -A0 "${{ runner.temp }}/bisect_out.txt" || echo "(no clear first-bad — see log below)" + echo '```' + echo "### Full bisect log" + echo '```' + git bisect log 2>/dev/null || true + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + git bisect reset >/dev/null 2>&1 || true From 1535152fe162ee766fefed130fbc5338ce9325a9 Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Wed, 24 Jun 2026 23:45:59 +0200 Subject: [PATCH 3/3] ci: trigger JIT hang bisect on PR open Run the bisect automatically when a PR is opened/reopened, using the default good/bad/symfony bounds (inputs fall back to defaults since pull_request runs carry no workflow_dispatch inputs). --- .github/workflows/jit-hang-bisect.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/jit-hang-bisect.yml b/.github/workflows/jit-hang-bisect.yml index 2adda56727b1..f641628813cb 100644 --- a/.github/workflows/jit-hang-bisect.yml +++ b/.github/workflows/jit-hang-bisect.yml @@ -3,6 +3,8 @@ name: JIT Hang Bisect # hang seen in the nightly COMMUNITY_asan job. Runs on a native x86-64 runner # (the bug does not reproduce on ARM or under qemu emulation). on: + pull_request: + types: [opened, reopened] workflow_dispatch: inputs: good: @@ -30,8 +32,8 @@ jobs: timeout-minutes: 350 env: SYMFONY_DIR: ${{ runner.temp }}/symfony - TEST_TIMEOUT: ${{ inputs.test_timeout }} - USE_ASAN: ${{ inputs.use_asan }} + TEST_TIMEOUT: ${{ inputs.test_timeout || '300' }} + USE_ASAN: ${{ inputs.use_asan || 'true' }} # Mirror the COMMUNITY_asan runtime environment USE_ZEND_ALLOC: '0' USE_TRACKED_ALLOC: '1' @@ -48,8 +50,8 @@ jobs: run: | git remote add upstream https://github.com/php/php-src.git || true git fetch --tags --quiet upstream - git rev-parse "${{ inputs.good }}^{commit}" - git rev-parse "${{ inputs.bad }}^{commit}" + git rev-parse "${{ inputs.good || 'php-8.3.0' }}^{commit}" + git rev-parse "${{ inputs.bad || '27c10ddce6302912a46d72a4dcd47c89f1209544' }}^{commit}" - name: Install build dependencies run: | @@ -62,8 +64,8 @@ jobs: - name: Set up Symfony test bed (pinned commit) run: | git clone --no-checkout --filter=blob:none https://github.com/symfony/symfony.git "$SYMFONY_DIR" - git -C "$SYMFONY_DIR" fetch --depth=1 origin "${{ inputs.symfony_commit }}" - git -C "$SYMFONY_DIR" checkout "${{ inputs.symfony_commit }}" + git -C "$SYMFONY_DIR" fetch --depth=1 origin "${{ inputs.symfony_commit || 'dce1b899ab0c0e4cef9159c083ef535387a8db12' }}" + git -C "$SYMFONY_DIR" checkout "${{ inputs.symfony_commit || 'dce1b899ab0c0e4cef9159c083ef535387a8db12' }}" ( cd "$SYMFONY_DIR" && composer install --no-progress --ignore-platform-req=php+ && php ./phpunit install ) - name: Write bisect predicate @@ -119,8 +121,8 @@ jobs: run: | git bisect reset >/dev/null 2>&1 || true git bisect start - git bisect bad "${{ inputs.bad }}" - git bisect good "${{ inputs.good }}" + git bisect bad "${{ inputs.bad || '27c10ddce6302912a46d72a4dcd47c89f1209544' }}" + git bisect good "${{ inputs.good || 'php-8.3.0' }}" set +e git bisect run "${{ runner.temp }}/bisect_step.sh" | tee "${{ runner.temp }}/bisect_out.txt" set -e