Skip to content

Fix GH-22060 and GH-22122: pin $this in zend_call_known_fcc#22151

Open
iliaal wants to merge 1 commit into
php:masterfrom
iliaal:fix/gh-22060-22122-uaf-pin-fcc-object
Open

Fix GH-22060 and GH-22122: pin $this in zend_call_known_fcc#22151
iliaal wants to merge 1 commit into
php:masterfrom
iliaal:fix/gh-22060-22122-uaf-pin-fcc-object

Conversation

@iliaal
Copy link
Copy Markdown
Contributor

@iliaal iliaal commented May 26, 2026

Both reports are the same use-after-free: zend_call_known_fcc() ran the callback with fcc->object as $this without holding a reference, so a callback that released the last reference to its own receiver (autoloader self-unregistering, SQLite3 authorizer calling setAuthorizer(null)) freed it mid-call. Pinning the receiver across the call covers the autoloader and the sqlite3/pdo_sqlite authorizers, which all dispatch through it.

Copy link
Copy Markdown
Member

@DanielEScherzer DanielEScherzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay for ext/reflection, don't know about the other parts

Copy link
Copy Markdown
Member

@Girgias Girgias left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to understand why we would need to add ref the fcc.object, and my understanding is that it represents the $this value, so I guess this is valid.

However, we are once again fixing bugs and affecting performance for idiosyncratic code. I would rather prefer a solution that bans unsetting such callables within the calls. Similar to a fix we recently did for an unserialising routine in SPL.

@iliaal
Copy link
Copy Markdown
Contributor Author

iliaal commented May 29, 2026

Pinning is two refcount ops against a full call-frame setup, so the per-call cost is negligible. Banning is the bigger change: the autoload loop is deliberately built to allow add/remove mid-iteration, and disabling the authorizer mid-call is a one-shot the sqlite3 test relies on. Turning either into a thrown error is a behavior change that can't go in a stable branch. If the stricter semantics are worth having, that's a master-only follow-up.

@iliaal iliaal requested a review from Girgias May 29, 2026 21:45
@iluuu1994
Copy link
Copy Markdown
Member

However, we are once again fixing bugs and affecting performance for idiosyncratic code.

Such things will come up more and more with the raise of AI.

A while ago when fixing a similar issue, @TimWolla asked me if zend_call_function() should just take care of at least making sure $this survives. I think that's reasonable, and something that could be changed on master. In that case, we should remove immediate add/delref around zend_call_function() calls throughout the codebase.

This should target master anyway (we agreed to not fixing such bugs on stable branches that are unlikely to cause issues in the real world). So maybe let's just go with the above instead.

@iliaal iliaal force-pushed the fix/gh-22060-22122-uaf-pin-fcc-object branch from 7720c11 to 497f3fa Compare June 1, 2026 11:23
@iliaal iliaal requested review from iluuu1994 and removed request for SakiTakamachi, TimWolla, adoy, arnaud-lb, derickr, devnexen and kocsismate June 1, 2026 11:25
@iliaal iliaal force-pushed the fix/gh-22060-22122-uaf-pin-fcc-object branch from 497f3fa to 502eb4b Compare June 1, 2026 11:28
@iliaal iliaal changed the title Fix GH-22060 and GH-22122: pin object/closure in callback dispatch Fix GH-22060 and GH-22122: pin $this across the call in zend_call_function Jun 1, 2026
@iliaal
Copy link
Copy Markdown
Contributor Author

iliaal commented Jun 1, 2026

First part's done: zend_call_function() now pins fci_cache->object across the call, and I've retargeted this to master. It covers both reports, since the autoloader and the sqlite3/pdo_sqlite authorizers all dispatch through it.

On removing the add/delref throughout the codebase: most of them can't go. The pin is released when the call returns, so it only guarantees $this for the call itself, not for code the caller runs afterward. zend_std_unset_property() is the clear case: it does (*guard) &= ~IN_UNSET after the unsetter returns, with guard pointing into the object, so dropping that GC_ADDREF brings back a UAF on the guard reset. The pure call-survival ones like Countable::count could go, but that's a separate cleanup. Kept this PR to the pin plus tests.

zend_call_known_fcc() ran the callback with fcc->object as $this
without holding a reference, so a callback that released the last
reference to its own receiver (an autoloader unregistering itself, a
SQLite3 authorizer calling setAuthorizer(null)) freed $this while its
frame was still executing. Hold a reference across the call.

Fixes phpGH-22060
Fixes phpGH-22122
@iliaal iliaal force-pushed the fix/gh-22060-22122-uaf-pin-fcc-object branch from 502eb4b to a85e9ba Compare June 1, 2026 12:10
@iliaal iliaal changed the title Fix GH-22060 and GH-22122: pin $this across the call in zend_call_function Fix GH-22060 and GH-22122: pin $this in zend_call_known_fcc Jun 1, 2026
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.

4 participants