Skip to content

Delayed OOM raised during compilation is dropped when compilation fails #24631

Open

Description

@pentolope

Description

If the main script exceeds memory_limit while being compiled and compilation then fails, the memory error is never reported. The parse error is reported instead. zend_execute_script() only handles a pending delayed OOM when zend_compile_file() returns an op_array:

php-src/Zend/zend.c

Lines 1988 to 1993 in 31c0ea7

zend_result ret = SUCCESS;
if (op_array) {
zend_execute(op_array, retval);
if (ZEND_HAS_OOM()) {
zend_handle_delayed_oom();
}

@bhaak found the same problem in sapi/fuzzer/fuzzer-sapi.c: 31c0ea7#r206151500. Their fix also applies here: handle the delayed OOM outside the if (op_array) block.

The following code:

php -n -r 'file_put_contents("oom.php", "<?php\n\$a = \x27" . str_repeat("A", 3 << 19) . "\x27;\n)\n");'
php -n -d memory_limit=4M oom.php

Resulted in this output:

Parse error: Unmatched ')' in oom.php on line 3

But I expected this output instead:

Fatal error: Allowed memory size of 4194304 bytes exhausted (tried to allocate 2097152 bytes) in oom.php on line 2

Without the ) line, the memory error is reported.

PHP Version

PHP 8.5.12-dev (cli) (NTS), built from 31c0ea7d7ff1d1fccb6492e52dab544011628065

Operating System

Linux x86_64

Activity

  1. srayuth089 commented on Oct 25, 2026

    @srayuth089

    Quick mechanism note that might help triage: this looks like a gap in the delayed-OOM state machine introduced in 3584a98.

    The fix's design (per our reading of zend_alloc.h / zend_execute_API.c in that commit):

    1. EG(delayed_oom) transitions NONE → PENDING → HANDLING → HANDLED
    2. When an allocation fails, OOM is set to PENDING with message/filename/lineno stored
    3. zend_handle_delayed_oom() is supposed to be called at a safe point to report the error

    The issue described here — OOM raised during zend_compile_file() but parse error reported instead — suggests that the safe-point check in zend_execute_script() only fires when compilation succeeds (returns an op_array). If compilation fails, the pending OOM in EG(delayed_oom) is never consumed — it stays at PENDING or gets reset without being reported.

    Looking at the state machine, the reset points are:

    • zend.c:814 (executor init — memset clears delayed_oom)
    • main/main.c:1877 (start of request shutdown — state = NONE)
    • main/main.c:1972 (after shutdown_memory_manager — state = NONE)

    If the compilation-failure path reaches one of these resets before zend_handle_delayed_oom() gets a chance to report, the OOM is silently swallowed — which matches what you're seeing.

    Possible fix direction: call zend_handle_delayed_oom() in the compilation-failure path of zend_execute_script() (alongside or before the parse error report), or make the reset points check-and-report before clearing.

    (We reverse-engineered this machinery while investigating the related allocation-failure paths in #24563 — happy to share more detail if useful.)

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