Skip to content

Add Memcached::SERIALIZER_PHPSER serializer - #584

Open
iliaal wants to merge 5 commits into
php-memcached-dev:masterfrom
iliaal:phpser-serializer
Open

iliaal wants to merge 5 commits into
php-memcached-dev:masterfrom
iliaal:phpser-serializer

Conversation

@iliaal

@iliaal iliaal commented Oct 15, 2026

Copy link
Copy Markdown
Member

Adds Memcached::SERIALIZER_PHPSER, backed by the C API in phpser 0.9.0+ (ext/phpser/php_phpser_api.h). phpser is my extension. Wiring follows igbinary: --enable-memcached-phpser, HAVE_MEMCACHED_PHPSER, ZEND_MOD_REQUIRED("phpser"), memcached.serializer = phpser, setOption, config.w32, and the PECL/PIE option. phpredis merged the same serializer in phpredis/phpredis#2959.

Two benefits: speed (19-85% faster encode, 26-92% faster decode than igbinary on phpser's ten-case ARM benchmark) and signing. With phpser.key set, each value carries an HMAC-SHA256 tag bound to the key and a memcached context, checked before any decode work.

php-memcached picks the decoder from the item's stored flags, not the client's serializer, so anyone who can write to memcached can store an item flagged MEMC_VAL_IS_SERIALIZED and get it unserialized. With phpser and phpser.key set:

  • Strings, bools, arrays, and objects are written through phpser. Integers and floats stay native, so increment()/decrement() work.
  • Reads accept only verified phpser items and native integers and floats. Anything else returns false with a warning, checked before decompression.
  • append()/prepend() are refused, as with compression.

Without phpser.key, phpser behaves like igbinary.

One side effect: a binary-protocol increment() with an initial value creates the item with string flags, so keyed get() returns false for it. Documented in the README.

Memcached::SERIALIZER_PHPSER (memcached.serializer=phpser) stores arrays,
objects, and null with phpser and flags them with the new
MEMC_VAL_IS_PHPSER type, as the igbinary serializer does. Scalars stay
native.

The module requires phpser and refuses to start unless phpser is 0.9.0 or
later and exports the expected API version. Build with
--enable-memcached-phpser; PECL and PIE expose the same option.
Items are decoded by the type in their stored flags, not by the client's
serializer, so anyone who can write to the memcached servers can store an
item flagged as PHP-serialized and have unserialize() run on it.

When the serializer is phpser and phpser.key is set, strings, booleans,
null, arrays, and objects are now written as signed phpser values;
integers and floats stay native so increment() and decrement() keep
working. Reads accept only phpser items, which phpser verifies before
decoding, and native integers and floats. Every other type reads back as
false with a warning, the same result as a value that fails to
unserialize. The check sits in s_memcached_result_to_zval(), which every
read path goes through, and runs before decompression.

append() and prepend() are refused in this mode, as they are with
compression, because they would break the signature.

A non-empty key shorter than phpser accepts also enables the gate; phpser
then rejects every phpser value.
Round trips with and without phpser.key, the stored type of each value,
counters, append/prepend refusal, and user flags. Attack tests write items
with forged flags over the raw protocol and through a PHP-serializer
client (PHP-serialized and igbinary gadgets, compressed gadgets, strings,
booleans, JSON, msgpack, unsigned, tampered, truncated, wrong-key, and
wrong-context phpser) and check that each reads back as false and that no
object is constructed, through get, getByKey, getMulti, getDelayed,
fetch, fetchAll, result callbacks, and read-through callbacks. A short
phpser.key fails closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant