Harden PHP C++ Code Against Non-Factory Constructions - #6566
Harden PHP C++ Code Against Non-Factory Constructions#6566InsertCreativityHere wants to merge 7 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Hardens PHP native wrapper objects against unsafe deserialization.
Changes:
- Marks native-state wrappers as non-serializable.
- Marks non-inheritable wrappers as final.
- Adds deserialization regression coverage.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
php/test/Ice/info/Client.php |
Tests rejection of crafted serialized objects. |
php/src/Types.cpp |
Hardens type and exception metadata wrappers. |
php/src/Proxy.cpp |
Disables proxy serialization. |
php/src/Properties.cpp |
Hardens properties wrappers. |
php/src/Logger.cpp |
Hardens logger wrappers. |
php/src/Endpoint.cpp |
Hardens endpoint and endpoint-info wrappers. |
php/src/Connection.cpp |
Hardens connection wrappers. |
php/src/Communicator.cpp |
Hardens communicator wrappers. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
pepone
left a comment
There was a problem hiding this comment.
Add a private __construct to _typeInfoMethods / _exceptionInfoMethods (php/src/Types.cpp:3651). Both tables are empty, so plain new is not blocked by final, and IcePHP_defineSequence('::Foo::Seq', new IcePHP_TypeInfo()) still segfaults on this branch — that's the fourth door in #6379 (comment), not reflection.
Drop the six per-subclass ZEND_ACC_NOT_SERIALIZABLE assignments in php/src/Endpoint.cpp. Zend propagates the flag through inheritance and the base is flagged before the subclasses are registered, so the one on Ice\EndpointInfo already covers the hierarchy.
In the new test, use test(false) after the call and catch Exception, like the clone tests 30 lines below it. The empty catch (Throwable) currently also passes when unserialize fails for some unrelated reason.
One correction for the description, since it's what the next reader will go by: ZEND_ACC_FINAL does close the reflection door. Zend refuses newInstanceWithoutConstructor() when a class is internal, has a create_object handler, and is final, so the seven classes you marked are already covered — Ice\ObjectPrx and the endpoint-info classes are the ones still open. I've recorded the full door matrix on #6379 along with a fifth door (subclass declaring a public __construct), and why final is the wrong tool for those two: they are the only public names here that are concrete classes rather than interfaces, so it would take PHPUnit mocking away.
externl
left a comment
There was a problem hiding this comment.
Built the extension on this branch and on its base against PHP 8.5 and ran the hostile constructions against both — the unserialize hole is real and this closes it (unserialize('O:13:"Ice\ObjectPrx":0:{}') then ice_toString() segfaults pre-PR, throws after). Three gaps though, and the first needs neither reflection nor unserialize.
new IcePHP_TypeInfo()andnew IcePHP_ExceptionInfo()still segfault from plain PHP.Types.cpp:3651and:3654declare empty method tables ({{0,0,0}}), so unlike every other wrapper class these two have no constructor at all, andZEND_ACC_FINALdoesn't preventnew.IcePHP_stringify("hi", new IcePHP_TypeInfo())andIcePHP_defineSequence("::Foo::Seq", new IcePHP_TypeInfo())both crash on the PR branch.ZEND_ACC_FINALis the reflection fix, and it's already here for 7 of the 15 classes. PHP rejectsnewInstanceWithoutConstructor()on a final internal class that has acreate_objecthandler, so the 7 you marked final now throwReflectionException— that half of option 2 is done. The 8 that got onlyNOT_SERIALIZABLE—Ice\ObjectPrxand the sevenIce\*EndpointInfo— still segfault, and not only via reflection:class Evil extends Ice\ObjectPrx { public function __construct() {} }thenice_toString()crashes too, since a private parent constructor is redeclarable in a subclass.Ice\ObjectPrxlooks safe to make final (slice2php emits*PrxHelperstatic classes and dispatches throughhandleGetMethodrather than subclassing), as do the leaf*EndpointInfoclasses;Ice\EndpointInfoandIce\IPEndpointInfoare extended inside the extension so they'd need something else.ZEND_ACC_NOT_SERIALIZABLEdoesn't exist before PHP 8.1, andphp/BUILDING.md:22still lists 8.0 as supported. CI installs the current PHP so it won't catch this.Config.h:73already has aPHP_VERSION_ID >= 80200guard as precedent, if the floor isn't just being moved instead.
Two smaller things: the flag blocks serialize() as well as unserialize(), which is a behavior change for the *EndpointInfo classes since they carry genuine public data (host, port, rawBytes) — and it leaves Ice\ConnectionInfo fully serializable while its sibling getInfo() result type throws, with no changelog fragment. And in php/test/Ice/info/Client.php, a class name that rots gives __PHP_Incomplete_Class, which isn't instanceof $className, so the assertion passes silently; the test also covers only unserialize, not the new/reflection/subclass paths above.
Clone is clean — Properties and ObjectPrx have clone_obj handlers that build fresh C++ objects, and every other wrapper sets clone_obj = nullptr so clone throws.
I added private
This is correct, that the flags are inherited, and removing them would not change anything.
Fixed as suggested!
I substantially updated the description. |
Fixed this by adding a
This comment (#6379 (comment)) explains why we shouldn't extend Either way, it's unnecessary. This PR adds a catch-all null check to
This was a documentation bug. In reality, the minimum PHP version we support is 8.4, so it's fine to use here!
I thought that this was minor enough to not warrant a changelog fragment. If you disagree and think this is a real case,
I updated the test to ensure both direct-construction and unserialization are rejected. |
There was a problem hiding this comment.
🟡 Changes recommended
Several ObjectPrx paths still dereference forged wrappers without the new null check and can crash PHP.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 10/10 changed files
- Comments generated: 1
- Review effort level: Balanced
pepone
left a comment
There was a problem hiding this comment.
Please drop the changelog fragment. This is defense-in-depth against objects fabricated outside the supported APIs, not a change to an expected usage pattern.
One remaining gap is ObjectPrx: handleClone, handleGetMethod, handleCompare, and fetchProxy bypass Wrapper::value and can still dereference a null ptr. If routing these paths through a checked helper is straightforward, let us include it here. Otherwise, I am fine deferring it to a focused follow-up issue/PR that records these paths and adds regression coverage.
|
I fixed the holes you mentioned, and checked for every use of The other call sites were re-routed to go through |
There was a problem hiding this comment.
🟢 Approval recommended
The defensive checks are consistently applied and the affected construction and comparison paths are covered by tests.
Review details
- Files reviewed: 10/10 changed files
- Comments generated: 0 new
- Review effort level: Balanced
|
@InsertCreativityHere A few more things after another pass over 6049790. The first two block merging; the rest I would like in this PR unless noted otherwise. 1. PHP 8.0 compatibility (blocking) Coming back to the PHP 8.0 question from the Copilot thread. I don't think we can treat 8.4 as the minimum for this change.
The only 8.1+ construct in the diff is inline void denySerialization(zend_class_entry* ce)
{
#if PHP_VERSION_ID >= 80100
ce->ce_flags |= ZEND_ACC_NOT_SERIALIZABLE;
#else
ce->serialize = zend_class_serialize_deny;
ce->unserialize = zend_class_unserialize_deny;
#endif
}One behavior difference for the new test in try {
test(@unserialize('O:' . strlen($className) . ':"' . $className . '":0:{}') === false);
} catch (Exception $ex) {
}Nothing else in CI has PHP 8.0: ubuntu-24.04 is 8.3 and macOS is 8.4+. The only place it shows up is the el9 RPM job, which runs in 2. Docs Please also revert the 3. The fatal in
4. Empty
5.
6.
7. Tests
8. Per-subclass flags in Yes, please drop the six (lines 255, 271, 279, 294, 303, 313). The ordering concern does not apply: Minor, your call
Out of scope, for a follow-up issue: |
Our C++ PHP wrapper classes need to be constructed through our own factories, so their
ptrvalues are non-null.This PR fixes a handful of ways that they could be constructed outside of our factories:
ZEND_ACC_NOT_SERIALIZABLEfor all the types which were affected by this:IcePHP_Communicator, IcePHP_Connection, IcePHP_Endpoint, IcePHP_Logger, IcePHP_Properties, IcePHP_TypeInfo, IcePHP_ExceptionInfo, ObjectPrx, .*EndpointInfoZEND_ACC_FINAL, which also disables subclasses getting around our checks. This was set for all the above types except forObjectPrxand the.*EndpointInfowhich are public types.newwas blocked forIcePHP_TypeInfoandIcePHP_ExceptionInfoby creating a private__constructwhich always throws. All the other types already did this.Then as an absolute fallback to cover the edge cases not worth having dedicated code to fight, this PR adds a null-check to the
Wraper::valuefunction. So if the pointer is ever null, it will only trigger a PHP error, instead of a dereferencing crash.All of these types were internal except for
ObjectPrxand.*EndpointInfo. And the only change to those 2 is that now they cannot be serialized or deserialized. It is unlikely anyone is ever doing this, so it's fine to remove in a patch release IMO.