Skip to content

ext/bcmath: guard BcMath\Number against an uninitialized bc_num#22259

Open
nicolas-grekas wants to merge 1 commit into
php:masterfrom
nicolas-grekas:bcmath-uninitialized-number-guard
Open

ext/bcmath: guard BcMath\Number against an uninitialized bc_num#22259
nicolas-grekas wants to merge 1 commit into
php:masterfrom
nicolas-grekas:bcmath-uninitialized-number-guard

Conversation

@nicolas-grekas

Copy link
Copy Markdown
Contributor

BcMath\Number is a final internal class with a custom create_object, so the engine rejects ReflectionClass::newInstanceWithoutConstructor() for it and unserialize() always routes through __unserialize(). A freshly create_object-ed instance has a NULL bc_num only transiently, before __construct()/__unserialize() runs; an uninitialized instance can never escape to userland.

C code can however build one via create_object directly (e.g. the deep-clone extension at symfony/php-ext-deepclone, while reconstructing object graphs). Every BcMath\Number handler dereferences intern->num, so such an instance is a NULL-pointer dereference waiting to happen: (string) $n, clone $n, $n <=> $x, $n + 1, $n->add(...), (bool) $n, var_dump($n) all crash.

This guards the entry points that read num (value stringification / __serialize / get_properties_for, clone, the compare and do_operation operator overloads, the arithmetic/sqrt/floor/ceil/round/divmod/powmod/compare methods, the bool cast and has_property) so they throw

The BcMath\Number object has not been correctly initialized by its constructor

instead of crashing, reusing the wording DateTime/DateTimeZone already use for the same situation. Fully constructed instances are unaffected.

No .phpt is included because the uninitialized state is not reachable from pure PHP; it is exercised by the deep-clone extension's test suite.

Surfaced while adding BcMath\Number support to the deep-clone extension and its polyfill for symfony/symfony#64323.

A BcMath\Number whose constructor or __unserialize() never ran has a
NULL bc_num. This cannot happen through normal PHP (the class is final
with a custom create_object, so newInstanceWithoutConstructor() is
rejected and unserialize() routes through __unserialize()), but C code
such as an extension calling create_object directly can build one, after
which every operation dereferences the NULL bc_num and crashes.

Guard the entry points (value stringification, clone, comparison,
arithmetic, the operator overloads, the bool cast and property checks)
so they throw a clean Error instead. Normal, fully constructed instances
are unaffected.
@ndossche

ndossche commented Jun 8, 2026

Copy link
Copy Markdown
Member

This is a general issue caused by shitty PHP design choices.
You could fix it ad-hoc for ext/bcmath, but that leaves other classes buggy. We have an open issue for this somewhere.
It would be better to fix this generally.

@nicolas-grekas

Copy link
Copy Markdown
Contributor Author

🤷 could still be worth fixing adhoc. I'll let maintainers do what they prefer with this PR :)

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.

2 participants