Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions .github/workflows/check_struct_versions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
name: check_struct_versions

# Weekly, cheap (git ls-remote only, no PHP build): checks whether any PHP
# series phpspy already tracks has a newer release tag than what's pinned in
# struct_dump.sh, or whether PHP 8.6 (currently pinned to `master`, no
# release tag yet) has finally gotten one. If so, dumps just that version on
# both architectures (tools/find_php_updates.sh already scoped this to real
# changes, so this is normally 0 cells, occasionally 2), patches the
# existing mirror headers with tools/render_struct_update.sh, and opens a PR
# for a human to review -- never auto-merges.
#
# A genuinely new PHP series beyond what phpspy supports (e.g. a future 8.7)
# is deliberately NOT handled here -- see find_php_updates.sh's stderr note.
# That needs code changes beyond struct data (phpspy.c dispatch, -V help
# text, README), which is a job for a human, not this workflow.

on:
schedule:
- cron: '0 6 * * 1' # every Monday, 06:00 UTC
workflow_dispatch:

jobs:
detect:
runs-on: ubuntu-24.04
outputs:
changes: ${{ steps.detect.outputs.changes }}
steps:
- uses: actions/checkout@v4
- id: detect
run: |
set -euo pipefail
lines=$(./tools/find_php_updates.sh)
echo "$lines"
json=$(printf '%s\n' "$lines" | awk 'NF{printf "{\"nn\":\"%s\",\"tag\":\"%s\"}\n", $1, $2}' | jq -s -c .)
echo "changes=$json" >> "$GITHUB_OUTPUT"

dump:
needs: detect
if: needs.detect.outputs.changes != '[]'
strategy:
fail-fast: false
matrix:
runs_on: [ubuntu-24.04, ubuntu-24.04-arm]
change: ${{ fromJson(needs.detect.outputs.changes) }}
runs-on: ${{ matrix.runs_on }}
steps:
- name: checkout phpspy
uses: actions/checkout@v4
with:
path: phpspy
- name: checkout php-src
uses: actions/checkout@v4
with:
repository: php/php-src
path: php-src
fetch-depth: 0
- name: deps
run: |
sudo apt-get update
sudo apt-get install -y \
autoconf bison re2c pkg-config build-essential gdb \
libxml2-dev libsqlite3-dev libssl-dev
- name: dump
env:
TAG: ${{ matrix.change.tag }}
run: ./phpspy/struct_dump.sh "$GITHUB_WORKSPACE/php-src" "$TAG"
- name: upload
env:
TAG: ${{ matrix.change.tag }}
uses: actions/upload-artifact@v4
with:
name: dump-${{ matrix.runs_on }}-${{ matrix.change.nn }}
path: phpspy/struct_dump.${{ matrix.change.tag }}.out
if-no-files-found: error

render:
needs: [detect, dump]
runs-on: ubuntu-24.04
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: install gawk
run: sudo apt-get update && sudo apt-get install -y gawk
- uses: actions/download-artifact@v4
with:
path: dumps
- name: render
env:
CHANGES: ${{ needs.detect.outputs.changes }}
run: |
set -euo pipefail
: > render_status.env
echo "$CHANGES" | jq -c '.[]' | while read -r change; do
nn=$(echo "$change" | jq -r .nn)
tag=$(echo "$change" | jq -r .tag)
tools/update_pinned_tag.sh "$tag" || echo "STATUS_${nn}_pin=failed" >> render_status.env
for pair in "ubuntu-24.04:x86_64" "ubuntu-24.04-arm:aarch64"; do
runs_on=${pair%%:*}
arch=${pair##*:}
raw="dumps/dump-${runs_on}-${nn}/struct_dump.${tag}.out"
if [ ! -f "$raw" ]; then
echo "render: missing dump artifact $raw" >&2
echo "STATUS_${nn}_${arch}=missing" >> render_status.env
continue
fi
if tools/render_struct_update.sh "$nn" "$arch" "$raw" .; then
echo "STATUS_${nn}_${arch}=ok" >> render_status.env
else
echo "STATUS_${nn}_${arch}=needs_review" >> render_status.env
fi
done
done
cat render_status.env
- name: summarize
id: summary
env:
CHANGES: ${{ needs.detect.outputs.changes }}
run: |
{
echo "body<<STRUCT_PR_BODY_EOF"
echo "Automated: struct offsets refreshed for a newer PHP release."
echo
echo "Detected by \`tools/find_php_updates.sh\`, dumped on both"
echo "architectures via \`struct_dump.sh\`, and patched in place by"
echo "\`tools/render_struct_update.sh\` (adjusts only the fields that"
echo "moved plus their immediately preceding pad -- see that script's"
echo "header comment for the exact scope and safety rules)."
echo
echo "Versions touched:"
echo "$CHANGES" | jq -r '.[] | "- \(.nn) -> \(.tag)"'
echo
if grep -q needs_review render_status.env 2>/dev/null; then
echo "**One or more architectures needed manual review and were left"
echo "unchanged -- see the \`render\` job log for exactly which field"
echo "and why.** Do not merge until those are resolved by hand."
fi
echo "STRUCT_PR_BODY_EOF"
} >> "$GITHUB_OUTPUT"
- name: open PR
env:
GH_TOKEN: ${{ github.token }}
PR_BODY: ${{ steps.summary.outputs.body }}
run: |
set -euo pipefail
if git diff --quiet; then
echo "No struct changes to open a PR for."
exit 0
fi
branch="bot/struct-update-$(date +%Y%m%d)"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "$branch"
git add structs/ struct_dump.sh
git commit -m "Update struct offsets for newer PHP release(s)"
git push origin "$branch"
title="Update struct offsets for newer PHP release(s)"
if grep -q needs_review render_status.env 2>/dev/null; then
title="[NEEDS REVIEW] $title"
fi
gh pr create --title "$title" --body "$PR_BODY" --head "$branch"
65 changes: 65 additions & 0 deletions .github/workflows/dump_structs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: dump_structs

# One-off, manually-triggered probe: builds php-src from source for every PHP
# version phpspy ships struct mirrors for, on both x86_64 and aarch64, and
# uploads the raw struct_dump.gdb output as build artifacts. This is how real
# (not guessed) aarch64 offsets get produced -- see struct_dump.sh/.gdb and
# CLAUDE.md. Not run on push/pull_request: 12 versions x 2 arches, each a full
# ./buildconf && ./configure && make, is far too slow for routine CI.

on:
workflow_dispatch:
workflow_call:

jobs:
list:
runs-on: ubuntu-24.04
outputs:
phpvs: ${{ steps.list.outputs.phpvs }}
steps:
- uses: actions/checkout@v4
- id: list
run: |
set -euo pipefail
json=$(tools/list_pinned_tags.sh | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "phpvs=$json" >> "$GITHUB_OUTPUT"

dump:
needs: list
strategy:
fail-fast: false
matrix:
runs_on: [ubuntu-24.04, ubuntu-24.04-arm]
phpv: ${{ fromJson(needs.list.outputs.phpvs) }}

runs-on: ${{ matrix.runs_on }}

steps:
- name: checkout phpspy
uses: actions/checkout@v4
with:
path: phpspy

- name: checkout php-src
uses: actions/checkout@v4
with:
repository: php/php-src
path: php-src
fetch-depth: 0

- name: deps
run: |
sudo apt-get update
sudo apt-get install -y \
autoconf bison re2c pkg-config build-essential gdb \
libxml2-dev libsqlite3-dev libssl-dev

- name: dump
run: ./phpspy/struct_dump.sh "$GITHUB_WORKSPACE/php-src" "${{ matrix.phpv }}"

- name: upload
uses: actions/upload-artifact@v4
with:
name: struct_dump-${{ matrix.runs_on }}-${{ matrix.phpv }}
path: phpspy/struct_dump.${{ matrix.phpv }}.out
if-no-files-found: warn
84 changes: 84 additions & 0 deletions .github/workflows/struct_drift_check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: struct_drift_check

# Monthly regression test: re-verifies that the checked-in struct mirrors
# for every already-supported PHP version still match a fresh from-source
# build, on both architectures. This re-dumps the exact tags already pinned
# in struct_dump.sh, so it's not about newer releases -- see
# check_struct_versions.yml for that. It exists to catch the rarer case:
# an upstream patch release on an ALREADY-pinned tag quietly changing a
# struct layout (this shouldn't happen for a fixed tag, since tags don't
# move -- but this is also a general regression test of the mirrors
# against reality, independent of whether anything upstream changed).
#
# Opens an issue, not a PR: unexpected drift here is surprising enough that
# it deserves a human looking at the actual cause before anything is
# auto-merged, rather than a bot quietly "fixing" it.

on:
schedule:
- cron: '0 6 1 * *' # 1st of the month, 06:00 UTC
workflow_dispatch:

jobs:
dump:
uses: ./.github/workflows/dump_structs.yml

check:
needs: dump
runs-on: ubuntu-24.04
permissions:
issues: write
steps:
- uses: actions/checkout@v4
- run: sudo apt-get update && sudo apt-get install -y gawk jq
- uses: actions/download-artifact@v4
with:
path: dumps
- name: compare
run: |
set -euo pipefail
mapfile -t phpvs < <(tools/list_pinned_tags.sh)
# Positional: must match phpspy's version dispatch order, same as
# struct_dump.sh's own comment says.
nns=(70 71 72 73 74 80 81 82 83 84 85 86)

: > drift.log
for i in "${!phpvs[@]}"; do
tag=${phpvs[$i]}
nn=${nns[$i]}
for pair in "ubuntu-24.04:x86_64" "ubuntu-24.04-arm:aarch64"; do
runs_on=${pair%%:*}
arch=${pair##*:}
raw="dumps/struct_dump-${runs_on}-${tag}/struct_dump.${tag}.out"
if [ ! -f "$raw" ]; then
echo "$nn/$arch ($tag): dump artifact missing entirely -- build likely failed" >> drift.log
continue
fi
before=$(sha256sum "structs/$arch/php_structs_$nn.h" | cut -d' ' -f1)
if tools/render_struct_update.sh "$nn" "$arch" "$raw" . 2>>drift.log; then
status=ok
else
status=needs_review
fi
after=$(sha256sum "structs/$arch/php_structs_$nn.h" | cut -d' ' -f1)
if [ "$before" != "$after" ] || [ "$status" = needs_review ]; then
echo "$nn/$arch ($tag): DRIFT ($status)" >> drift.log
fi
done
done
# This job only checks; it never commits. Revert any in-place
# edits render_struct_update.sh made so the working tree stays
# clean for the drift.log to be the sole output.
git checkout -- structs/
cat drift.log
echo "drift_found=$([ -s drift.log ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
id: compare
- name: open issue on drift
if: steps.compare.outputs.drift_found == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh issue create \
--title "struct_drift_check: unexpected drift in already-pinned PHP versions" \
--body "$(printf 'Monthly drift check found the checked-in struct mirrors no longer match a fresh from-source build of an already-pinned PHP tag.\n\n```\n%s\n```\n\nSee the workflow run for full logs: %s/%s/actions/runs/%s' "$(cat drift.log)" "$GITHUB_SERVER_URL" "$GITHUB_REPOSITORY" "$GITHUB_RUN_ID")"
7 changes: 5 additions & 2 deletions phpspy_trace.c
Original file line number Diff line number Diff line change
Expand Up @@ -340,8 +340,11 @@ static int trace_locals(trace_context *context, zend_op *zop, zend_execute_data
HASH_FIND(hh, entry->varmap, tmp, tmp_len, var);
if (!var) continue;
num_vars_found += 1;
/* See ZEND_CALL_VAR_NUM macro in php-src */
try_copy_proc_mem("zval", ((zval*)(remote_execute_data)) + ((int)(5 + i)), &zv, sizeof(zv));
/* See ZEND_CALL_VAR_NUM macro in php-src. The frame-slot count is
version-dependent (PHP 7.0 differs from every later version); see
phpspy_frame_slot, defined per-phpv in phpspy_trace_tpl.c and for
USE_ZEND in structs/structs.h. */
try_copy_proc_mem("zval", ((zval*)(remote_execute_data)) + ((int)(phpspy_frame_slot + i)), &zv, sizeof(zv));
try(rv, sprint_zval(context, &zv, tmp, sizeof(tmp), &tmp_len));
context->event.varpeek.entry = entry;
context->event.varpeek.var = var;
Expand Down
12 changes: 12 additions & 0 deletions phpspy_trace_tpl.c
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@
#define sprint_pdo_binds concat2(sprint_pdo_binds_, phpv)
#define sprint_pdo_bind concat2(sprint_pdo_bind_, phpv)

/* ZEND_CALL_FRAME_SLOT = ceil(sizeof(zend_execute_data) / sizeof(zval)). Measured
directly against every supported PHP version's real headers: 6 on 7.0 (which
still carries execute_data.called_scope, removed in 7.1), 5 on every later
version (sizeof(zend_execute_data) is 72 or 80 there, both of which round up
to 5 slots of 16 bytes). */
#if phpv == 70
#define phpspy_frame_slot 6
#else
#define phpspy_frame_slot 5
#endif

#include "phpspy_trace.c"

#undef concat1
Expand Down Expand Up @@ -73,6 +84,7 @@
#undef trace_pdo
#undef sprint_pdo_binds
#undef sprint_pdo_bind
#undef phpspy_frame_slot
#undef copy_executor_globals
#undef copy_zarray_bucket
#undef sprint_zstring
Expand Down
7 changes: 7 additions & 0 deletions struct_dump.gdb
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ whatis zval
fieldof zval u2.next
printf "\n"

# Cross-check for phpspy_frame_slot (phpspy_trace_tpl.c / structs/structs.h):
# ZEND_CALL_FRAME_SLOT = ceil(sizeof(zend_execute_data) / sizeof(zval)).
printf "frame_slot\n"
printf " sizeof(zend_execute_data) %lu\n", sizeof(zend_execute_data)
printf " sizeof(zval) %lu\n", sizeof(zval)
printf "\n"

printf "Bucket\n"
whatis Bucket
fieldof Bucket val
Expand Down
Loading
Loading