Skip to content

opcache: bounds-validate relocation offsets and document the trust model - #270

Merged
lisachenko merged 1 commit into
8.4from
claude/opcache-123-bounds-validation
Aug 20, 2026
Merged

opcache: bounds-validate relocation offsets and document the trust model#270
lisachenko merged 1 commit into
8.4from
claude/opcache-123-bounds-validation

Conversation

@lisachenko

Copy link
Copy Markdown
Owner

What this changes

Fixes #123 — final PR of the relocator/serializer chain (stacked on #269).

Every stored offset/count/span is validated in the relocate() (untrusted-input) path before dereference: requireOffset ([0, memSize]), requireStringOffset (tagged [0, strSize)), requireSpan/requireCount across every count-driven array (buckets, literals, arg_info including the [-1] return slot, vars, type lists, attribute args, property tables, propInfoTable, class/trait names, NUL-terminated alias/precedence arrays + excludes, hooks, dynamic_func_defs, ast, warnings, early bindings). Violations throw OpCacheException::malformedPayload. The tagged bound tracks the section serialize() rebuilds, so the re-relocate inside derelocate() validates correctly; serialize() and the #117 graph serializer inherit validation (they operate on the already-relocated image). Covers the payload shapes added across the whole chain.

Trust model documented in docs/opcache-binary.md: a .bin must come from a trusted source; system_id is a build fingerprint, not authenticity; adler32 is an accidental-corruption check, not tamper protection; the bounds checks are defense-in-depth that convert a memory-safety catastrophe into a clean exception, not a licence to load untrusted code. A keyed MAC is noted as a deploy-side responsibility / possible follow-up, deliberately not built in.

BoundsValidationTest: truncated buffer, out-of-range scriptOffset, hostile pointer field, hostile hash count, out-of-range interned offset — all refused; a well-formed image still relocates + round-trips (no false positives). Run in the debug84 + debug84-zts containers (where a bounds miss segfaults loudest) — no crashes, clean refusals.

Environment it was verified on

  • PHP version (full first line of php -v): PHP 8.4.19 (cli) (built: Mar 30 2026 19:28:35) (NTS) + ZTS debug container
  • Thread safety: NTS + ZTS
  • OS / architecture: Linux x86-64 (Ubuntu)
  • Debug build (--enable-debug)? yes — debug84 NTS + debug84-zts, opcache gate 64 each, malformed-payload refusals crash-free

Also: host 536 tests, opcache gate 64; PHPStan level max clean; cs-fixer clean.

Checklist

  • Targets the minimum affected version branch (chain → 8.4)
  • composer test passes on the matching PHP minor
  • composer phpstan (level max) and composer cs:check are green
  • Tests added or updated; new files carry the same path-scoped ignores the config already grants PayloadRelocator — no baseline additions
  • tools/generator/symbols.php unchanged — nothing generated touched
  • Conventional Commits used for the commit messages

🤖 Generated with Claude Code

https://claude.ai/code/session_01BDcCQiYqbMkjRPyhWgLL6M


Generated by Claude Code

…ust model

Defense-in-depth for the file-cache reader (issue #123). PayloadRelocator
turned every stored offset into base + stored and drove its loops off count
fields read straight from the payload, without range checks - so a crafted
.bin carrying the current build's system_id (a build fingerprint, not an
authenticator; adler32 is forgeable) fed into getReflection() was an FFI
arbitrary read/write primitive.

Now every stored value is validated before it becomes an address the engine
walks, in the relocate() (untrusted-input) path:
- requireOffset: interior-pointer offsets against [0, memSize]
- requireStringOffset: tagged interned-string offsets against [0, strSize),
  plain string offsets against [0, memSize]
- requireSpan / requireCount: scriptOffset and every count-driven element
  array - hashtable buckets/packed data, literals, arg_info (incl. the
  arg_info[-1] return slot), vars, type lists, attribute args, class property
  tables, properties_info_table, class/trait names, trait alias/precedence
  NUL-terminated arrays and their exclude lists, property hooks,
  dynamic_func_defs, ast children/nodes, warnings and early bindings
A violation throws OpCacheException::malformedPayload - a loud refusal, never
an out-of-bounds walk. The derelocate()/serialize() path and the #117 graph
ScriptSerializer operate on an already-relocated in-process image and inherit
this validation. The tagged-offset bound tracks the string section serialize()
just rebuilt (re-pinned from the header str_size), so the relocate() inside
derelocate() validates against the section it produced, not the stale size.

docs/opcache-binary.md gains a "Trust model" section: .bin input must come
from a trusted source; system_id is a build fingerprint (ABI guard), not
authenticity; adler32 catches accidental corruption, not tampering; the bounds
checks turn a memory-safety catastrophe into a clean exception but are not a
licence to load untrusted code. The optional keyed-MAC for distributing
protected binaries is described as a deploy-side responsibility and flagged as
a possible follow-up, deliberately not built in (key management belongs to the
application).

BoundsValidationTest proves the loud refusal on a truncated buffer, an
out-of-range scriptOffset, a hostile pointer field, a hostile hash count and
an out-of-range interned-string offset - and that a well-formed image still
relocates and round-trips (no false positives). Run in the debug84 and
debug84-zts containers too, where an unguarded out-of-bounds read segfaults
loudest: no crashes, all refusals clean.

Acceptance evidence:
- full default suite (536 tests, baseline skips), --group opcache
  --fail-on-skipped OK (64 tests, 472 assertions) on host, debug84 and
  debug84-zts; phpstan level max clean; php-cs-fixer clean

Fixes #123

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDcCQiYqbMkjRPyhWgLL6M
@lisachenko
lisachenko force-pushed the claude/opcache-117-graph-serializer branch from 6a4bc8f to 08ed9b7 Compare August 20, 2026 00:30
@lisachenko
lisachenko force-pushed the claude/opcache-123-bounds-validation branch from d49bf0f to 7a1f13f Compare August 20, 2026 00:30
Base automatically changed from claude/opcache-117-graph-serializer to 8.4 August 20, 2026 00:45
@lisachenko
lisachenko marked this pull request as ready for review August 20, 2026 00:45
@lisachenko
lisachenko merged commit 8b94bfc into 8.4 Aug 20, 2026
21 checks passed
@lisachenko
lisachenko deleted the claude/opcache-123-bounds-validation branch August 20, 2026 00:46
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.

opcache: bounds-validate relocation offsets and document the trust model

2 participants