Skip to content

Fix PHP 8.6 sapi_globals offset, PHP 7.0 frame-slot bug, and get real aarch64 struct data - #165

Open
rlerdorf wants to merge 3 commits into
adsr:masterfrom
rlerdorf:fix-struct-bugs
Open

rlerdorf wants to merge 3 commits into
adsr:masterfrom
rlerdorf:fix-struct-bugs

Conversation

@rlerdorf

@rlerdorf rlerdorf commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Three bugs in the struct mirrors, fixed

1. PHP 8.6 x86_64 offset bug. sapi_globals_struct_86.global_request_time was
declared at offset 440; real PHP 8.6.0beta2 has it at 432. Effect: -r reported
# ts = 0.000000 on 8.6 instead of a real timestamp. Verified via gdb/offsetof
against an installed PHP 8.6 build, and independently reproduced by a fresh
from-source build in CI (see below).

2. PHP 7.0 frame-slot bug. The CV frame-slot constant used by --peek-var
(phpspy_trace.c, ZEND_CALL_VAR_NUM) was hardcoded as the literal 5. The real
value is ZEND_CALL_FRAME_SLOT = ceil(sizeof(zend_execute_data)/sizeof(zval)),
which is 6 on PHP 7.0 (it still carries execute_data.called_scope, removed in
7.1) and 5 on every later version. Verified against real headers for all 12
supported versions. Before: --peek-var silently found nothing on 7.0. After:
tests/test_varpeek.sh passes on 7.0; every other version is unaffected (the
new phpspy_frame_slot resolves to the same 5 they already used). Works in
both build modes -- under USE_ZEND it's just ZEND_CALL_FRAME_SLOT directly.

3. aarch64 structs were mostly untested placeholders.
structs/aarch64/php_structs_{70,71,72,73,74,80,81,82}.h were byte-identical
copies of their x86_64 counterparts with a "these structs are wrong for
aarch64" comment -- never actually measured on arm hardware. 83/84/85 carried
a real but unverified guess (a 16-byte shrink in sapi_globals_struct, since
zend_stat_t/struct stat is 128 bytes on aarch64/glibc vs 144 on
x86_64/glibc); 86 carried a guess that was wrong given the offset fixed in
(1). See below for how this was fixed with real data.

How the aarch64 data was derived

Added .github/workflows/dump_structs.yml, a workflow_dispatch-only job
(too slow for routine push/PR CI -- it's a full php-src source build per
cell) that builds every supported PHP version from source on both
ubuntu-24.04 and ubuntu-24.04-arm, using the (also updated in this PR)
struct_dump.sh/struct_dump.gdb, and uploads the raw offset dumps as
artifacts. struct_dump.sh's version list had stopped at 8.3/master; this PR
adds the php-8.4.25/php-8.5.10 tags it was missing and an optional
single-version argument so a matrix cell can dump just one version.

I ran that workflow once against a fork
(run, all 24
cells succeeded). Before touching any header, the x86_64 leg of that run was
diffed field-for-field against independently-gathered x86_64 ground truth
(gdb/offsetof against locally installed PHP 7.0-8.6 builds) -- byte-identical
across all 12 versions, which is what gives confidence the build+dump pipeline
itself is trustworthy. The aarch64 leg then showed exactly one consistent
difference from x86_64 for every version -- sapi_globals_struct.global_request_time
shifts by exactly -16 bytes (matching the zend_stat_t size delta above) -- and
nothing else in the struct surface phpspy reads differs by architecture. So:

  • 83 and 85's existing guessed delta turned out correct -- confirmed against
    real data, no change needed.
  • 70-74/80-82 needed that same delta, applied for the first time (replacing
    the placeholder copies).
  • 86 needed the new delta following the corrected x86_64 offset from (1):
    432 - 16 = 416, not the previously-guessed 424.

Verification

Full test suite run against all locally available PHP versions (7.0-8.6,
NTS, both debug and release builds) before and after this change. No new
failures; the only pre-existing failure (pdo_args_packed_array on a few
versions) is an unrelated HASH_FLAG_PACKED issue in packed-array handling,
confirmed identical with and without this patch. tests/test_varpeek.sh
specifically flips from failing to passing on PHP 7.0, which is the
regression test for bug (2).

No PHPSPY_VERSION bump included -- leaving that to your judgment on when a
new version is warranted.

🤖 Generated with Claude Code

rlerdorf and others added 3 commits September 8, 2026 07:12
…truct probe

structs/x86_64/php_structs_86.h: sapi_globals_struct_86.global_request_time
was declared at offset 440; real PHP 8.6.0beta2 has it at 432 (verified via
gdb/offsetof). Before: `-r` reported `# ts = 0.000000` on 8.6. After: a
correct wall-clock timestamp.

phpspy_trace.c/phpspy_trace_tpl.c/structs/structs.h: the CV frame-slot
constant used by --peek-var was hardcoded as the literal 5. The real value
is ZEND_CALL_FRAME_SLOT = ceil(sizeof(zend_execute_data)/sizeof(zval)),
which is 6 on PHP 7.0 (it still carries execute_data.called_scope, removed
in 7.1) and 5 on every later version -- verified against all 12 supported
versions' real headers. Before: --peek-var silently found nothing on 7.0.
After: tests/test_varpeek.sh passes on 7.0; every other version unaffected.

struct_dump.sh/struct_dump.gdb: bring the dump tooling current -- add the
missing php-8.4.25/php-8.5.10 tags (it stopped at 8.3/master), add an
optional single-version argument so a CI matrix cell can dump just one
version, and add sizeof(zend_execute_data)/sizeof(zval) cross-check lines
for the frame-slot fix above.

.github/workflows/dump_structs.yml: new, workflow_dispatch-only job that
builds php-src from source for every supported version on both x86_64 and
aarch64 and uploads the raw struct_dump.gdb output as artifacts -- this is
how real (not placeholder/guessed) aarch64 offsets get produced; see
CLAUDE.md and the phpspy CLAUDE.md for the struct-mirror background.

Verified: full test suite passes unchanged across all 13 locally installed
PHP versions (70-86, excluding asan) before and after this change; the only
failures present (pdo_args_packed_array on 72/73/74/80/81/82) are a
pre-existing, unrelated HASH_FLAG_PACKED bug, confirmed identical via
git stash before committing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8kSnhaw7WUaR61G6cBp1w
Leave version bumps to the maintainer's judgment on when a new version
is warranted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8kSnhaw7WUaR61G6cBp1w
structs/aarch64/php_structs_{70,71,72,73,74,80,81,82}.h were byte-identical
copies of their x86_64 counterparts with a "these structs are wrong for
aarch64" comment -- never actually measured on arm. 83/84/85 carried one
real but unverified guess (a 16-byte shrink in sapi_globals_struct, since
zend_stat_t/struct stat is 128 bytes on aarch64/glibc vs 144 on x86_64/glibc);
86 carried a guess that was wrong given the x86_64 fix in the prior commit.

Derived via the new dump_structs.yml workflow (workflow_dispatch): it builds
php-src from source for every supported version on both ubuntu-24.04 and
ubuntu-24.04-arm and runs the (now current) struct_dump.gdb against each
resulting binary, uploading the raw offsets as build artifacts. Run at
https://github.com/rlerdorf/phpspy/actions/runs/34219729971 (all 24 cells
succeeded).

Before touching any header, the x86_64 leg of that same run was diffed
field-for-field against this session's own independently-gathered x86_64
ground truth (gdb/offsetof against the locally installed PHP 7.0-8.6) --
byte-identical across all 12 versions, confirming the build+dump pipeline
itself is trustworthy. The aarch64 leg then showed a single, consistent
difference from x86_64 for every version: sapi_globals_struct's
global_request_time shifts by exactly -16 bytes, matching the
zend_stat_t size delta above, and nothing else in the struct surface
phpspy reads differs by architecture. That means:

- 83 and 85's existing guessed delta was correct -- confirmed against real
  hardware, no change needed.
- 70-74/80-82 needed the same delta, applied for the first time.
- 86 needed the new delta following the corrected x86_64 offset from the
  prior commit (432 - 16 = 416, not the previously-guessed 424).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8kSnhaw7WUaR61G6cBp1w
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.

1 participant