Skip to content

Free the previous function body in redefine() instead of leaking it #64

Description

@lisachenko

Context

Follow-up to the memory-lifetime overhaul (#62, merged in #63). One bounded residual was deliberately left in place and documented in docs/long-running.md: FunctionLikeTrait::redefine() on a user-defined function/method leaks the previous function body.

Current behaviour

redefine() (src/Reflection/FunctionLikeTrait.php) copies the closure's zend_function wholesale over the original one. The original op_array's exclusive allocations — opcodes, literals, vars, live ranges — become unreachable and stay allocated:

Zend/zend_opcode.c(1111): Freeing ... (opcode array)
Zend/zend_compile.c(7573): Freeing ... (literals)
Zend/zend_string.h(176/252): Freeing ... (strings)

The leak is bounded: one previous body per redefined function, not per call in a loop over the same function... actually it is per redefine() call, so a worker that repeatedly redefines the same function grows without bound. Realistic usage (redefine once at boot) leaks a few hundred bytes per function. ReflectionFunctionTest::testRedefine and ReflectionMethodTest::testRedefine currently carry a commented ini_set('report_memleaks', '0') exemption for exactly this.

Why it was not fixed in #63

A wholesale destroy_op_array() on the original before the overwrite is incorrect, because redefine() first copies the original's common struct into the closure's function to preserve identity — after that copy the two functions share raw pointers:

  • function_name (zend_string*, refcounted — could be addref'd before destroy)
  • arg_info (plain allocated array, not refcounted — destroying the original frees it while the redefined entry still points at it → use-after-free on any reflection of parameters)
  • doc_comment and friends

And the two candidate primitives have export problems:

  • destroy_op_array is a real ZEND_API export and could be added to tools/generator/symbols.php, but is too coarse while pointers are shared;
  • _efree (for freeing just the exclusive arrays) cannot be declared safely with one signature: debug builds add ZEND_FILE_LINE_DC parameters that are actually used, so a hand-authored release-shape declaration would pass garbage file/line pointers on the debug CI build.

Possible directions

  1. Unshare then destroy — before the overwrite: addref function_name/doc_comment, duplicate arg_info (size is computable from num_args + variadic + return-type slot), then export destroy_op_array and destroy the original. Most complete, most surgery.
  2. Bucket-pointer replacement — instead of memcpy-ing over the original struct, point the function/method-table bucket at the closure's zend_function (like addRawMethod() does), keep the closure alive in a Core-level registry, and release the original entry through the table's own zend_function_dtor. Avoids manual freeing entirely, but changes redefine() mechanics and still needs the name/scope fixups plus the shared-pointer audit.
  3. Leave as documented residual — acceptable for redefine-once-at-boot usage; the docs and test exemptions already state it.

Whichever direction wins, the acceptance test is simple: remove the ini_set('report_memleaks', '0') exemptions from the two testRedefine tests and drop the corresponding row from the immortal-by-design table in docs/long-running.md; the debug leak gate then enforces it.

Related (separate, probably fine as-is): addMethod() intentionally immortalizes the closure body because the class method table stores a pointer into the closure object — releasing it at any userland-reachable moment would dangle the method entry during class destruction.

Not urgent — filed to keep the option on the table.

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions