Skip to content

[6.x] Fix infinite recursion when a non-public data method matches an augmented key - #15482

Open
daun wants to merge 3 commits into
statamic:6.xfrom
daun:fix/entry-class-augmentation-recursion
Open

daun wants to merge 3 commits into
statamic:6.xfrom
daun:fix/entry-class-augmentation-recursion

Conversation

@daun

@daun daun commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

TL;DR: A field whose name matches a protected or private method on the entry crashes a request with memory exhaustion. Augmentation only checks if the method exists, then calls it from an outside scope that can't reach it, so PHP routes it to __call() and it augments the same handle forever, resulting in an infinite loop + crash. This PR requires the method to be public for it to be considered.

Problem

When augmenting a handle, AbstractAugmented looks for a method of the same name on the entry/term/asset/etc and calls it instead of reading the stored value. I.e. a public isSold() method overrides an is_sold field.

The check here is method_exists(), which is true for both protected and private methods. However the call happens from outside the entry class: $this->data->$method(). PHP won't call the non-public method and forwards to __call() instead. Augmentables then use HasAugmentedInstance::__call(), which augments that same handle again. Nothing stops the loop, so the request runs out of memory.

Example

This adds an internal helper method for coercing a type. It never was meant for actual augmentation, but crashes.

class Product extends Entry
{
    protected function unitsPerSet(): int
    {
        return (int) $this->value('units_per_set');
    }
}

$entry->augmentedValue('units_per_set');

Fix

Only treat the method as a match if it's public/reachable. Checked via ReflectionMethod rather than is_callable() since that also returns true for unreachable methods. Cached to avoid repetitive lookups.

Scope

This only changes the lookup on the entry. The separate lookup for methods on the Augmented* class itself is left alone on purpose: those are called from inside that class, so its own protected methods are reachable and need to keep working (permalink(), mount(), updatedBy()). There's a test that covers that.

Not breaking

Before this change, a non-public method matching a key could only and in an error: either the infinite loop above or Error: Call to protected method. There was no version of this that worked so nothing can be relying on it. Public methods still override their field, exactly as before.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant