Skip to content

sprintf()/pack()/bin2hex() _exit(137) at hard memory limit where 8.5.11 raised a catchable OOM (incomplete fallible-alloc conversion, 8.5.12/8.4.27) #24492

Open

Description

@VijhnAustin

Description

Since commit 31c0ea7d ("Avoid state corruption via bailout", CVE-2026-109618/109619), a non-fallible allocation that would cross the new hard memory limit (default 200% of memory_limit) terminates the process via _exit(137) in zend_mm_hard_limit_error() (Zend/zend_alloc.c). Most allocation sites were moved to the new fallible API, but several core functions still allocate non-fallibly from a small, caller-supplied size, so they kill the whole process instead of raising the catchable "Allowed memory size exhausted" fatal that 8.5.11 produced for the same input.

The following code:

<?php
sprintf("%400000000s", "");

Run with: php -n -d memory_limit=128M repro.php

Resulted in this output (8.5.12):

Hard memory limit of 268435456 bytes exhausted (tried to allocate 505413632 bytes) in Command line code on line 1

Exit code 137 — process terminated; the allocation is never actually performed (the hard-limit check rejects first).

But I expected this output instead (the 8.5.11 behavior):

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 503316512 bytes) in Command line code on line 1

Exit code 255 — catchable fatal, the process/worker survives.

The same process kill occurs from other small integer parameters — pack("x300000000"), number_format(1.0, 100000000), bcadd("1","1",100000000) — and from large in-heap inputs to bin2hex / base64_encode / urlencode / var_export / convert_uuencode / utf8_encode / quoted_printable_encode. A crafted phar:// manifest length triggers it from a ~100-byte file.

Sites still using the non-fallible allocator in 8.5.12:

  • smart_string_alloc() in PAD_CHAR — main/spprintf.c
  • zend_string_alloc(outputsize, 0) — ext/standard/pack.c:431
  • zend_string_safe_alloc(..., 0) in php_bin2hex() / php_utf8_encode() — ext/standard/string.c

Mechanism: zend_mm_check_limit() calls zend_mm_hard_limit_error() → _exit(137) when !heap->fallible and the allocation would cross hard_limit. Routing these sites through the fallible API (femalloc / zend_string_finit / smart_str_ferealloc) would keep the OOM catchable, consistent with the rest of the patch.

Reproduces identically on 8.4.27 (8.4.26 behaves like 8.5.11). The path is in Zend/zend_alloc.c, so it is platform-independent; tested here on official Windows x64 NTS builds.

PHP Version

PHP 8.5.12 (cli) (built: Oct 20 2026 04:19:56) (NTS Visual C++ 2022 x64)
Copyright (c) The PHP Group
Built by The PHP Group
Zend Engine v4.5.12, Copyright (c) Zend Technologies

Operating System

Windows 11 (code path is OS-independent — Zend/zend_alloc.c)

Activity

  1. rmetzler commented on Oct 21, 2026

    @rmetzler

    Another site with the same problem: ext/xml built against Expat (--with-expat). The memory handling suite passed to XML_ParserCreate_MM() still uses the non-fallible allocator:

    • php_xml_malloc_wrapper() → emalloc() (ext/xml/xml.c:209)
    • php_xml_realloc_wrapper() → erealloc() (ext/xml/xml.c:214)

    So any document that makes Expat allocate past memory_limit kills the process at the hard limit. With libxml2 the same script still produces the catchable fatal error. No CI job builds ext/xml with Expat, which is probably why make test there doesn't show it. In an Expat build these tests from the 8.5.12 tarball fail:

    • ext/xml/tests/oom_xml_parse_ns.phpt
    • ext/xml/tests/gh14637.phpt
    <?php
    $uri = str_repeat("u", 8192);
    $data = "<r xmlns:p=\"$uri\"";
    for ($i = 0; $i < 4000; $i++) {
        $data .= " p:a$i=\"\"";
    }
    $data .= "/>";
    
    $parser = xml_parser_create_ns();
    xml_set_element_handler($parser, function () {}, function () {});
    xml_parse($parser, $data, true);

    php -n -d extension=xml -d memory_limit=8M repro.php

    Expat 2.9.0 build, exit code 137:

    Hard memory limit of 16777216 bytes exhausted (tried to allocate 2097152 bytes) in /repro.php on line 11
    

    Same sources with libxml2 2.13.9, exit code 255:

    Fatal error: Allowed memory size of 8388608 bytes exhausted (tried to allocate 2097152 bytes) in /repro.php on line 11
    

    --enable-debug build, test diffs:

    oom_xml_parse_ns.phpt
    + Hard memory limit of 16777216 bytes exhausted at .../ext/xml/xml.c:209 (tried to allocate 2097152 bytes) in .../oom_xml_parse_ns.php on line 12
    + Termsig=9
    
    gh14637.phpt
    + Hard memory limit of 41943040 bytes exhausted at .../ext/xml/xml.c:214 (tried to allocate 4194304 bytes) in .../gh14637.php on line 15
    + Termsig=9
    

    Expat handles a failed allocation by reporting XML_ERROR_NO_MEMORY, so I switched the two wrappers to the fallible allocator (femalloc/ferealloc), as was done for libmagic's cdf.c: rmetzler@7833d0ab63 (branch fix/xml-expat-fallible-alloc, based on PHP-8.2). With it, all ext/xml tests pass in a ZTS + Expat build of PHP-8.2 and PHP-8.5, including the two above. On PHP-8.5 the only exception is xml003.phpt, which fails with Expat with or without the patch. Happy to open a PR if that's the right direction.

    Environment: PHP 8.5.12 ZTS (release and --enable-debug), official php-8.5.12.tar.xz built with Alpine's community/php85 APKBUILD and --with-expat, Alpine 3.23.6 (musl), aarch64 locally; the same tests also fail in CI on x86_64. NTS not tested.

  2. rmetzler commented on Oct 21, 2026

    @rmetzler

    Opened #24516 for the ext/xml part (against PHP-8.4).

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