Skip to content

igbinary_unserialize dynamically creates conflicting properties when member access is changed  #387

@timwhitlock

Description

@timwhitlock

This may be related to #156, I'm not sure. But that's a very old ticket so I'm wondering if I can expect a change to this behaviour, or at least a good workaround.

When a class definition has changed since serialization, it's possible for igbinary_unserialize to result in multiple properties with the same name, but different public/private access. I only noticed this behaviour when upgrading to PHP 8.2 where the creation of dynamic properties is deprecated, but the issue is reproducible in earlier PHP versions too.

The behaviour is best demonstrated with a working example:

Firstly, serialize some legacy code (a simple object with a public property) and store the binary data somewhere:

 class Foo {
    public $bar;
 }
 $legacy = new Foo;
 $legacy->bar = 'baz';
 echo igbinary_serialize($legacy); // store the output

Then update your class definition to use a private variable, and a default value to highlight the problem:

 class Foo {
    
    private string $bar = 'default';
    
    public function getBar():string {
        return $this->bar;
    }
}

Then unserialize your saved blob. I'll do this with the standard PHP unserialize function for comparison:

$data = 'O:3:"Foo":1:{s:3:"bar";s:3:"baz";}';
echo "php unserialize => ",unserialize($data)->getBar(),"\n";

$data = "\0\0\0\2\x17\3Foo\x14\1\x11\3bar\x11\3baz";
echo "igbinary_unserialize => ",igbinary_unserialize($data)->getBar(),"\n";

The above prints as follows, showing the PHP unserialize function is unaffected, but igbinary doesn't set the private property:

php unserialize => baz
igbinary_unserialize => default

The latter does in fact set a property, but it dynamically sets a public property with the same name. Under PHP >= 8.2 you'll get a deprecation warning unless you annotate the class with #[AllowDynamicProperties], but the behaviour exists regardless. If you dump the instance you'll see two properties named "bar" as follows:

object(Foo)#1 (2) {
  ["bar":"Foo":private]=>
  string(7) "default"
  ["bar"]=>
  string(3) "baz"
}

It was my own stupid fault for changing the class definition of serialized objects without a proper migration strategy, but I was blissfully unaware of this issue, and I don't imagine this is uncommon.

The only way I've found to access the originally serialized property is by casting the object to an array, like this:

$foo = (array) igbinary_unserialize($data);
echo "dynamic => ",$foo['bar'],"\n";
echo "private => ",$foo["\0Foo\0bar"],"\n";

Produces:

 dynamic => baz
 private => default

I'm currently working around this issue using a __wakeup method that copies the dynamically set property from the array (as shown above) back onto the private member. However, this isn't a permanent fix as the dynamically set property still exists in duplicate and will be serialized again. I've not found any way to unset it.

An easier fix is this: $foo = unserialize(serialize($foo)); The PHP serializer lets the dynamic property overwrite the private member, but this has to be done outside the instance. It does however solve the problem of removing the duplicate property after the wakeup fix.

It seems that in PHP 8.3 the deprecated behaviour is still not removed, but I anticipate one day it will be and all my legacy objects will not be unserializable. I know this may not be considered a "bug" as such, but are there any plans to make igbinary_unserialize behave as per unserialize in this respect?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions