#!/usr/bin/env bash ## lsphp-abi-gate.test.sh — proves scripts/lsphp-abi-gate.sh can FAIL. ## ## The gate is what lets the emergency php.net lsphp build keep LiteSpeed's ## packaged PECL .so files. A gate that always says "compatible" would be worse ## than none, so every rule is exercised against synthetic header trees whose ## outcome is known: three layouts that must pass (unchanged; new member in ## padding; member appended to a *_globals struct) and five that must fail ## (member inserted mid-struct; member removed; member retyped/resized; a ## non-globals struct grown at the end; a struct embedded in another growing). ## Plus: a probe that extracts too few members must refuse, not pass. ## ## Needs gcc + pahole (dwarves). Exits 0 only if every case behaves. set -euo pipefail HERE=$(cd "$(dirname "$0")/.." && pwd) GATE="$HERE/lsphp-abi-gate.sh" for t in gcc pahole; do command -v "$t" >/dev/null 2>&1 || { echo "SKIP-FAIL: $t not installed (apt-get install gcc dwarves)" >&2; exit 1; } done T=$(mktemp -d) trap 'rm -rf "$T"' EXIT ## $1 = dir, $2 = body of struct _zend_object, $3 = body of _zend_executor_globals, ## $4 = extra declarations mk() { mkdir -p "$1/php/main" cat >"$1/php/main/php.h" < #include struct _zend_object { $2 }; struct _zend_executor_globals { $3 }; struct _holder { struct _zend_object obj; int after; }; typedef struct { int a; void *b; } anon_cache; $4 struct _zend_executor_globals executor_globals; struct _holder holder_sym; anon_cache anon_sym; EOF } BASE_OBJ='uint32_t gc; uint32_t handle; bool flag; void *ce; void *handlers;' BASE_EG='void *a; int b; void *reserved[4];' mk "$T/old" "$BASE_OBJ" "$BASE_EG" '' PASS=0; FAILN=0 expect() { # $1 = pass|fail, $2 = name, $3 = new dir local rc=0 ABI_MIN_MEMBERS=5 bash "$GATE" "$T/old" "$3" "$T/w-$2" >"$T/out-$2" 2>&1 || rc=$? if { [ "$1" = pass ] && [ "$rc" = 0 ]; } || { [ "$1" = fail ] && [ "$rc" != 0 ]; }; then PASS=$((PASS + 1)); echo "ok - $2 ($1, rc=$rc)" else FAILN=$((FAILN + 1)); echo "FAIL - $2: expected $1, got rc=$rc"; sed 's/^/ /' "$T/out-$2" fi } mk "$T/same" "$BASE_OBJ" "$BASE_EG" '' expect pass unchanged "$T/same" ## bool after `flag` lands in the 3 padding bytes before the pointer `ce` mk "$T/pad" 'uint32_t gc; uint32_t handle; bool flag; bool newflag; void *ce; void *handlers;' "$BASE_EG" '' expect pass new-member-in-padding "$T/pad" mk "$T/eg" "$BASE_OBJ" 'void *a; int b; void *reserved[4]; void *delayed_oom;' '' expect pass globals-appended "$T/eg" mk "$T/mid" 'uint32_t gc; void *inserted; uint32_t handle; bool flag; void *ce; void *handlers;' "$BASE_EG" '' expect fail member-inserted-mid-struct "$T/mid" mk "$T/rm" 'uint32_t gc; uint32_t handle; bool flag; void *handlers;' "$BASE_EG" '' expect fail member-removed "$T/rm" mk "$T/retype" 'uint32_t gc; uint64_t handle; bool flag; void *ce; void *handlers;' "$BASE_EG" '' expect fail member-resized "$T/retype" ## appended to a NON-globals struct: no old member moves, but sizeof grows — ## extensions that allocate sizeof(zend_object)+x would under-allocate mk "$T/grow" 'uint32_t gc; uint32_t handle; bool flag; void *ce; void *handlers; void *extra;' "$BASE_EG" '' expect fail non-globals-struct-grew "$T/grow" ## same change seen from the embedding struct: the report must name _holder.after ## as MOVED, i.e. the container check works on its own, not only via the size rule if grep -E '^BAD moved member _holder int_after#1' "$T/out-non-globals-struct-grew" >/dev/null; then PASS=$((PASS + 1)); echo "ok - embedded-struct-shifts-container (reported _holder.after moved)" else FAILN=$((FAILN + 1)); echo "FAIL - embedded-struct-shifts-container: _holder.after not reported"; sed 's/^/ /' "$T/out-non-globals-struct-grew" fi ## the anonymous typedef is tracked too mkdir -p "$T/anon/php/main" sed 's/typedef struct { int a; void \*b; } anon_cache;/typedef struct { int a; int z; void *b; long c; } anon_cache;/' "$T/old/php/main/php.h" >"$T/anon/php/main/php.h" expect fail anonymous-typedef-changed "$T/anon" ## a probe that finds (almost) nothing must refuse rc=0; ABI_MIN_MEMBERS=1000 bash "$GATE" "$T/old" "$T/same" "$T/w-min" >"$T/out-min" 2>&1 || rc=$? if [ "$rc" != 0 ] && grep 'probe is broken' "$T/out-min" >/dev/null; then PASS=$((PASS + 1)); echo "ok - too-few-members-refused (rc=$rc)" else FAILN=$((FAILN + 1)); echo "FAIL - too-few-members-refused: rc=$rc"; cat "$T/out-min"; fi echo "lsphp-abi-gate: $PASS passed, $FAILN failed" [ "$FAILN" = 0 ]