Skip to content

fix(reflection): ClassSpecializer::evict() leaves address-keyed handler caches and malloc'd handler blocks live — recycled addresses inherit stale handlers #191

Description

@lisachenko

Problem

ReflectionClass keeps two process-lifetime caches keyed by the numeric address of a zend_class_entry:

  • ReflectionClass::getObjectHandlers() / allocateClassObjectHandlers() (src/Reflection/ReflectionClass.php:142-153, :2908-2916) — mints a persistent (malloc) zend_object_handlers block per class entry that is never freed, cached by Core::addressOf($classEntry).
  • $propertyTableCapacity (src/Reflection/ReflectionClass.php:1738) — same address keying.

The docblock argues address-keying "keeps the cache bounded", but nothing ever removes entries. Since ClassSpecializer::evict() now exists and destroys class entries, the allocator can hand the same address to a newly-created class entry — which then silently inherits the evicted class's handler block and capacity entry. With opcache class-entry churn in long-running workers this is a real (if hard-to-hit) misbehaviour, and the handler blocks are an unbounded persistent-memory leak besides.

Proposed fix

Add an internal ReflectionClass::forgetClassEntry(int $address) (or similar) that:

  1. unsets the getObjectHandlers() cache entry for the address,
  2. frees the malloc'd handler block iff z-engine allocated it (it is already registered via Core::trackedNew()-style tracking, so Core::untrackAndFree() semantics apply),
  3. unsets the $propertyTableCapacity entry,

and have ClassSpecializer::evict() call it as part of eviction. Needs a test in the internal group covering evict → re-create at (potentially) the same address → handlers are freshly allocated.

Notes

  • Design-sensitive: freeing the handler block is only legal if no live object still points at it — eviction semantics must guarantee that first (documented in ClassSpecializer::evict()'s contract).
  • Found during the 2026-08 modernization review.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions