Skip to content

Fix GH-23725: use-after-free when __toString() frees a frameless argument - #23731

Open
iliaal wants to merge 1 commit into
php:masterfrom
iliaal:fix/gh-23725-frameless-arg-uaf
Open

iliaal wants to merge 1 commit into
php:masterfrom
iliaal:fix/gh-23725-frameless-arg-uaf

Conversation

@iliaal

@iliaal iliaal commented Sep 17, 2026 •

Copy link
Copy Markdown
Member

Frameless calls hand the handler the caller's operand zvals without taking a reference, so an argument freed by user code the handler triggers leaves it reading freed memory. Array arguments are now pinned in the Z_FLF_PARAM_ARRAY macros, replacing the per-function guards from 8ce7f7f as suggested on #23221, which this supersedes. String arguments are pinned only when user code can still run after they are read, and str_replace()/preg_replace() skip pinning when all three arguments are strings.

Fixes #23725

@arnaud-lb

arnaud-lb commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

This looks good to me in general: With this change, frameless functions can have the same expectations about their arguments than normal ones. But I would like to see @iluuu1994's opinion on this.

so #21639 stays open

Is this correct? I believe that this branch fixes that issue as well

Z_FLF_PARAM_STR still reads its string directly on the fast path

Unless something is blocking, I think that we should update Z_FLF_PARAM_STR as well. Test case:

$a = 'foo' . time();
$b = new class {
    function __toString() {
        global $a;
        $a[0] = '!';
        return '0';
    }
};
var_dump(strtr($a, 'o', $b)); // expected: f00..., actual: !00

@iluuu1994

iluuu1994 commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

Wordpress i-count goes up by 0.44%, that seems like more than just noise... The reason I was hesitant with these fixes is that frameless calls have a modest performance improvement to begin with. Full correctness for artificial code might make the benefit negligible. I don't know how else to address this, the fix does look technically reasonable, but I'm not sure is worth it. At least not considering how many other memory issues can be triggered with malicious code.

The other option would be to disallow CVs and always send a copy using QM_ASSIGN. At least then we can drop all the checks, including the string checks. I don't know if that's actually faster, somewhat doubtful given the additional VM overhead. I don't have any other ideas.

@iliaal

iliaal commented Sep 24, 2026

Copy link
Copy Markdown
Member Author

Agreed, almost .5% regression for a fix is too much, I am not sure the alternative you suggested would be faster. A little frustrating TBH, needs more thought, or just won't fix

@iliaal
iliaal force-pushed the fix/gh-23725-frameless-arg-uaf branch from 4e514ed to cf6284b Compare September 24, 2026 20:41
@iliaal

iliaal commented Sep 24, 2026

Copy link
Copy Markdown
Member Author

Reworked: str_replace() and preg_replace() skip pinning when all three arguments are strings, the helper-level array pins from 8ce7f7f are gone because the frameless handlers pin the arrays themselves, and an argument that is already a string is pinned only when a later argument needs conversion or the handler can run user code afterwards, which covers the strtr() case too. Locally that takes WordPress from +0.46% to -0.07% against the merge base; the CI benchmark will confirm.

@iliaal
iliaal requested a review from iluuu1994 September 24, 2026 20:47
…rgument

Frameless calls pass the caller's operand zvals straight to the handler
without taking a reference, so an argument freed by user code that the
handler triggers leaves it reading freed memory. Take a reference on array
arguments in the Z_FLF_PARAM_ARRAY* macros and in the one-argument implode(),
which makes the matching guards from 8ce7f7f redundant. String arguments
are pinned only when user code can still run after they are read: a later
argument needs conversion, strtr() walks a replacement array, implode()
converts an element, or property_exists() may autoload. str_replace() and
preg_replace() skip the pins when all three arguments are already strings.

Fixes phpGH-23725
@ndossche

Copy link
Copy Markdown
Member

I have a completely other idea that may generalize nicer.

Instead of trying to protect against malicious code: detect when a malicious change happened and tell the programmer to f- off. We can add that detection code to the slow path so that it probably doesn't affect real-world code; and if it affects such code anyway then the code was already slow.

The PoC linked here is a bit specific to frameless+__toString but may be generalised. Maybe this is a bad idea, but maybe this is a way to go forward with this, or maybe it brings inspiration to fix these classes of issues (I think the same conceptual idea can work for error handlers).

PoC: https://gist.github.com/ndossche/ecba0a86b9f284a80382d064318a5e8f

@iliaal
iliaal force-pushed the fix/gh-23725-frameless-arg-uaf branch from cf6284b to e82d80e Compare September 24, 2026 20:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

heap uaf in php_pcre

4 participants