Skip to content

Fix GH-24674: Share the unserialize context only with Serializable - #24751

Open
iliaal wants to merge 1 commit into
php:masterfrom
iliaal:fix/gh-24674-unserialize-context
Open

iliaal wants to merge 1 commit into
php:masterfrom
iliaal:fix/gh-24674-unserialize-context

Conversation

@iliaal

@iliaal iliaal commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

php_var_unserialize_init() hands the context of a parse in progress to any caller, so user code that runs mid-parse and calls unserialize() (a GC destructor, an error handler, an allowed_classes __toString(), a session_decode() overwrite) resolves R: and r: into the outer value. The context is now shared only with the unserialize handler of a C: payload, only while serialize_lock is not held, as serialize() does, and only within the fiber that published it. This targets master because it narrows which nested unserialize() calls see back-references. #24433 fixed the failed __unserialize() destructor case from the report.

Fixes #24674

@bukka bukka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't like the Fiber part. This could break IO hooks, as any IO there may suspend. Could we make the context fiber-local instead and keep the existing nesting behavior?

php_var_unserialize_init() joined the context of any parse in progress, so
user code that runs mid-parse and calls unserialize() resolved
back-references into the outer value: a destructor run by the garbage
collector, an error handler for a parser warning, an allowed_classes
__toString(), a session_decode() overwrite. Only the unserialize handler of
a C: payload is meant to share it. object_custom() now publishes the
context for the duration of that call, a nested init shares it only while
serialize_lock is not held, as serialize() does, and a parse hides it for
its own duration. The published context is kept per fiber, so a fiber
switch inside the handler neither exposes it to another fiber nor loses
it, and a bailout restores the previous one. A nested init under
serialize_lock, as in a C: payload unserialized from __wakeup() or
__unserialize(), now keeps the allowed_classes and max_depth of the
payload.

Fixes phpGH-24674
@iliaal
iliaal force-pushed the fix/gh-24674-unserialize-context branch from f151074 to 8fbf61a Compare October 30, 2026 12:50
@iliaal

iliaal commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Reworked: dropped zend_fiber_switch_block(). The published context is now parked per fiber by a fiber switch observer, so a suspend inside Serializable::unserialize() keeps working and other fibers don't see the context. Sharing stays limited to the C: handler. The error handler, GC destructor and session_decode() cases run on the same fiber as the outer parse, so keeping the existing nesting, even fiber-local, would still let them read the outer value. serialize_lock is still shared across fibers, as on master; I'd keep that for a separate change.

@bukka bukka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It looks better but I think it needs also Fiber destroy observer otherwise it might leave the destroyed fiber in BG(unserialize).parked with an old fiber context which could be then reused by later Fiber.

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.

var_destroy() UAF also reachable through SplObjectStorage::__unserialize() — separate entry point from #24481

2 participants