121 lines
6.4 KiB
Bash
121 lines
6.4 KiB
Bash
#!/bin/bash
|
|||
|
|
## lsphp-abi-gate.sh — may PHP extensions compiled against OLD_INC be loaded into
|
||
|
|
## a PHP built with NEW_INC? Used by lsphp-src-build.sh (emergency php.net build)
|
||
|
|
## before it keeps LiteSpeed's packaged PECL .so files (redis, imagick,
|
||
|
|
## memcached, igbinary, msgpack) next to a newer PHP patch release.
|
||
|
|
##
|
||
|
|
## WHY: PHP refuses a module whose ZEND_MODULE_API_NO / build-id differ (loud),
|
||
|
|
## but a changed struct layout under the SAME API number loads fine and corrupts
|
||
|
|
## memory (silent). Patch releases are not supposed to change layouts, yet the
|
||
|
|
## php2shell fix (8.x.y, 2026-10-21) did add fields — so this is measured, not
|
||
|
|
## assumed.
|
||
|
|
##
|
||
|
|
## HOW: compile one TU including the public PHP headers (each header in the list
|
||
|
|
## below that exists in BOTH trees) with -g against each tree, dump every struct/
|
||
|
|
## union with pahole (incl. anonymous typedef'd ones), and compare:
|
||
|
|
## * every pre-existing member keeps its offset and size (else FAIL)
|
||
|
|
## * a new member may only use bytes no old member used (else FAIL)
|
||
|
|
## (padding, or appended at the end)
|
||
|
|
## * a struct may change size only if it is *_globals — engine/module globals
|
||
|
|
## are singletons behind one exported symbol (executor_globals, ps_globals);
|
||
|
|
## no extension embeds or allocates them (else FAIL)
|
||
|
|
## A member that is renamed or retyped counts as removed: a false alarm that fails
|
||
|
|
## loud, never a silent pass.
|
||
|
|
##
|
||
|
|
## Usage: lsphp-abi-gate.sh OLD_INC NEW_INC WORKDIR (each *_INC contains php/)
|
||
|
|
## Env: ABI_MIN_MEMBERS (default 1000) — fewer members extracted from OLD_INC
|
||
|
|
## means the probe itself is broken; refuse rather than pass vacuously.
|
||
|
|
## Exit 0 = compatible (report on stdout), 1 = incompatible (report on stderr).
|
||
|
|
## Tested by scripts/tests/lsphp-abi-gate.test.sh.
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
OLD_INC="${1:?usage: lsphp-abi-gate.sh OLD_INC NEW_INC WORKDIR}"
|
||
|
|
NEW_INC="${2:?usage: lsphp-abi-gate.sh OLD_INC NEW_INC WORKDIR}"
|
||
|
|
WORK="${3:?usage: lsphp-abi-gate.sh OLD_INC NEW_INC WORKDIR}"
|
||
|
|
MIN="${ABI_MIN_MEMBERS:-1000}"
|
||
|
|
mkdir -p "$WORK"
|
||
|
|
|
||
|
|
HDRS=(php.h php_ini.h SAPI.h ext/standard/info.h ext/standard/php_var.h
|
||
|
|
ext/standard/php_string.h ext/standard/basic_functions.h ext/standard/url.h
|
||
|
|
ext/standard/php_incomplete_class.h ext/standard/php_smart_string.h
|
||
|
|
ext/session/php_session.h ext/json/php_json.h ext/date/php_date.h
|
||
|
|
ext/hash/php_hash.h ext/random/php_random.h ext/spl/spl_exceptions.h
|
||
|
|
ext/pcre/php_pcre.h zend_exceptions.h zend_interfaces.h zend_smart_str.h
|
||
|
|
zend_closures.h zend_generators.h zend_observer.h zend_attributes.h
|
||
|
|
zend_enum.h zend_weakrefs.h zend_fibers.h zend_extensions.h)
|
||
|
|
|
||
|
|
: >"$WORK/abi.c"
|
||
|
|
for h in "${HDRS[@]}"; do
|
||
|
|
for t in "$OLD_INC/php" "$NEW_INC/php"; do
|
||
|
|
[ -e "$t/$h" ] || [ -e "$t/main/$h" ] || [ -e "$t/Zend/$h" ] || continue 2
|
||
|
|
done
|
||
|
|
printf '#include "%s"\n' "$h" >>"$WORK/abi.c"
|
||
|
|
done
|
||
|
|
[ -s "$WORK/abi.c" ] || { echo "ABI gate: none of the probe headers exist in both trees" >&2; exit 1; }
|
||
|
|
|
||
|
|
dump() { # $1 = include root, $2 = tag
|
||
|
|
local inc="$1/php" o="$WORK/abi-$2.o"
|
||
|
|
gcc -g -O0 -c -fno-eliminate-unused-debug-types \
|
||
|
|
-I"$inc" -I"$inc/main" -I"$inc/TSRM" -I"$inc/Zend" -I"$inc/ext" -I"$inc/ext/date/lib" \
|
||
|
|
-o "$o" "$WORK/abi.c"
|
||
|
|
## M <struct> <decl>#<n> <offset[:bit]> <size> and S <struct> <size>
|
||
|
|
## Named: "struct _zend_object {" ... "};" /* size: N ... */
|
||
|
|
## Anonymous: "typedef struct {" ... "} zend_fcall_info_cache;"
|
||
|
|
pahole -a -F dwarf "$o" | awk '
|
||
|
|
/^(typedef )?(struct|union) ([A-Za-z_0-9]+ )?\{/ {
|
||
|
|
depth = 1; nm = 0; sz = ""; delete seen
|
||
|
|
name = ($1 == "typedef") ? "" : $2; next }
|
||
|
|
depth == 0 { next }
|
||
|
|
depth == 1 && /^\}/ {
|
||
|
|
if (name == "") { n = $0; sub(/^\}[ \t]*/, "", n); sub(/;.*$/, "", n); name = n }
|
||
|
|
for (i = 1; i <= nm; i++) print "M", name, mem[i]
|
||
|
|
if (sz != "") print "S", name, sz
|
||
|
|
depth = 0; next }
|
||
|
|
depth == 1 && /\/\* size: [0-9]+/ { t = $0; sub(/^.*size: /, "", t); sub(/[^0-9].*$/, "", t); sz = t; next }
|
||
|
|
/\{[ \t]*$/ { depth++; next }
|
||
|
|
/^[ \t]+\}/ { depth-- }
|
||
|
|
/;[ \t]*\/\*[ \t]+[0-9]+(:[ \t]*[0-9]+)?[ \t]+[0-9]+[ \t]+\*\// {
|
||
|
|
d = $0; sub(/;.*$/, "", d); gsub(/[ \t]+/, " ", d); sub(/^ /, "", d); gsub(/ /, "_", d)
|
||
|
|
c = $0; sub(/^.*\/\*[ \t]+/, "", c); sub(/[ \t]+\*\/.*$/, "", c); gsub(/:[ \t]+/, ":", c)
|
||
|
|
k = split(c, w, /[ \t]+/)
|
||
|
|
seen[d]++
|
||
|
|
mem[++nm] = d "#" seen[d] " " w[1] " " w[k] }
|
||
|
|
' >"$WORK/abi-$2.txt"
|
||
|
|
}
|
||
|
|
dump "$OLD_INC" old
|
||
|
|
dump "$NEW_INC" new
|
||
|
|
|
||
|
|
NOLD=$(awk '$1 == "M"' "$WORK/abi-old.txt" | wc -l)
|
||
|
|
NSTRUCT=$(awk '$1 == "S"' "$WORK/abi-old.txt" | wc -l)
|
||
|
|
if [ "$NOLD" -lt "$MIN" ]; then
|
||
|
|
echo "ABI gate: only $NOLD struct members extracted from $OLD_INC (< $MIN) — the probe is broken; refusing to call that compatible." >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
REPORT=$(awk '
|
||
|
|
FNR == NR { if ($1 == "M") { old[$2 " " $3] = $4 " " $5; ost[$2] = 1 } else if ($1 == "S") osz[$2] = $3; next }
|
||
|
|
$1 == "M" { new[$2 " " $3] = $4 " " $5; nst[$2] = 1 }
|
||
|
|
$1 == "S" { nsz[$2] = $3 }
|
||
|
|
END {
|
||
|
|
for (k in old) { split(k, p, " "); if (!(p[1] in nst)) continue
|
||
|
|
if (!(k in new)) print "BAD removed/renamed member " k " (was " old[k] ")"
|
||
|
|
else if (old[k] != new[k]) print "BAD moved member " k ": " old[k] " -> " new[k] }
|
||
|
|
for (k in new) { if (k in old) continue; split(k, p, " "); if (!(p[1] in ost)) continue
|
||
|
|
split(new[k], q, " "); s = q[1] + 0; e = s + q[2]
|
||
|
|
hit = ""
|
||
|
|
for (o in old) { split(o, r, " "); if (r[1] != p[1]) continue; split(old[o], z, " ")
|
||
|
|
if (s < z[1] + z[2] && z[1] + 0 < e) { hit = o; break } }
|
||
|
|
if (hit != "") print "BAD new member " k " at " new[k] " overlaps old " hit
|
||
|
|
else print "OK new member " k " at " new[k] " (padding/appended)" }
|
||
|
|
for (st in osz) { if (!(st in nsz) || osz[st] == nsz[st]) continue
|
||
|
|
if (st ~ /_globals$/) print "OK size " st " " osz[st] " -> " nsz[st] " (*_globals singleton)"
|
||
|
|
else print "BAD size " st " " osz[st] " -> " nsz[st] }
|
||
|
|
}' "$WORK/abi-old.txt" "$WORK/abi-new.txt" | sort)
|
||
|
|
|
||
|
|
if [[ $'\n'"$REPORT" == *$'\nBAD '* ]]; then
|
||
|
|
printf '%s\n' "$REPORT" >&2
|
||
|
|
echo "ABI gate: INCOMPATIBLE ($NOLD members / $NSTRUCT structs compared)" >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
[ -z "$REPORT" ] || printf '%s\n' "$REPORT"
|
||
|
|
echo "ABI gate: compatible ($NOLD members / $NSTRUCT structs compared)"
|