Skip to content

Runtime: dropping an array or object to refcount 0 frees only the header — packed values, string-key nodes and property boxes are never freed or delref'd, so every container death leaks (lib/JIT/Builtin/Refcount.php) #36215

Description

@PurHur

Category

Foundation: · memory model · child of #36188

Problem

Refcount.php:440-497 (drop-to-zero): four opaque calls — phpc_destruct_try_invoke, phpc_destruct_delref_allowed, phpc_weakref_clear_object_typed, phpc_gc_unregister — then free(ptr) of the header only. Nothing frees a __hashtable__'s values array, its __strkey_node__/__objkey_node__ chains, or an __object__'s property boxes, and the contained values are never delref'd, so nested strings/arrays/objects leak transitively. In {main} nothing is freed at all until shutdown by design (phpc_destruct_delref_allowed() returns 0, :458-480, #4013).

A phpc fcgi worker or any loop that builds and discards arrays grows without bound; the compliance/differential harnesses never see it because each case is one short process. The VM has a Refcount + COW model that does release (lib/VM/HashTable.php), so the semantics to mirror exist in-tree.

php-src reference

  • Zend/zend_variables.c rc_dtor_funczend_array_destroy / zend_objects_store_del: destroying a container delrefs every element, then frees buckets, then the header.

PHP implementation target

  • Emit __hashtable__dtor(ht): delref each packed value, walk and free strKeys/objKeys (delref key strings and values), free values; __object__dtor(obj): delref each property box, free slots. Dispatch on typeinfo kind at Refcount.php:495 before free.
  • Decide {main} policy explicitly: keep "no frees in main" only for globals that outlive the script; temporaries and locals in main must be released (or document why not and measure the cost on the examples).
  • Test under valgrind --leak-check=full and ASan (PHP_COMPILER_ASAN=1 link flag) on one differential sweep; add --repeat 10 runs — ownership changes are where heap corruption hides (AOT: script-scope ++/-- goes wrong once several statements or variables are involved (loop after statements, second variable) #23842).

Repro

./script/docker-exec.sh -- bash -lc 'source script/php-env.sh && cat > build/leak.php <<'"'"'EOP'"'"'
<?php function mk($i){ return ["k$i" => str_repeat("x", 100), "n" => [$i, $i+1]]; } for ($i = 0; $i < 200000; $i++) { $a = mk($i); } echo "done\n";
EOP
php bin/compile.php -o build/leak build/leak.php && /usr/bin/time -v ./build/leak 2>&1 | grep -E "done|Maximum resident"'

Expected: flat RSS (a few MB); today RSS grows with $i.

Done when

  • The probe's peak RSS is flat (< 20 MB) and identical output; valgrind reports no definitely-lost blocks on script/aot-smoke.sh cases
  • script/differential-sweep.sh --aot --repeat 10 unchanged; examples-fastcgiweb-smoke soak of 10k requests shows flat RSS
  • docs/runtime-semantics.md documents ownership rules for containers and {main}

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

    IMPORTANTCritical patharea:compilerCompiler / CFG / JITbugSomething isn't workingimplementation-readySpec complete: repro, php-src ref, done-when — safe for workers to claimphase-3:aotPhase 3 – AOT deployment

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions