Skip to content

var_destroy() destructor re-entry UAF survives the 2026-10 security fixes (stock 8.3.36 / 8.4.27) #24672

Closed as duplicate

Description

@srayuth089

var_destroy() frees the var_hash back-reference extension chunks, then fires __destruct without taking BG(serialize_lock). A destructor that calls unserialize('R:NNNN;') with N beyond VAR_ENTRIES_MAX resolves against the freed chunks — use-after-free read.

This survives the 2026-10 security fixes (3584a98): confirmed on stock builds of 8.3.36 and 8.4.27 (independent research had ASAN-confirmed it on 8.5.12). No existing tracker found.

Reproducer (public PoC from the Chain-B research, runs as-is):

$ docker run --rm -v "$PWD":/x:ro -w /x php:8.3-cli php poc/verify_uaf_asan.php
PHP 8.3.36 (cli) (built: Oct 21 2026)
[*] __destruct fired (serialize_lock=0, BG(unserialize).level>0)
[*] R:1500 resolved to: integer
[+] UAF CONFIRMED: R:1500 read from freed memory!
$ docker run --rm -v "$PWD":/x:ro -w /x php:8.4-cli php poc/verify_uaf_asan.php   # 8.4.27: same output

Script: https://github.com/ikow/php2shell-poc (poc/verify_uaf_asan.php). 8.5.12 ASAN evidence: https://github.com/dinosn/php-cve-2026-10961x-lab-poc (fault at var_unserializer.c:906 on 8.5.12; our runs above are stock non-ASAN builds, PHP-level UAF read signal).

Possible fix location: take BG(serialize_lock) around the __destruct dispatch in var_destroy(), consistent with the guard used elsewhere for engine-driven destructor dispatch.

Aware of the SECURITY.md classification for malicious destructors / unserialize() inputs — filing as a public memory-safety bug per that policy; redirect me if you want it handled differently.

Credit: chain family discovered by ikow (Chain B); 8.5.12 ASAN verification by dinosn; stock 8.3.36/8.4.27 verification by me.

Activity

  1. srayuth089 commented on Oct 25, 2026

    @srayuth089
    Author

    Closing as a duplicate of #24481 — my earlier searches for an existing tracker missed it, and #24481 (filed Oct 21, with an exploit in the report) covers this fully, including cross-branch stock-build confirmation (8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 / 7.4.33, per the Valgrind confirmation in the thread) and the write-side analysis. Credit for the discovery belongs there; nothing further from my side.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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