Add pure-PHP memory dumper for reli analysis#1
Merged
Conversation
Implement php-memory-dump: capture the current PHP process's own memory in reli's RDUMP v3 format from inside the process, in pure PHP with no extension and no FFI. The hard part ext-rdump gets for free from the linker — the runtime addresses of executor_globals / compiler_globals / basic_globals — is recovered here the same way reli does when attaching from outside: parse the PHP binary's ELF symbol table (.symtab / .dynsym; stock distro builds export the three engine globals in .dynsym even when stripped) and add the load bias from /proc/self/maps. Region bytes are streamed out of /proc/self/mem via fseek/fread, which reaches the full 64-bit user address range and zero-fills any unreadable page instead of faulting. The memory map is frozen before any output buffer is allocated and regions are streamed rather than accumulated, keeping the (non-stop-the-world) perturbation window small. Capture rule mirrors ext-rdump: every readable volatile mapping, plus read-only file-backed segments under full mode. Verified end to end: reli inspector:memory:analyze / :report on a PHP 8.4 NTS x86-64 self-dump reconstructs the heap, object inventory, and the call stack at the moment of capture. NTS only (ZTS globals live in TLS, out of a pure-PHP reader's reach); 64-bit little-endian Linux only. https://claude.ai/code/session_012vbYstASQ1Um7Xns7KqRMF
ElfImage slurped the whole PHP binary (~6 MB) via file_get_contents just to read its symbol table, which dominated the dumper's footprint and needlessly churned the heap being snapshotted: the measured peak added by dump() was ~6.3 MiB (emalloc) / 5.9 MiB real, essentially the binary size. Read lazily through an open handle instead — only the ELF/program/section headers and the chosen .dynsym/.dynstr (or PT_DYNAMIC) slices, a few hundred KiB total — and stream regions through a 256 KiB buffer (down from 1 MiB). Peak added by dump() drops to ~0.6-1 MiB emalloc with no new 2 MiB ZendMM chunk forced, independent of heap/output size. reli analyze/report on the resulting dump is unchanged (call stack and object inventory still reconstructed); unit suite still green. https://claude.ai/code/session_012vbYstASQ1Um7Xns7KqRMF
Public API mirroring reli's sidecar client MemoryLimitHandler::register(),
but doing the dump in-process rather than over a socket to a daemon. One
call installs a shutdown handler that snapshots the process the moment it
dies of memory_limit exhaustion:
OomDumpHandler::register('/var/log/php-oom-%p-%t.rdump');
Surviving the OOM in-process is the hard part, so two levers are on by
default: it raises memory_limit before dumping (the deterministic lever —
memory_limit is enforced on ZendMM real usage, and the process is exiting
anyway), and it frees an emergency reserve first thing in the handler. The
reserve defaults to 4 MiB because freeing a sub-2 MiB block returns it to a
chunk free-list without unmapping it, recovering zero memory_limit headroom
(a 1 MiB reserve yields a truncated, unusable dump). Path templating (%p,
%t, %%) lets one registration give each worker a distinct file; classes are
pre-loaded so autoload never runs inside the exhausted handler.
Best-effort by nature: an OOM on a VM-stack page push never lets the handler
body run at all (that case needs ext-rdump's C zend_error_cb hook).
Verified end to end: a child that exhausts a 48M limit leaves a complete,
non-truncated dump that reli analyzes, with the report flagging the very
array that overflowed the limit as the dominant retained branch. Unit suite
covers path expansion, the memory-limit predicate, and a subprocess OOM
that asserts the written dump walks cleanly to EOF.
https://claude.ai/code/session_012vbYstASQ1Um7Xns7KqRMF
Lower the floor to PHP 7.0 (matching ext-rdump's 7.0-8.5 reach) the same way reli's sidecar client does: develop in modern PHP (8.1+), ship a downgraded artifact. - composer "php": ">=7.0"; "composer downgrade" copies src/ to build/php70/src and rewrites it with Rector. - Rector's level set only reaches 7.1, so three custom rules under tools/rector fill the 7.1->7.0 gap (lifted from reli's Downgrade*ToPhp70Rector): strip nullable type hints, void return types, and class-const visibility. A sed pass handles 0o octal literals. - MemoryDumper no longer uses PHP_OS_FAMILY (7.2+); it checks PHP_OS so the platform guard itself runs on the 7.0 floor. - tools/smoke.php loads an arbitrary src tree and takes a dump, used by CI to exercise the downgraded build on PHP 7.0/7.1/7.2/7.4/8.0 (where Rector and PHPUnit can't run). - CI: modern unit tests on 8.1-8.4; build the downgrade on 8.4 and assert no 7.1+ syntax survives + lint; legacy smoke matrix on the artifact. Verified: the downgraded build is 7.0-syntax-clean (no nullable/void/const- visibility/0o), lints clean, and runs end to end — produces a valid dump reli analyzes, with match->switch and str_contains->strpos paths exercised. https://claude.ai/code/session_012vbYstASQ1Um7Xns7KqRMF
Add reli-cross-version.yml, the pure-PHP analogue of ext-rdump's same-named workflow, proving the full producer->analyzer contract across PHP versions: - build: downgrade the modern source to PHP 7.0 once on 8.4. - make-dumps: load that build on every php:7.0-cli .. php:8.5-cli image and write a self-contained (full=true) dump via ci/reli-make-dump.sh — no compile step, just autoload the downgraded tree and call dump(). - analyze: set reli up once on PHP 8.4 and analyse every dump via ci/reli-analyze.sh, asserting RDUMP magic, format version 3, the recorded origin version tag, and that inspector:memory:analyze produces a report. The script is ext-rdump's reli-analyze.sh almost verbatim — the RDUMP format is identical regardless of producer, so the analyzer side is shared. - zts-rejection: assert the NTS-only contract fails fast on php:*-zts (no bogus dump written). Verified locally (Docker Hub is unreachable in this sandbox, so the old-PHP runtimes run in GH Actions): ci/reli-make-dump.sh against the downgraded build writes a 130 MB self-contained dump, and reli-analyze.sh's exact assertions all pass on it via host reli (magic, format 3, version tag, "Memory Analysis Report"). https://claude.ai/code/session_012vbYstASQ1Um7Xns7KqRMF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR introduces a complete pure-PHP memory dumping library that captures a PHP process's own memory and writes it in reli's RDUMP format for offline analysis. The implementation requires no C extensions or FFI, making it accessible to any PHP environment.
Key Changes
MemoryDumper): Main entry point that orchestrates the dump process by freezing the memory map, resolving engine globals, and streaming regions to diskElfImage): Minimal 64-bit little-endian ELF reader that parses the PHP binary's symbol table to locateexecutor_globals,compiler_globals, andbasic_globalswithout loading the entire binary into memoryMemoryMap,MapEntry): Reads and parses/proc/self/mapsto understand the process's virtual address space layoutRegionReader): Safely reads process memory through/proc/self/memwith page-level error handling and zero-filling for unreadable regionsRdumpWriter): Streams output in reli's RDUMP format (magic "RDUMP", version 3) compatible with existing reli analysis toolsOomDumpHandler): Optional shutdown handler that captures a dump whenmemory_limitis exhausted, with configurable emergency reserves and path templatingNotable Implementation Details
fseek/freadon/proc/self/memwith explicit buffering control to reliably read arbitrary virtual addresses.dynsymwhen.symtabis unavailable (as in Debian/Ubuntu distro builds) by walkingPT_DYNAMICand sizing viaDT_GNU_HASH/DT_HASHPlatform Support
/proc/self/mapsand/proc/self/mem)https://claude.ai/code/session_012vbYstASQ1Um7Xns7KqRMF