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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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