Skip to content

uaf in zend_test_compile_to_ast() on parse error #24523

Closed

Description

@peter-pe

Description

Summary

zend_compile_string_to_ast() destroys CG(ast_arena) when zendparse()
returns an error, but does not clear that compiler-global pointer before
copying it to the ast_arena out-parameter. zend_test_compile_to_ast() then
unconditionally calls zend_arena_destroy() on the returned dangling pointer.

This report was tested against php-src commit
b9ea38ef639f5b9bbb67d27834be9c52b922520b (master, reporting itself as
PHP 8.7.0-dev). The optional zend_test extension was enabled with
--enable-zend-test.

Relevant locations

  • allocation: Zend/zend_language_scanner.l:718
  • first destruction after a parse error: Zend/zend_language_scanner.l:723
  • propagation through the out-parameter: Zend/zend_language_scanner.l:729
  • second destruction: ext/zend_test/test.c:2513
  • first read through the freed arena in the ASan build: Zend/zend_arena.h:152

Allocation, propagation, destruction, and use chain

zend_test_compile_to_ast(str)
    -> local zend_arena *ast_arena
    -> zend_compile_string_to_ast(str, &ast_arena, ...)
        -> CG(ast_arena) = zend_arena_create(1024 * 32)
        -> zendparse() returns non-zero for invalid source
        -> zend_arena_destroy(CG(ast_arena))
        -> CG(ast_arena) is not cleared
        -> *ast_arena = CG(ast_arena)
           copies the dangling pointer to the caller's local variable
    -> zend_ast_export(..., NULL, ...) is safe
    -> zend_ast_destroy(NULL) is safe
    -> zend_arena_destroy(ast_arena)
        -> reads arena->prev through the already-freed arena

zend_restore_lexical_state() restores the previous CG(ast_arena) value,
but it does not alter the caller-local ast_arena after the freed pointer has
already been written through the out-parameter.

Code description

/* Zend/zend_language_scanner.l */

703 ZEND_API zend_ast *zend_compile_string_to_ast(
704         zend_string *code, zend_arena **ast_arena, zend_string *filename) {
...
715     zend_save_lexical_state(&original_lex_state);
716     zend_prepare_string_for_scanning(&code_zv, filename);
717     CG(ast) = NULL;
718     CG(ast_arena) = zend_arena_create(1024 * 32);
719     LANG_SCNG(yy_state) = yycINITIAL;

721     if (zendparse() != 0) {
722         zend_ast_destroy(CG(ast));
723         zend_arena_destroy(CG(ast_arena));
            /* CG(ast_arena) is not cleared here. */
724         CG(ast) = NULL;
725     }

727     /* restore_lexical_state changes CG(ast) and CG(ast_arena) */
728     ast = CG(ast);
729     *ast_arena = CG(ast_arena);
            /* On the error path this is the pointer freed at line 723. */

731     zend_restore_lexical_state(&original_lex_state);
...
736     return ast;
737 }
/* ext/zend_test/test.c */

2499 static PHP_FUNCTION(zend_test_compile_to_ast)
2500 {
...
2507     zend_arena *ast_arena;
2508     zend_ast *ast = zend_compile_string_to_ast(
             str, &ast_arena, ZSTR_EMPTY_ALLOC());

2510     zend_string *result = zend_ast_export("", ast, "");
2512     zend_ast_destroy(ast);
2513     zend_arena_destroy(ast_arena);
2515     RETVAL_STR(result);
2516 }

On this parse-error path, ast is NULL; zend_ast_export_ex() and
zend_ast_destroy() return immediately for a null AST. The arena destruction
does not have such a null or lifetime guard:

/* Zend/zend_arena.h, ZEND_TRACK_ARENA_ALLOC configuration */

149 static zend_always_inline void zend_arena_destroy(zend_arena *arena)
150 {
151     do {
152         zend_arena *prev = arena->prev;
...
157         efree(arena);
158         arena = prev;
159     } while (arena);
160 }

--enable-address-sanitizer defines ZEND_TRACK_ARENA_ALLOC. Therefore the
ASan run uses the tracked-arena implementation above and reports an 8,024-byte
arena allocation rather than the normal 32 KiB arena request at line 718.

Reproducer

<?php

zend_test_compile_to_ast("<?php (");

Build and run steps

The sanitizer build used an isolated archive of the tested source commit and
did not modify the source checkout. bison 3.8.2 and re2c 3.1 were
available during buildconf.

git clone https://github.com/php/php-src.git php-src-asan
cd php-src-asan
git checkout b9ea38ef639f5b9bbb67d27834be9c52b922520b

./buildconf --force
CC=clang CXX=clang++ CFLAGS='-O0 -g' CXXFLAGS='-O0 -g' \
  ./configure --enable-debug --enable-address-sanitizer --enable-zend-test
make -j2

Save the PHP code above as repro.php and run:

ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:halt_on_error=1 \
USE_ZEND_ALLOC=0 \
sapi/cli/php -n repro.php

The actual test binary was built in the same sanitizer configuration as the
separate DOM test, so it additionally had --enable-pcntl and
-DPHP_DOM_REPRO_FAULT_INJECTION. Neither is exercised by this reproducer and
both can be omitted for this issue.

Test environment

php-src: b9ea38ef639f5b9bbb67d27834be9c52b922520b (master, PHP 8.7.0-dev)
Platform: Linux x86_64
Compiler: clang 18.1.3
Configure: --enable-debug --enable-address-sanitizer --enable-zend-test --enable-pcntl
Runtime: ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:halt_on_error=1, USE_ZEND_ALLOC=0

Complete AddressSanitizer report

=================================================================
==2091699==ERROR: AddressSanitizer: heap-use-after-free on address 0x52500000a110 at pc 0x5dfa2b43d802 bp 0x7ffddbfb5520 sp 0x7ffddbfb5518
READ of size 8 at 0x52500000a110 thread T0
    #0 0x5dfa2b43d801 in zend_arena_destroy /tmp/php-asan-repro.uJtwEm/src/Zend/zend_arena.h:152:29
    #1 0x5dfa2b438c1d in zif_zend_test_compile_to_ast /tmp/php-asan-repro.uJtwEm/src/ext/zend_test/test.c:2513:2
    #2 0x5dfa2b976347 in ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER /tmp/php-asan-repro.uJtwEm/src/Zend/zend_vm_execute.h:1323:2
    #3 0x5dfa2b80d1f3 in execute_ex /tmp/php-asan-repro.uJtwEm/src/Zend/zend_vm_execute.h:110585:12
    #4 0x5dfa2b80dbab in zend_execute /tmp/php-asan-repro.uJtwEm/src/Zend/zend_vm_execute.h:116023:2
    #5 0x5dfa2bbf9200 in zend_execute_script /tmp/php-asan-repro.uJtwEm/src/Zend/zend.c:1981:3
    #6 0x5dfa2b451c16 in php_execute_script_ex /tmp/php-asan-repro.uJtwEm/src/main/main.c:2590:13
    #7 0x5dfa2b452188 in php_execute_script /tmp/php-asan-repro.uJtwEm/src/main/main.c:2630:9
    #8 0x5dfa2bbff23e in do_cli /tmp/php-asan-repro.uJtwEm/src/sapi/cli/php_cli.c:941:5
    #9 0x5dfa2bbfd46c in do_php_cli /tmp/php-asan-repro.uJtwEm/src/sapi/cli/php_cli.c:1354:18
    #10 0x5dfa2bbfc831 in main /tmp/php-asan-repro.uJtwEm/src/sapi/cli/php_cli_main.c:18:9
    #11 0x7f22a922a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #12 0x7f22a922a28a in __libc_start_main csu/../sysdeps/nptl/libc-start.c:360:3
    #13 0x5dfa2a0035a4 in _start (/tmp/php-asan-repro.uJtwEm/src/sapi/cli/php+0x6035a4) (BuildId: 7d03423bb36838a54b17dd3f41520e42316c80c1)

0x52500000a110 is located 16 bytes inside of 8024-byte region [0x52500000a100,0x52500000c058)
freed by thread T0 here:
    #0 0x5dfa2a09e15a in free (/tmp/php-asan-repro.uJtwEm/src/sapi/cli/php+0x69e15a) (BuildId: 7d03423bb36838a54b17dd3f41520e42316c80c1)
    #1 0x5dfa2b69df73 in __zend_free /tmp/php-asan-repro.uJtwEm/src/Zend/zend_alloc.c:3693:2
    #2 0x5dfa2b6a1df4 in _efree /tmp/php-asan-repro.uJtwEm/src/Zend/zend_alloc.c:2910:3
    #3 0x5dfa2bb04551 in zend_arena_destroy /tmp/php-asan-repro.uJtwEm/src/Zend/zend_arena.h:157:3
    #4 0x5dfa2bb03ba2 in zend_compile_string_to_ast /tmp/php-asan-repro.uJtwEm/src/Zend/zend_language_scanner.l:723:3
    #5 0x5dfa2b438be1 in zif_zend_test_compile_to_ast /tmp/php-asan-repro.uJtwEm/src/ext/zend_test/test.c:2508:18
    #6 0x5dfa2b976347 in ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER /tmp/php-asan-repro.uJtwEm/src/Zend/zend_vm_execute.h:1323:2
    #7 0x5dfa2b80d1f3 in execute_ex /tmp/php-asan-repro.uJtwEm/src/Zend/zend_vm_execute.h:110585:12
    #8 0x5dfa2b80dbab in zend_execute /tmp/php-asan-repro.uJtwEm/src/Zend/zend_vm_execute.h:116023:2
    #9 0x5dfa2bbf9200 in zend_execute_script /tmp/php-asan-repro.uJtwEm/src/Zend/zend.c:1981:3
    #10 0x5dfa2b451c16 in php_execute_script_ex /tmp/php-asan-repro.uJtwEm/src/main/main.c:2590:13
    #11 0x5dfa2b452188 in php_execute_script /tmp/php-asan-repro.uJtwEm/src/main/main.c:2630:9
    #12 0x5dfa2bbff23e in do_cli /tmp/php-asan-repro.uJtwEm/src/sapi/cli/php_cli.c:941:5
    #13 0x5dfa2bbfd46c in do_php_cli /tmp/php-asan-repro.uJtwEm/src/sapi/cli/php_cli.c:1354:18
    #14 0x5dfa2bbfc831 in main /tmp/php-asan-repro.uJtwEm/src/sapi/cli/php_cli_main.c:18:9
    #15 0x7f22a922a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #16 0x7f22a922a28a in __libc_start_main csu/../sysdeps/nptl/libc-start.c:360:3
    #17 0x5dfa2a0035a4 in _start (/tmp/php-asan-repro.uJtwEm/src/sapi/cli/php+0x6035a4) (BuildId: 7d03423bb36838a54b17dd3f41520e42316c80c1)

previously allocated by thread T0 here:
    #0 0x5dfa2a09e3f3 in malloc (/tmp/php-asan-repro.uJtwEm/src/sapi/cli/php+0x69e3f3) (BuildId: 7d03423bb36838a54b17dd3f41520e42316c80c1)
    #1 0x5dfa2b6a2323 in __zend_malloc /tmp/php-asan-repro.uJtwEm/src/Zend/zend_alloc.c:3665:14
    #2 0x5dfa2b6a1cf0 in _emalloc /tmp/php-asan-repro.uJtwEm/src/Zend/zend_alloc.c:2900:10
    #3 0x5dfa2bb04398 in zend_arena_create /tmp/php-asan-repro.uJtwEm/src/Zend/zend_arena.h:141:36
    #4 0x5dfa2bb03abb in zend_compile_string_to_ast /tmp/php-asan-repro.uJtwEm/src/Zend/zend_language_scanner.l:718:18
    #5 0x5dfa2b438be1 in zif_zend_test_compile_to_ast /tmp/php-asan-repro.uJtwEm/src/ext/zend_test/test.c:2508:18
    #6 0x5dfa2b976347 in ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER /tmp/php-asan-repro.uJtwEm/src/Zend/zend_vm_execute.h:1323:2
    #7 0x5dfa2b80d1f3 in execute_ex /tmp/php-asan-repro.uJtwEm/src/Zend/zend_vm_execute.h:110585:12
    #8 0x5dfa2b80dbab in zend_execute /tmp/php-asan-repro.uJtwEm/src/Zend/zend_vm_execute.h:116023:2
    #9 0x5dfa2bbf9200 in zend_execute_script /tmp/php-asan-repro.uJtwEm/src/Zend/zend.c:1981:3
    #10 0x5dfa2b451c16 in php_execute_script_ex /tmp/php-asan-repro.uJtwEm/src/main/main.c:2590:13
    #11 0x5dfa2b452188 in php_execute_script /tmp/php-asan-repro.uJtwEm/src/main/main.c:2630:9
    #12 0x5dfa2bbff23e in do_cli /tmp/php-asan-repro.uJtwEm/src/sapi/cli/php_cli.c:941:5
    #13 0x5dfa2bbfd46c in do_php_cli /tmp/php-asan-repro.uJtwEm/src/sapi/cli/php_cli.c:1354:18
    #14 0x5dfa2bbfc831 in main /tmp/php-asan-repro.uJtwEm/src/sapi/cli/php_cli_main.c:18:9
    #15 0x7f22a922a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #16 0x7f22a922a28a in __libc_start_main csu/../sysdeps/nptl/libc-start.c:360:3
    #17 0x5dfa2a0035a4 in _start (/tmp/php-asan-repro.uJtwEm/src/sapi/cli/php+0x6035a4) (BuildId: 7d03423bb36838a54b17dd3f41520e42316c80c1)

SUMMARY: AddressSanitizer: heap-use-after-free /tmp/php-asan-repro.uJtwEm/src/Zend/zend_arena.h:152:29 in zend_arena_destroy
Shadow bytes around the buggy address:
  0x525000009e80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x525000009f00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x525000009f80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x52500000a000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x52500000a080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x52500000a100: fd fd[fd]fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x52500000a180: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x52500000a200: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x52500000a280: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x52500000a300: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x52500000a380: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack after return:      f2
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
==2091699==ABORTING

PHP Version

PHP 8.5.12 (cli) (built: Oct 22 2026 10:59:14) (NTS DEBUG)
Copyright (c) The PHP Group
Zend Engine v4.5.12, Copyright (c) Zend Technologies
    with Zend OPcache v8.5.12, Copyright (c), by Zend Technologies

Operating System

Ubuntu 24.04

Activity

  1. changed the title [-]`zend_test_compile_to_ast()` uses a freed arena after a parse error[/-] [+]uaf in `zend_test_compile_to_ast()` on parse error[/+] on Oct 22, 2026
  2. mierudayo commented on Oct 22, 2026

    @mierudayo

    zend_compile_string_to_ast() frees CG(ast_arena) on the parse-error path but doesn't clear the pointer, so the caller gets a dangling pointer and frees it again. Fix: either drop the zend_arena_destroy() there and let the caller own the arena, or set CG(ast_arena) = NULL and null-check in the caller.

  3. added a commit that references this issue on Oct 22, 2026
    81dfb44
  4. added 2 commits that reference this issue on Oct 22, 2026
    11bbfc0
    a59b0ca
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions