Skip to content

[WIP] feat: add PHP 8.6 support - #4273

Draft
Leiyks wants to merge 31 commits into
masterfrom
leiyks/php-8.6-support
Draft

Leiyks wants to merge 31 commits into
masterfrom
leiyks/php-8.6-support

Conversation

@Leiyks

@Leiyks Leiyks commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

No description provided.

Leiyks and others added 10 commits October 5, 2026 16:19
Pin the git-built extensions (memcached, memcache, xdebug, phpredis) to
commits. On 8.6, build apcu/ast from git, apply the unmerged memcache 8.6
PRs, patch rdkafka 6.0.5 and sqlsrv 5.13.3, and pin mongodb and parallel.
Stop loading opcache.so for apache on 8.5+, where opcache is built in.
BOOKWORM_NEXT_VERSION is temporarily set back to 11, since all consumers
use the _bookworm-11 tags and no bookworm-12 base exists. Set it back to
12 before merging.
Official PHP 8.6 Windows builds use vs18, so extensions must be built with
it too. VS 2026 doesn't offer the 10.0.19041 SDK; use 10.0.26100.
The bookworm images build with clang, where the removed zval_dtor() and
EMPTY_SWITCH_DEFAULT_CASE() are hard errors. Also run the new 8.6 build
steps as separate commands so set -e catches failures instead of
silently skipping the extension.
- Add 8.6 (API 20260924) to the generator version list, plus
  TEST_INTEGRATIONS_86/TEST_WEB_86 and the 8.6 xfail list (copied from 8.5).
- Windows ZTS test_c and profiler arm64 jobs now run the two newest versions,
  so 8.5 keeps its coverage.
- PHP lint, Configuration Consistency and the sidecar uid verify job stay on
  the newest GA release ($latest_ga_minor_major, 8.5 until 8.6 GA).
- $windows_max_version switch (default 8.6) gates Windows jobs and tells the
  Windows packaging job the newest API to bundle.
- $sury_max_version gate keeps 8.6 out of "verify debian": Sury only ships
  8.6.0~beta3, whose API predates 20260924.
- verify alpine uses php:8.6-rc-fpm-alpine until GA.
- run-tests.php is parallel by default on 8.6: pass -j1 where tests ran
  serially (profiler phpt, x-profiling phpt, loader).
- Root docker-compose and prof_asan GitHub workflow get 8.6.
- Add 20260924 to the final-artifact, SSI and debug-artifact API lists.
- Skip Windows APIs above WINDOWS_MAX_PHP_API so the bundle does not fail
  when Windows 8.6 is switched off.
- package.xml max 8.6.99, datadog-setup.php supports 8.6.
- verify_packages: no php8.6-opcache on Debian (built in since 8.5);
  php86 binaries and /etc/php86 on Alpine.
- XtOffsetOf was removed: use offsetof.
- ZEND_RESULT_CODE was removed: use zend_result (auto_flush.h now includes
  ext/compatibility.h for the PHP 7 typedef).
- php_hash_bin2hex was removed: use zend_bin2hex on 8.6.
- PG(error_log) is a zend_string* on 8.6.
- zend_ini_string() returns const char* on 8.6 (hard error in C++ tests).

Co-authored-by: Bob Weinand <bob.weinand@datadoghq.com>
Raise MAX_API_VERSION to 420260924 and MAX_PHP_VERSION to 8.6.

_php_error_log() takes zend_string* arguments since PHP 8.6. The loader is
built once (against 8.3), so with DD_TRACE_DEBUG=1 on 8.6 it passed a char
buffer as a zend_string and logged garbage (out-of-bounds read). Dispatch on
the runtime module API number, which is now recorded before the first log
line.
@datadog-prod-us1-4

datadog-prod-us1-4 Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Pipelines  Tests

✨ Unblock PR with BitsAI

❌ Errors

Your PR has failed checks. Please review the issues below and take necessary action before merging.

🚦 62 Pipeline jobs failed

DataDog/apm-reliability/dd-trace-php | ASAN test_c: [7.4, amd64] — 🔧 Needs a code fix, caused by this PR

View more details · View in GitLab

DataDog/apm-reliability/dd-trace-php | appsec integration tests (ssi): [test8.3-release-ssi] — 🔧 Needs a code fix, caused by this PR

View more details · View in GitLab

DataDog/apm-reliability/dd-trace-php | appsec integration tests: [test7.0-release-zts] — 🔧 Needs a code fix, caused by this PR

View more details · View in GitLab

View all 62 failed jobs.

⚠️ Warnings

❄️ 10 New flaky tests detected

tmp/build_extension/tests/ext/live-debugger/exception-replay_internal_function_args.phpt (Exception replay names the arguments of an internal function frame) from PHP.tmp.build_extension.tests.ext.live.debugger
002&#43;   [&#34;dividend&#34;]=&gt;
002-   [&#34;num1&#34;]=&gt;
009&#43;   [&#34;divisor&#34;]=&gt;
009-   [&#34;num2&#34;]=&gt;
ext/curl/tests/curl_getinfo_CURLINFO_HEADER_OUT.phpt (curl_getinfo CURLINFO_HEADER_OUT) from php.ext.curl.tests
--
     Host: localhost:%d
     %s
     Request-num: 1
006&#43; x-datadog-sampling-priority: 1
007&#43; x-datadog-tags: _dd.p.tid=6ac6430900000000,_dd.p.dm=-0
008&#43; x-datadog-trace-id: 4762781862918616660
009&#43; x-datadog-parent-id: 4762781862918616660
010&#43; traceparent: 00-6ac64309000000004218ccdeb9eff654-4218ccdeb9eff654-03
011&#43; tracestate: dd=p:4218ccdeb9eff654;t.dm:-0,ot=rv:a88ed695d66c70;th:0
...

View in Flaky Test Management

ℹ️ Info

No other issues found (see more)

🧪 All tests passed

🎯 Code Coverage (details)
• Patch Coverage: 81.82%
• Overall Coverage: 56.01% (-12.41%)

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 83a8b71 | Docs | View more details | Give us feedback!

Leiyks added 14 commits October 5, 2026 17:30
…uilds

Split the >= 8.5 git extension builds (memcached, memcache, xdebug,
phpredis) into one command per step so set -e stops on failures. Apply the
memcache PR #120 + #122 delta from a committed patch instead of fetching
unmerged PR commits by SHA. Check the sha256 of the patched rdkafka/sqlsrv
tarballs and fail if the sed patches left a site unpatched.
Restore BOOKWORM_NEXT_VERSION=12 and point just the php-8.6 service (tag
and base image) at BOOKWORM_CURRENT_VERSION, so the other ci-images
Bookworm entries keep their -12 tags and nothing needs reverting.
Start-BitsTransfer probes with HEAD, which the GitHub releases mirror
(added in #4265) rejects with 405, so every Windows CI image build failed
before reaching docker build. GET on the same URL returns 200 and the
expected sha256.
…-n for the -j probe

Also drop the unused leaked ABI_NO variable from the link tracing extension jobs.
- ZEND_RESULT_CODE was removed: use int, like dd_appsec_rshutdown().
- zval_dtor was removed: use zval_ptr_dtor_nogc.
- ZEND_LTOA was removed: format with ZEND_LONG_FMT.
- php_verror lost its params argument.
- php_hash_bin2hex was removed: use zend_bin2hex on 8.6.
- Drop the 8.5 prerelease skip from module_order_opcache.phpt.
- Gradle matrix and images for 8.6.0RC2; the RC tarball URL comes from
  images.gradle (PHP_TARBALL_URL build arg).
- build_dev_php.sh: fix the 85000 typo so 8.5+ use the built-in opcache,
  and build Xdebug from the same master pin as the bookworm CI image on 8.6.
- Add test8.6-release and test8.6-release-zts jobs.

The 8.6 image digests still need to be pushed and added to
tag_mappings.gradle.
ZEND_INTERNAL_FUNCTION is now a zend_function_type enumerator, so re-export
it under a php_function_type_enum cfg. The ZTS executor_globals_offset is
negative on 8.6 (EG lives before the TSRM cache), so apply it with
byte_offset instead of byte_add.
OPcache is compiled into PHP 8.5+ and its zend_extension has no handle, so
ddog_php_jit_enabled() always returned false. Skip the handle check and
resolve symbols through RTLD_DEFAULT, like the tracer fix in #4226.
- phpinfo blames JIT for disabled allocation profiling only on PHP versions
  where JIT actually disables it (also fixes 8.4.7+ and patched 8.1/8.2).
- jit_04/jit_05 assert `true`/`false` with JIT on PHP 8.5+, skip without JIT.
- post-install.sh selects ddtrace-<api>-alpine-zts.so on ZTS Alpine instead
  of the glibc ZTS build (official php:8.6 Alpine images are ZTS).
php-src #22487 replaced the scanner errloc and the parser location with
line_start/line before 8.6.0RC2, so the persistent decode overrides wrote
to the wrong method slots and every 8.6 MINIT crashed.
PHP 8.6 stores zend_ini_entry->def and ini_get_all(..., true) reads it,
so registering the defs from a stack array left a dangling pointer.
The 8.6 appsec and profiler compile jobs pushed package loader to 52
needs, above GitLab's limit of 50, so the package child pipeline was
never created.
PHP 8.6's TAILCALL VM tail-calls ZEND_VM_LEAVE/ZEND_VM_ENTER_EX instead of
returning to execute_ex (php-src a135ac7736). The generator trampoline's call
handler returned a plain opline, so execute_ex ran the post-op ZEND_RETURN on
its stale execute_data (the userland caller), losing the generator and never
firing the end hook. Set ZEND_VM_ENTER_BIT so execute_data is reloaded.

username_success_tab_newline.phpt dumps object ids, so the language test
runner turns it into EXPECTF, where %0A/%0D/%09 match NUL bytes (same as the
already-xfailed *_null_byte.phpt tests).
static const zend_op *ZEND_OPCODE_HANDLER_CCONV zai_interceptor_handle_created_generator_call(void) {
zai_interceptor_handle_created_generator_func();
// Since 8.6 the TAILCALL VM's ZEND_VM_LEAVE no longer returns to execute_ex, whose execute_data may be stale: set ZEND_VM_ENTER_BIT to reload it.
return (const zend_op *)((uintptr_t)&zai_interceptor_generator_post_op[2] | 1 /* ZEND_VM_ENTER_BIT */);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning ZEND_VM_ENTER_BIT when the call vm is executed will actually give you an off-by-one jump into the function. Care has to be taken to only do this when the tail call vm is actually used.

#if PHP_VERSION_ID >= 80600
zend_bin2hex(exception_hash, (const unsigned char *)&exception_long_hash, sizeof(exception_long_hash));
#else
php_hash_bin2hex(exception_hash, (unsigned char *)&exception_long_hash, sizeof(exception_long_hash));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please #define php_hash_bin2hex zend_bin2hex in compatibility.h instead.

@pr-commenter

pr-commenter Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Benchmarks [ appsec ]

Benchmark execution time: 2026-10-07 13:15:17

Comparing candidate commit 83a8b71 in PR branch leiyks/php-8.6-support with baseline commit 736b037 in branch master.

📊 Benchmarking dashboard

Found 0 performance improvements and 0 performance regressions! Performance is the same for 12 metrics, 0 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

Comment thread .gitlab/generate-tracer.php Outdated
Comment on lines +200 to +204
// Oldest and two newest supported Windows targets, kept in sync automatically.
windows_test_c_job("windows test_c: zts", "zts", array_values(array_unique(array_merge(
[reset($windows_minor_major_targets)],
array_slice($windows_minor_major_targets, -2)
))));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Comment thread .gitlab/generate-profiler.php Outdated
// across PHP versions (both LP64), so we only run the newest version.
$arm64_latest = [end($profiler_minor_major_targets)];
// across PHP versions (both LP64), so we only run the two newest versions.
$arm64_latest = array_slice($profiler_minor_major_targets, -2);

@bwoebi bwoebi Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Comment thread .gitlab/generate-common.php Outdated
Comment on lines +23 to +26
// Newest PHP with Windows support. PHP 8.6 needs the vs18 (VS 2026) toolchain;
// set this to "8.5" if that cannot run on our Windows runners (no 8.6 Windows artifact ships then).
$windows_max_version = "8.6";
$windows_minor_major_targets = array_values(array_filter($all_minor_major_targets, function($v) use ($windows_max_version) { return version_compare($v, "7.2", ">=") && version_compare($v, $windows_max_version, "<="); }));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unneeded

@pr-commenter

pr-commenter Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Benchmarks [ tracer ]

Benchmark execution time: 2026-10-07 13:58:23

Comparing candidate commit 83a8b71 in PR branch leiyks/php-8.6-support with baseline commit 736b037 in branch master.

📊 Benchmarking dashboard

Found 0 performance improvements and 0 performance regressions! Performance is the same for 192 metrics, 2 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

Unstable benchmarks

These benchmarks have a confidence interval too wide to call a change; treat them as noise rather than signal.

scenario:ComposerTelemetryBench/benchTelemetryParsing

  • unstable execution_time [-235.414ns; +1635.414ns] or [-1.418%; +9.852%]

scenario:LaravelBench/benchLaravelDdprof-opcache

  • unstable execution_time [-955.975µs; +783.895µs] or [-7.140%; +5.855%]

Leiyks added 7 commits October 7, 2026 14:31
…rix tweaks

- interceptor: on 8.6 only the TAILCALL VM gets ZEND_VM_ENTER_BIT, via a
  separate handler picked at runtime with zend_vm_kind(); CALL returns
  &post_op[2] like 8.5.
- Alias php_hash_bin2hex to zend_bin2hex in compatibility.h (tracer, appsec)
  instead of inline #if at the call sites.
- Windows ZTS back to oldest + newest, profiler arm64 back to newest only.
- Drop the $windows_max_version switch and WINDOWS_MAX_PHP_API.
zai_hook_safe_finish moves rsp onto a malloc'd stack. Valgrind only treats an
SP change as a stack switch if it lands in another registered stack or moves
more than --max-stackframe (2MB). Otherwise it handles it as a normal stack
adjustment and marks the memory in between as noaccess/undefined. That is how
the 8.6 valgrind job flagged fibers/fiber_observer_bailout.phpt: invalid
writes in zai_hook_safe_finish, then uninitialised reads up to
zend_fiber_destroy_context. Register both sides of the switch.

Verified locally with --max-stackframe=4G to force the near-stack layout: 327
errors before, 0 after.
PHP 8.6 caches static closures that have no bound variables in the declaring
function's runtime cache until the request ends (ZEND_DECLARE_LAMBDA_FUNCTION,
EG(lambda_cache)). The WeakReference therefore stays alive even on vanilla
PHP without ddtrace. Use a non-static closure so the test checks trace()
again.
PHP 8.6 asserts get_property_ptr_ptr is never called with BP_VAR_IS. BP_VAR_W
takes the same path IS did before (no undefined-property warning, unlike RW).
8.6 converts internal arg_info to zend_arg_info with a zend_string name, so
reading it as zend_internal_arg_info gave garbage names and the wrong stride.
- run-tests: print termsig for signaled tests (8.6 spawns without a shell)
- silence 8.6 return-in-finally deprecation and hexdec overflow notice in phpts
- guzzle web fixture: avoid the deprecated Is class name in the JSON response
- xfail new 8.6 language tests affected by injected headers / object handles
- drop frankenphp and cakephp_latest from 8.6 until upstream supports it
…filter_register tests

Without the 8.6 shell wrapper, stdout EOF often arrives before the process has
exited, so proc_get_status() reports running and the signal is lost.
Comment on lines +39 to +41
#if PHP_VERSION_ID >= 80600
# define php_hash_bin2hex zend_bin2hex
#endif

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we usually do it the other way around. Change to use the modern name, then alias the modern name to the old one in compatibility.h

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, that was me confusing the old and new named when I suggested that before. (and he then did >= 80600 :-D)

Comment on lines +37 to +39
if [[ -n ${PHP_TARBALL_URL:-} ]]; then
download_url=$PHP_TARBALL_URL
elif [[ $version_id -lt 50400 ]]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of this indirection via images.gradle, just put the url directly here, like is done for museum.php.net

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants