Name Mangling and Serialization in PHP

Serialization in PHP is the process of taking a snapshot of the state of an object and converting it to a storable string to be reconstructed later. It is a powerful tool, however there are a few pitfalls to be aware of that can cause issues if you’re not careful. In order for a serialized object to be unserialized, the class that it is an instance of must be defined in the context that it is being unserialized so that PHP knows how to reconstruct the object. Some interesting and unexpected behaviors can arise when the class definition changes in between the time when the object was originally constructed and when it is unserialized. For example, this could happen when new code is deployed.

Consider the following simple class:

<?php
class Foo {
protected $bar = 1;
}
view raw gistfile1.php hosted with ❤ by GitHub

We instantiate this class, serialize the object and push the result on to a job queue. A worker later pulls it off the queue and unserializes it, except in this context the $bar property has been renamed to $baz;

<?php
class Foo {
protected $baz = 1;
}
view raw gistfile1.php hosted with ❤ by GitHub

var_dump()ing the object, results in the following output:

class Foo#1 (2) {
protected $baz =>
int(1)
protected $bar =>
int(1)
}
view raw gistfile1.txt hosted with ❤ by GitHub

The object now appears to be a combination of the original and new class definitions. If we attempt to echo $foo->bar, one would expect to get a fatal error since it is a protected property. However instead the result is “PHP Notice: Undefined property: Foo::$bar”

To understand why this happens, we need to dive into the internals of PHP, specifically the Zend Engine which powers the PHP runtime. The Zend Engine is written in C, so having some basic knowledge of the language will be helpful, but is not strictly necessary. The PHP lxr website is an excellent way to explore the PHP source with many useful features to help you find your way around.

These code snippets will be from the PHP 5.3 branch. If you look at newer branch, it will have some other data structures for optimizing storage of declared properties, but the relevant components are the same. First a quick introduction to how the Zend Engine represents objects internally.

typedef struct _zend_object {
zend_class_entry *ce;
HashTable *properties;
HashTable *guards; /* protects from __get/__set ... recursion */
} zend_object;
view raw gistfile1.c hosted with ❤ by GitHub

The zend_object consists of three pointers: one to the declaring class (*ce), one to a hash table of the object’s properties (*properties), and one to a hash table of recursion guards (*guards). Guards are not relevant to our topic so we’ll disregard them. The class definition (*ce) contains all of the information about the class such as its constants, functions, and properties.

When you access a property of an object in PHP, the Zend Engine internally calls zend_std_read_property.

zval *zend_std_read_property(zval *object, zval *member, int type TSRMLS_DC) /* {{{ */
{
// ...
property_info = zend_get_property_info(zobj->ce, member, (zobj->ce->__get != NULL) TSRMLS_CC);
if (!property_info || zend_hash_quick_find(zobj->properties, property_info->name, property_info->name_length+1, property_info->h, (void **) &retval) == FAILURE) {
view raw gistfile1.c hosted with ❤ by GitHub

zend_std_read_property calls into zend_get_property_info which looks up the property by name in the object’s class definition (*ce), verifies that the property can be accessed in the current scope, and returns a zend_property_info struct. If the property has not been declared in the class definition, then it returns a default set of values. This is the reason that properties that aren’t explicitly declared default to public visibility.

typedef struct _zend_property_info {
zend_uint flags;
char *name;
int name_length;
ulong h;
char *doc_comment;
int doc_comment_len;
zend_class_entry *ce;
} zend_property_info;
view raw gistfile1.c hosted with ❤ by GitHub

zend_property_info contains information about the property such as its visibility, hash key, and name. The Zend Engine uses a technique known as name mangling to resolve the property names of class. The *name pointer is the mangled name of the property. To see how the Zend Engine mangles property names, let’s take a look at a snippet of code from zend_declare_property_ex which is called when a property is defined on an object.

switch (access_type & ZEND_ACC_PPP_MASK) {
case ZEND_ACC_PRIVATE: {
char *priv_name;
int priv_name_length;
zend_mangle_property_name(&priv_name, &priv_name_length, ce->name, ce->name_length, name, name_length, ce->type & ZEND_INTERNAL_CLASS);
zend_hash_update(target_symbol_table, priv_name, priv_name_length+1, &property, sizeof(zval *), NULL);
property_info.name = priv_name;
property_info.name_length = priv_name_length;
}
break;
case ZEND_ACC_PROTECTED: {
char *prot_name;
int prot_name_length;
zend_mangle_property_name(&prot_name, &prot_name_length, "*", 1, name, name_length, ce->type & ZEND_INTERNAL_CLASS);
zend_hash_update(target_symbol_table, prot_name, prot_name_length+1, &property, sizeof(zval *), NULL);
property_info.name = prot_name;
property_info.name_length = prot_name_length;
}
break;
case ZEND_ACC_PUBLIC:
if (ce->parent) {
char *prot_name;
int prot_name_length;
zend_mangle_property_name(&prot_name, &prot_name_length, "*", 1, name, name_length, ce->type & ZEND_INTERNAL_CLASS);
zend_hash_del(target_symbol_table, prot_name, prot_name_length+1);
pefree(prot_name, ce->type & ZEND_INTERNAL_CLASS);
}
zend_hash_update(target_symbol_table, name, name_length+1, &property, sizeof(zval *), NULL);
property_info.name = ce->type & ZEND_INTERNAL_CLASS ? zend_strndup(name, name_length) : estrndup(name, name_length);
property_info.name_length = name_length;
break;
}
view raw gistfile1.c hosted with ❤ by GitHub

Depending on the declared visibility, zend_mangle_property_name is called with different parameters. For our example class “Foo” with a property “bar”, it would return “[NUL]Foo[NUL]bar” if it were declared private, “[NUL]*[NUL]bar” if it were declared protected, and simply “bar” if it were declared public, where [NUL] is the null byte. If you examine the output of a serialized object, you can actually see the mangled names of all of the object’s properties.

Why do we need to store multiple properties with the same name? Remember that these are actually distinct properties, and you can access a property of the same name from a subclass.

<?php
class Foo {
private $bar = 'foo';
public function getBar()
{
return $this->bar;
}
}
class Baz extends Foo {
private $bar = 'baz';
}
$baz = new Baz();
echo $baz->getBar(); // Prints 'foo'
view raw gistfile1.php hosted with ❤ by GitHub

Let’s get back to our example now. In the new context, the class entry defines a single property called $baz, while the properties hash table contains two items: $baz, which is stored with a mangled name of “[NUL]*[NUL]baz” and $bar which is stored with a mangled name of “[NUL]*[NUL]bar”. When zend_get_property_info tries to look up $bar in the class entry, it can’t find it since it doesn’t exist in the new class definition, so it returns that it has public visibility which has the mangled name “bar”. However as we saw earlier, the properties table has it stored under the mangled name “[NUL]*[NUL]bar”. Because there is a mismatch between the mangled name of the declaring class and the object, when it tries to look it up, it can’t find an entry with that hash key and results in the undefined property PHP notice.

As we’ve seen, serialization can be a bit tricky, but when used correctly, is an invaluable part of the PHP toolset. When deploying PHP code which changes the class definition of a serialized object, be sure to exercise caution to avoid making changes that will cause these class definition/object mismatches.