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 25360ef and c561f7d.
The shadow of a free list pointer is currently BSWAP(next) ^ shadow_key, which does not depend on where it is stored, meaning that:
This commit adds the address of the slot that holds the shadow into the mix:
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 25360ef and c561f7d.