[php-src] PR #23376: Bind free list shadow pointers to the slot holding them
| From: | jvoisin | Date: | Wed, 19 Aug 2026 11:43:58 +0000 |
| Subject: | [php-src] PR #23376: Bind free list shadow pointers to the slot holding them | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-38005@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/23376
Author: jvoisin
The shadow of a free list pointer is currently BSWAP(next) ^ shadow_key, which does not depend on
where it is stored, meaning that:
- zend_mm_free_small() encodes whatever heap->free_slot[bin] happens to be, including NULL when
the bin has been drained, meaning the slot ends up holding shadow_key verbatim.
- A (next, shadow) pair harvested from one free slot is valid in every other slot of every bin. An
attacker who can read one free slot can therefore forge a link anywhere in the heap without needing
the key.
This commit adds the address of the slot that holds the shadow into the mix:
shadow = BSWAP(next) ^ shadow_key ^ (uintptr_t)holder
The holder term cancels on decode, so this is one extra xor on a register that is already live, with
no branch. Encoding NULL now yields shadow_key ^ holder rather than the key, and a shadow only
verifies in the slot it was written for.
This was verified under GDB: Freeing into a drained bin used to store shadow_key exactly; it now
stores shadow_key ^ holder (xoring the two back gives the slot address). Naïvely replaying a valid
(next, shadow) pair from one slot into another and traversing from it is accepted before this change
and aborts with "zend_mm_heap corrupted" after.
Performance-wise, the impact is in the noise level, which is expected as it more or less adds a
single
xor instruction per zend_mm_set_next_free_slot().
This commit is a follow up on 25360ef2495 and c561f7da858.