Skip to content

mixed_box of an owned object leaks the producer's reference (?Class returns leak one object per call) #484

Description

@mirchaemanuel

Summary

Boxing an owned object into a Mixed cell leaks the producer's reference: __rt_mixed_from_value retains object payloads (tag 6 → __rt_incref), and __rt_mixed_free_deep correctly releases the child when the cell dies — but EIR lowering treats Op::MixedBox as consuming its operand and never releases the producer's own reference. Net +1 on the payload per boxing of an owning temporary.

The everyday shape is a nullable-object return:

class P { public int $v = 7; }
function g(): ?P { return new P(); }   // return coerces to Mixed: object_new (rc=1) → mixed_box (payload rc→2)

$acc = 0;
for ($n = 0; $n < 50; $n++) { $c = g(); $acc += $c->v; }
echo $acc;   // 350, correct

Verified on

main (7396e65b), macOS ARM64, --heap-debug: 50 leaked blocks in 50 iterations — exactly the P objects. The boxed cells themselves are freed correctly on rebind (__rt_decref_mixed__rt_mixed_free_deepdecref_any(child) drops the child from 2 to 1 — never to 0).

IR of the return path:

v5: Heap(Object) php=P own=maybe_owned = object_new …   ; producer reference, rc=1
v6: Heap(Mixed) = mixed_box v5                           ; runtime retains payload, rc→2
return v6                                                ; v5's reference never released

Expected

One reference per owner: after boxing, the cell owns the payload; an owning-temporary operand's producer reference must be released (a borrowed operand — e.g. boxing a loaded local — is correct as-is: the retain gives the box its own reference alongside the local's).

Suggested direction

In the Op::MixedBox lowering/emission path(s) in ir_lower, release the operand after boxing when value_is_owning_temporary(operand) — same ownership pattern as the recent throw-of-borrowed fix in #477. Needs an audit of all MixedBox emission sites (return coercion, argument coercion, storage coercion) so the rule is applied uniformly.

Notes

Surfaced while verifying #447 (whose original stored-tryFrom shape is fixed on current main). Enum tryFrom itself no longer exhibits this class (its payload is a persistent singleton); any ?Class/mixed-returning function that produces fresh objects does.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions