Commit 3584a98bda0 for php

commit 3584a98bda0f2c0d367255b046798b0d3a221a19
Author: Arnaud Le Blanc <arnaud.lb@gmail.com>
Date:   Sun Oct 18 17:26:39 2026 +0200

    Avoid state corruption via bailout

    - Replace bailout on OOM with UnwindExit
    - Properly reset various globals that may be accessed during startup

    Fix GHSA-w8hw-w9fx-j79w (CVE-2026-109618)
    Fix GHSA-6wmf-4rv5-p3rp (CVE-2026-109619)

    Co-authored-by: Ilija Tovilo <ilija.tovilo@me.com>
    Co-authored-by: Jakub Zelenka <bukka@php.net>
    Co-authored-by: Nora Dossche <7771979+ndossche@users.noreply.github.com>
    Co-authored-by: Nicolas Grekas <nicolas.grekas@gmail.com>

diff --git a/Zend/tests/bug40770.phpt b/Zend/tests/bug40770.phpt
index bdbae4cf8f1..33be8238ed2 100644
--- a/Zend/tests/bug40770.phpt
+++ b/Zend/tests/bug40770.phpt
@@ -16,9 +16,9 @@
 <?php
 ini_set('display_errors',true);
 $mb=148;
-$var = '';
+$var = [];
 for ($i=0; $i<=$mb; $i++) {
-        $var.= str_repeat('a',1*1024*1024);
+        $var[] = str_repeat('a',1*1024*1024);
 }
 ?>
 --EXPECTF--
diff --git a/Zend/tests/bug54268.phpt b/Zend/tests/bug54268.phpt
index 3567aaa6ee5..f0df31bd7e3 100644
--- a/Zend/tests/bug54268.phpt
+++ b/Zend/tests/bug54268.phpt
@@ -2,6 +2,7 @@
 Bug #54268 (Double free when destroy_zend_class fails)
 --INI--
 memory_limit=8M
+opcache.jit=disable
 --SKIPIF--
 <?php
 $zend_mm_enabled = getenv("USE_ZEND_ALLOC");
diff --git a/Zend/tests/bug76846.phpt b/Zend/tests/bug76846.phpt
index cd837bd8609..49b8df729f1 100644
--- a/Zend/tests/bug76846.phpt
+++ b/Zend/tests/bug76846.phpt
@@ -2,6 +2,7 @@
 Bug #76846: Segfault in shutdown function after memory limit error
 --INI--
 memory_limit=33M
+zend.enable_gc=0
 --SKIPIF--
 <?php
 $zend_mm_enabled = getenv("USE_ZEND_ALLOC");
@@ -12,13 +13,18 @@
 --FILE--
 <?php

+class C {
+    public $c;
+    public function __construct() {
+        $this->c = $this;
+    }
+}
 register_shutdown_function(function() {
-    new stdClass;
+    new C;
 });

-$ary = [];
 while (true) {
-    $ary[] = new stdClass;
+    new C;
 }

 ?>
diff --git a/Zend/tests/fibers/get-return-after-bailout.phpt b/Zend/tests/fibers/get-return-after-bailout.phpt
index 04bd464cfab..7d8e146ef8c 100644
--- a/Zend/tests/fibers/get-return-after-bailout.phpt
+++ b/Zend/tests/fibers/get-return-after-bailout.phpt
@@ -1,12 +1,5 @@
 --TEST--
 Fiber::getReturn() after bailout
---SKIPIF--
-<?php
-$zend_mm_enabled = getenv("USE_ZEND_ALLOC");
-if ($zend_mm_enabled === "0") {
-    die("skip Zend MM disabled");
-}
-?>
 --FILE--
 <?php

@@ -16,13 +9,13 @@
 });

 $fiber = new Fiber(static function (): void {
-    str_repeat('X', PHP_INT_MAX);
+    var_dump(new class { public function __debugInfo() { return ''; } }); // cause a bailout
 });
 $fiber->start();

 ?>
 --EXPECTF--
-Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) %sget-return-after-bailout.php on line %d
+Fatal error: __debuginfo() must return an array in %s on line %d

 Fatal error: Uncaught FiberError: Cannot get fiber return value: The fiber exited with a fatal error in %sget-return-after-bailout.php:%d
 Stack trace:
diff --git a/Zend/tests/gh11189.phpt b/Zend/tests/gh11189.phpt
index adbc3ce487c..32aa533cbea 100644
--- a/Zend/tests/gh11189.phpt
+++ b/Zend/tests/gh11189.phpt
@@ -8,7 +8,7 @@
 if (getenv("USE_ZEND_ALLOC") === "0") die("skip ZMM is disabled");
 ?>
 --INI--
-memory_limit=2M
+memory_limit=10M
 --FILE--
 <?php

@@ -28,5 +28,4 @@

 ?>
 --EXPECTF--
-Success
-Fatal error: Allowed memory size of %s bytes exhausted%s(tried to allocate %s bytes) in %s on line %d
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/Zend/tests/gh11189_1.phpt b/Zend/tests/gh11189_1.phpt
index 17b9967bc31..0791ca87d62 100644
--- a/Zend/tests/gh11189_1.phpt
+++ b/Zend/tests/gh11189_1.phpt
@@ -28,5 +28,4 @@

 ?>
 --EXPECTF--
-Success
-Fatal error: Allowed memory size of %s bytes exhausted%s(tried to allocate %s bytes) in %s on line %d
+Fatal error: Allowed memory size of 2097152 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/Zend/tests/oom_SplFixedArray___unserialize.phpt b/Zend/tests/oom_SplFixedArray___unserialize.phpt
new file mode 100644
index 00000000000..3f984239345
--- /dev/null
+++ b/Zend/tests/oom_SplFixedArray___unserialize.phpt
@@ -0,0 +1,20 @@
+--TEST--
+OOM SplFixedArray::__unserialize()
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+$elems = [];
+for ($i = 0; $i < 64*1024; $i++) {
+    $elems[] = "i:$i;N;";
+}
+$payload = 'O:13:"SplFixedArray":' . count($elems) . ':{' . implode('', $elems) . '}';
+
+$pad = str_repeat('a', max(10*1024*1024 - memory_get_usage(true) - 512*1024, 1));
+
+unserialize($payload);
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 11
diff --git a/Zend/tests/oom_SplFixedArray___wakeup.phpt b/Zend/tests/oom_SplFixedArray___wakeup.phpt
new file mode 100644
index 00000000000..d644acc5cf9
--- /dev/null
+++ b/Zend/tests/oom_SplFixedArray___wakeup.phpt
@@ -0,0 +1,19 @@
+--TEST--
+OOM SplFixedArray::fromArray() 003
+--INI--
+memory_limit=16M
+--FILE--
+<?php
+
+$a = new SplFixedArray();
+for ($i = 0; $i < 128*1024; $i++) {
+    @$a->{$i} = 0;
+}
+
+$pad = str_repeat('a', max(16*1024*1024 - memory_get_usage(true) - 512*1024, 1));
+
+$a->__wakeup();
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 16777216 bytes exhausted %S(tried to allocate %d bytes) in %s on line 10
diff --git a/Zend/tests/oom_SplFixedArray_copy_ctor.phpt b/Zend/tests/oom_SplFixedArray_copy_ctor.phpt
new file mode 100644
index 00000000000..b2183395f35
--- /dev/null
+++ b/Zend/tests/oom_SplFixedArray_copy_ctor.phpt
@@ -0,0 +1,16 @@
+--TEST--
+OOM SplFixedArray copy ctor
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+$a = new SplFixedArray(64*1024);
+
+$pad = str_repeat('a', max(10*1024*1024 - memory_get_usage(true) - 1*1024*1024, 1));
+
+clone $a;
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 7
diff --git a/Zend/tests/oom_SplFixedArray_fromArray_001.phpt b/Zend/tests/oom_SplFixedArray_fromArray_001.phpt
new file mode 100644
index 00000000000..4aa73525f63
--- /dev/null
+++ b/Zend/tests/oom_SplFixedArray_fromArray_001.phpt
@@ -0,0 +1,12 @@
+--TEST--
+OOM SplFixedArray::fromArray()
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+SplFixedArray::fromArray([10*1024*1024 => 0], true);
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 3
diff --git a/Zend/tests/oom_SplFixedArray_fromArray_002.phpt b/Zend/tests/oom_SplFixedArray_fromArray_002.phpt
new file mode 100644
index 00000000000..aca5ac1b930
--- /dev/null
+++ b/Zend/tests/oom_SplFixedArray_fromArray_002.phpt
@@ -0,0 +1,14 @@
+--TEST--
+OOM SplFixedArray::fromArray() 002
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+$array = array_fill(0, 128*1024, 0);
+$pad = str_repeat('a', max(10*1024*1024 - memory_get_usage(true) - 512*1024, 1));
+SplFixedArray::fromArray($array, false);
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 5
diff --git a/Zend/tests/oom_after_main_script.phpt b/Zend/tests/oom_after_main_script.phpt
new file mode 100644
index 00000000000..aa8754da03f
--- /dev/null
+++ b/Zend/tests/oom_after_main_script.phpt
@@ -0,0 +1,26 @@
+--TEST--
+OOM: shutdown functions run after an OOM raised at the end of the main script
+--INI--
+memory_limit=8M
+opcache.optimization_level=0
+--FILE--
+<?php
+
+register_shutdown_function(function () {
+    echo "Shutdown function called\n";
+});
+
+function test() {
+    $a = str_repeat('a', 3 * 1024 * 1024);
+    $b = $a . $a;
+    echo "Done\n";
+}
+
+test();
+
+?>
+--EXPECTF--
+Done
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+Shutdown function called
diff --git a/Zend/tests/oom_array_fill_001.phpt b/Zend/tests/oom_array_fill_001.phpt
new file mode 100644
index 00000000000..515572ebe81
--- /dev/null
+++ b/Zend/tests/oom_array_fill_001.phpt
@@ -0,0 +1,18 @@
+--TEST--
+OOM array_fill() packed
+--INI--
+memory_limit=100M
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE !== 8) {
+    die('skip 64-bit only');
+}
+?>
+--FILE--
+<?php
+
+var_dump(array_fill(0, 1024*1024*1024-1, (string)time()));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 104857600 bytes exhausted %S(tried to allocate %d bytes) in %s on line 3
diff --git a/Zend/tests/oom_array_fill_002.phpt b/Zend/tests/oom_array_fill_002.phpt
new file mode 100644
index 00000000000..1da2f62bb14
--- /dev/null
+++ b/Zend/tests/oom_array_fill_002.phpt
@@ -0,0 +1,18 @@
+--TEST--
+OOM array_fill() mixed
+--INI--
+memory_limit=100M
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE !== 8) {
+    die('skip 64-bit only');
+}
+?>
+--FILE--
+<?php
+
+var_dump(array_fill(-1, 1024*1024*1024-1, (string)time()));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 104857600 bytes exhausted %S(tried to allocate %d bytes) in %s on line 3
diff --git a/Zend/tests/oom_array_fill_003.phpt b/Zend/tests/oom_array_fill_003.phpt
new file mode 100644
index 00000000000..cdf90d30477
--- /dev/null
+++ b/Zend/tests/oom_array_fill_003.phpt
@@ -0,0 +1,18 @@
+--TEST--
+OOM array_fill() packed > HT_MAX_SIZE
+--INI--
+memory_limit=100M
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE !== 8) {
+    die('skip 64-bit only');
+}
+?>
+--FILE--
+<?php
+
+var_dump(array_fill(0, 1024*1024*1024, (string)time()));
+
+?>
+--EXPECTF--
+Fatal error: Possible integer overflow in memory allocation (1073741824 * 32 + 32) in %s on line 3
diff --git a/Zend/tests/oom_array_fill_004.phpt b/Zend/tests/oom_array_fill_004.phpt
new file mode 100644
index 00000000000..0bbb100f183
--- /dev/null
+++ b/Zend/tests/oom_array_fill_004.phpt
@@ -0,0 +1,18 @@
+--TEST--
+OOM array_fill() mixed > HT_MAX_SIZE
+--INI--
+memory_limit=100M
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE !== 8) {
+    die('skip 64-bit only');
+}
+?>
+--FILE--
+<?php
+
+var_dump(array_fill(-1, 1024*1024*1024, (string)time()));
+
+?>
+--EXPECTF--
+Fatal error: Possible integer overflow in memory allocation (1073741824 * 32 + 32) in %s on line 3
diff --git a/Zend/tests/oom_array_pad_001.phpt b/Zend/tests/oom_array_pad_001.phpt
new file mode 100644
index 00000000000..fbdfdd871a8
--- /dev/null
+++ b/Zend/tests/oom_array_pad_001.phpt
@@ -0,0 +1,12 @@
+--TEST--
+OOM array_pad left packed
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+var_dump(array_pad([1,2,3], -1*1024*1024, (string)time()));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 3
diff --git a/Zend/tests/oom_array_pad_002.phpt b/Zend/tests/oom_array_pad_002.phpt
new file mode 100644
index 00000000000..23fba26bffb
--- /dev/null
+++ b/Zend/tests/oom_array_pad_002.phpt
@@ -0,0 +1,12 @@
+--TEST--
+OOM array_pad right packed
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+var_dump(array_pad([1,2,3], 1024*1024, (string)time()));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 3
diff --git a/Zend/tests/oom_array_pad_003.phpt b/Zend/tests/oom_array_pad_003.phpt
new file mode 100644
index 00000000000..14fe2b8b5a6
--- /dev/null
+++ b/Zend/tests/oom_array_pad_003.phpt
@@ -0,0 +1,12 @@
+--TEST--
+OOM array_pad left mixed
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+var_dump(array_pad(['foo' => 'bar'], -1*1024*1024, (string)time()));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 3
diff --git a/Zend/tests/oom_array_pad_004.phpt b/Zend/tests/oom_array_pad_004.phpt
new file mode 100644
index 00000000000..eecd3afaab1
--- /dev/null
+++ b/Zend/tests/oom_array_pad_004.phpt
@@ -0,0 +1,12 @@
+--TEST--
+OOM array_pad right mixed
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+var_dump(array_pad(['foo' => 'bar'], 1024*1024, (string)time()));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 3
diff --git a/Zend/tests/oom_array_pad_005.phpt b/Zend/tests/oom_array_pad_005.phpt
new file mode 100644
index 00000000000..b9e27260e4b
--- /dev/null
+++ b/Zend/tests/oom_array_pad_005.phpt
@@ -0,0 +1,12 @@
+--TEST--
+OOM array_pad left packed, non-refcounted pad value
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+var_dump(array_pad([1,2,3], -1*1024*1024, 0x4141414141));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 3
diff --git a/Zend/tests/oom_array_pad_006.phpt b/Zend/tests/oom_array_pad_006.phpt
new file mode 100644
index 00000000000..655f5ebbf39
--- /dev/null
+++ b/Zend/tests/oom_array_pad_006.phpt
@@ -0,0 +1,12 @@
+--TEST--
+OOM array_pad right packed, non-refcounted pad value
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+var_dump(array_pad([1,2,3], 1024*1024, 0x4141414141));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 3
diff --git a/Zend/tests/oom_array_pad_007.phpt b/Zend/tests/oom_array_pad_007.phpt
new file mode 100644
index 00000000000..c6df268337a
--- /dev/null
+++ b/Zend/tests/oom_array_pad_007.phpt
@@ -0,0 +1,12 @@
+--TEST--
+OOM array_pad left mixed, non-refcounted pad value
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+var_dump(array_pad(['foo' => 'bar'], -1*1024*1024, 0x4141414141));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 3
diff --git a/Zend/tests/oom_array_pad_008.phpt b/Zend/tests/oom_array_pad_008.phpt
new file mode 100644
index 00000000000..1f1779fe07e
--- /dev/null
+++ b/Zend/tests/oom_array_pad_008.phpt
@@ -0,0 +1,12 @@
+--TEST--
+OOM array_pad right mixed, non-refcounted pad value
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+var_dump(array_pad(['foo' => 'bar'], 1024*1024, 0x4141414141));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 3
diff --git a/Zend/tests/oom_call_function.phpt b/Zend/tests/oom_call_function.phpt
new file mode 100644
index 00000000000..c76dc4036ce
--- /dev/null
+++ b/Zend/tests/oom_call_function.phpt
@@ -0,0 +1,18 @@
+--TEST--
+OOM OOM in zend_call_function()
+--INI--
+memory_limit=10M
+--ENV--
+call_user_func=call_user_func
+--FILE--
+<?php
+
+$a = [];
+for ($i = 0; $i < 10; $i++) {
+    /* Check that call_user_func() doesn't reset vm_interrupt without checking for OOMs */
+    $a[] = getenv('call_user_func')('str_repeat', 'a', 10*1024*1024);
+}
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 6
diff --git a/Zend/tests/oom_call_function_002.phpt b/Zend/tests/oom_call_function_002.phpt
new file mode 100644
index 00000000000..4471af8a361
--- /dev/null
+++ b/Zend/tests/oom_call_function_002.phpt
@@ -0,0 +1,27 @@
+--TEST--
+OOM zend_call_function() after OOM: destructor called by an internal function
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+
+class A {
+    public function __destruct() {
+        echo "not reached\n";
+    }
+}
+
+/* array_splice() allocates the new table first, which crosses the soft limit,
+ * then destroys the removed element. The destructor call goes through
+ * zend_call_known_function(), which used to bailout with "Couldn't execute method"
+ * instead of the OOM. */
+$n = 200000;
+$arr = range(1, $n);
+$arr[$n - 1] = new A;
+array_splice($arr, $n - 1, 1);
+
+echo "not reached\n";
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/Zend/tests/oom_call_function_003.phpt b/Zend/tests/oom_call_function_003.phpt
new file mode 100644
index 00000000000..558020882ca
--- /dev/null
+++ b/Zend/tests/oom_call_function_003.phpt
@@ -0,0 +1,39 @@
+--TEST--
+OOM zend_call_function() after OOM: delayed __unserialize() call
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+
+class U {
+    public function __unserialize(array $data): void {
+        echo "not reached\n";
+    }
+}
+
+register_shutdown_function(function () {
+    /* The delayed call happens in var_destroy(), which used to bail out with
+     * "Couldn't execute method" */
+    var_dump(unserialize('a:1:{i:0;R:1;}'));
+});
+
+/* An object with __unserialize(), then 40000 one-element arrays: ~760 KB
+ * serialized, ~10 MB unserialized. */
+$n = 40000;
+$ser = 'a:' . ($n + 1) . ':{i:0;O:1:"U":1:{s:1:"a";i:1;}';
+for ($i = 1; $i <= $n; $i++) {
+    $ser .= 'i:' . $i . ';a:1:{i:0;i:0;}';
+}
+$ser .= '}';
+
+unserialize($ser);
+
+echo "not reached\n";
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+array(1) {
+  [0]=>
+  *RECURSION*
+}
diff --git a/Zend/tests/oom_debug_backtrace.phpt b/Zend/tests/oom_debug_backtrace.phpt
new file mode 100644
index 00000000000..dde8792c546
--- /dev/null
+++ b/Zend/tests/oom_debug_backtrace.phpt
@@ -0,0 +1,21 @@
+--TEST--
+OOM: debug_backtrace() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+
+function f(int $n) {
+    if ($n > 0) {
+        f($n - 1);
+        return;
+    }
+    debug_backtrace();
+}
+
+f(30000);
+
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/Zend/tests/oom_debug_backtrace_args.phpt b/Zend/tests/oom_debug_backtrace_args.phpt
new file mode 100644
index 00000000000..5664402084d
--- /dev/null
+++ b/Zend/tests/oom_debug_backtrace_args.phpt
@@ -0,0 +1,22 @@
+--TEST--
+OOM: debug_backtrace() doesn't reach hard limit - args
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+
+function f(int $n) {
+    if ($n > 0) {
+        f($n - 1, ...$GLOBALS['args']);
+        return;
+    }
+    debug_backtrace();
+}
+
+$args = array_fill(0, 512, 1);
+f(680, ...$args);
+
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/Zend/tests/oom_debug_backtrace_named_args.phpt b/Zend/tests/oom_debug_backtrace_named_args.phpt
new file mode 100644
index 00000000000..bb13c1fa75c
--- /dev/null
+++ b/Zend/tests/oom_debug_backtrace_named_args.phpt
@@ -0,0 +1,29 @@
+--TEST--
+OOM: debug_backtrace() doesn't reach hard limit - named args
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+
+/* A variadic is required: extra named args are only accepted by a function
+ * that collects them. */
+function f(int $n, ...$rest) {
+    if ($n > 0) {
+        f($n - 1, ...$GLOBALS['named']);
+        return;
+    }
+    debug_backtrace();
+}
+
+$named = [];
+for ($i = 0; $i < 512; $i++) {
+    $named["a$i"] = 1;
+}
+$GLOBALS['named'] = $named;
+
+f(450, ...$named);
+
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/Zend/tests/oom_error_get_last.phpt b/Zend/tests/oom_error_get_last.phpt
new file mode 100644
index 00000000000..3d291d47fc4
--- /dev/null
+++ b/Zend/tests/oom_error_get_last.phpt
@@ -0,0 +1,27 @@
+--TEST--
+OOM: the message returned by error_get_last() can outlive the error state
+--INI--
+memory_limit=8M
+opcache.optimization_level=0
+--FILE--
+<?php
+
+register_shutdown_function(function () {
+    $error = error_get_last();
+    error_clear_last();
+    echo $error['message'], "\n";
+});
+
+function test() {
+    $a = [];
+    while (true) {
+        $a[] = str_repeat('a', 1024);
+    }
+}
+
+test();
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes)
diff --git a/Zend/tests/oom_exception_trace.phpt b/Zend/tests/oom_exception_trace.phpt
new file mode 100644
index 00000000000..6ad4cb2ea5d
--- /dev/null
+++ b/Zend/tests/oom_exception_trace.phpt
@@ -0,0 +1,25 @@
+--TEST--
+OOM: Exception constructor doesn't reach hard limit while fetching backtrace
+--INI--
+memory_limit=8M
+zend.exception_ignore_args=0
+--FILE--
+<?php
+
+function f(int $n) {
+    if ($n > 0) {
+        f($n - 1);
+        return;
+    }
+    throw new Exception();
+}
+
+try {
+    f(30000);
+} catch (Exception $e) {
+}
+
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/Zend/tests/oom_hard_limit_exceeded.phpt b/Zend/tests/oom_hard_limit_exceeded.phpt
new file mode 100644
index 00000000000..9878ebcd81d
--- /dev/null
+++ b/Zend/tests/oom_hard_limit_exceeded.phpt
@@ -0,0 +1,26 @@
+--TEST--
+OOM: Hard limit exceeded
+--EXTENSIONS--
+zend_test
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+
+function f() {
+    zend_test_emalloc(1024*1024*1024);
+}
+
+function shutdown_function() {
+    echo "Not executed\n";
+}
+
+register_shutdown_function('shutdown_function');
+f();
+
+?>
+--EXPECTF--
+Hard memory limit of 20971520 bytes exhausted %S(tried to allocate %d bytes) in %s on line 5
+
+Termsig=9
diff --git a/Zend/tests/oom_hard_limit_exceeded_during_complilation.phpt b/Zend/tests/oom_hard_limit_exceeded_during_complilation.phpt
new file mode 100644
index 00000000000..0facbe6be62
--- /dev/null
+++ b/Zend/tests/oom_hard_limit_exceeded_during_complilation.phpt
@@ -0,0 +1,42 @@
+--TEST--
+OOM: Hard limit exceeded during compilation
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+if (!function_exists('str_increment')) {
+    function str_increment(string $s) {
+        $s++;
+        return $s;
+    }
+}
+
+$fname = __DIR__ . '/OOM_hard_limit_exceeded_during_complilation.test.php';
+$fd = fopen($fname, 'w');
+
+fprintf($fd, "<?php\n");
+
+$name = str_repeat('A', 100);
+for ($i = 0; $i < 10000; $i++) {
+    fprintf(
+        $fd,
+        "class %s { public \$a; public \$b; function f() {} function g() {} }\n",
+        $name,
+    );
+    $name = str_increment($name);
+}
+
+fclose($fd);
+
+require $fname;
+
+?>
+--CLEAN--
+<?php
+unlink(__DIR__ . '/OOM_hard_limit_exceeded_during_complilation.test.php');
+?>
+--EXPECTF--
+Hard memory limit of 20971520 bytes %s (tried to allocate %d bytes) in %s on line %d
+
+Termsig=9
diff --git a/Zend/tests/oom_interrupt.phpt b/Zend/tests/oom_interrupt.phpt
new file mode 100644
index 00000000000..ed408fca4d8
--- /dev/null
+++ b/Zend/tests/oom_interrupt.phpt
@@ -0,0 +1,38 @@
+--TEST--
+OOM: zend_interrupt_function
+--EXTENSIONS--
+pcntl
+posix
+--FILE--
+<?php
+
+// Handle signals from a zend_interrupt_function
+pcntl_async_signals(true);
+
+$handled = 0;
+
+pcntl_signal(SIGTERM, function () use (&$handled) {
+    echo "Handled SIGTERM\n";
+    if (++$handled >= 10) {
+        exit;
+    }
+});
+
+$a = [];
+for ($i = 0; ; $i++) {
+    $a[0] = $i;
+    posix_kill(posix_getpid(), SIGTERM);
+}
+
+?>
+--EXPECT--
+Handled SIGTERM
+Handled SIGTERM
+Handled SIGTERM
+Handled SIGTERM
+Handled SIGTERM
+Handled SIGTERM
+Handled SIGTERM
+Handled SIGTERM
+Handled SIGTERM
+Handled SIGTERM
diff --git a/Zend/tests/oom_interrupt_unfinished_call_frame.phpt b/Zend/tests/oom_interrupt_unfinished_call_frame.phpt
new file mode 100644
index 00000000000..78fa400314c
--- /dev/null
+++ b/Zend/tests/oom_interrupt_unfinished_call_frame.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Delayed OOM while a call frame is only partially built
+--DESCRIPTION--
+A delayed OOM throws from the VM interrupt handler, which runs between two
+oplines. Argument unpacking grows test()'s frame past a VM stack page, so it is
+ZEND_CALL_ALLOCATED and owns the page; the interrupt is then delivered after the
+explode() call, while that frame is still pending.
+--FILE--
+<?php
+
+function test(...$args) {}
+
+$args = array_fill(0, 20000, 1);
+$str = str_repeat('abcdefgh,', 300);
+
+test(...$args, ...[explode(',', $str)]);
+
+?>
+===DONE===
+--EXPECT--
+===DONE===
diff --git a/Zend/tests/oom_interrupt_unfinished_call_frame_fiber.phpt b/Zend/tests/oom_interrupt_unfinished_call_frame_fiber.phpt
new file mode 100644
index 00000000000..d4b5fee4a8c
--- /dev/null
+++ b/Zend/tests/oom_interrupt_unfinished_call_frame_fiber.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Delayed OOM while a call frame is only partially built (fiber VM stack)
+--FILE--
+<?php
+
+function test(...$args) {}
+
+$fiber = new Fiber(function () {
+    $args = array_fill(0, 2000, 1);
+    $str = str_repeat('abcdefgh,', 100);
+    test(...$args, ...[explode(',', $str)]);
+});
+$fiber->start();
+
+?>
+===DONE===
+--EXPECT--
+===DONE===
diff --git a/Zend/tests/oom_json_decode_001.phpt b/Zend/tests/oom_json_decode_001.phpt
new file mode 100644
index 00000000000..41044871d49
--- /dev/null
+++ b/Zend/tests/oom_json_decode_001.phpt
@@ -0,0 +1,14 @@
+--TEST--
+OOM json_decode() array
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+$elems = '0'.str_repeat(',0', 1024*1024);
+$json = '[' . $elems . ']';
+var_dump(json_decode($json));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 5
diff --git a/Zend/tests/oom_json_decode_002.phpt b/Zend/tests/oom_json_decode_002.phpt
new file mode 100644
index 00000000000..75fcf791c75
--- /dev/null
+++ b/Zend/tests/oom_json_decode_002.phpt
@@ -0,0 +1,15 @@
+--TEST--
+OOM json_decode() nested arrays
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+$depth = 1024;
+$nested = str_repeat('[', $depth) . str_repeat(']', $depth);
+$json = '[' . $nested . str_repeat(','.$nested, 1024) . ']';
+var_dump(json_decode($json, depth: $depth+2));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 6
diff --git a/Zend/tests/oom_json_decode_003.phpt b/Zend/tests/oom_json_decode_003.phpt
new file mode 100644
index 00000000000..aefd03288f9
--- /dev/null
+++ b/Zend/tests/oom_json_decode_003.phpt
@@ -0,0 +1,22 @@
+--TEST--
+OOM json_decode() object
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+$chunks = [];
+$chunksize = 64;
+for ($i = 1; $i < 128*1024; $i++) {
+    if (isset($chunks[$i % $chunksize])) {
+        $chunks[$i % $chunksize] .= ',"k'.$i.'":0';
+    } else {
+        $chunks[$i % $chunksize] = '"k'.$i.'":0';
+    }
+}
+$json = '{' . implode(',', $chunks) . '}';
+var_dump(json_decode($json));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 13
diff --git a/Zend/tests/oom_json_decode_004.phpt b/Zend/tests/oom_json_decode_004.phpt
new file mode 100644
index 00000000000..d1541a34704
--- /dev/null
+++ b/Zend/tests/oom_json_decode_004.phpt
@@ -0,0 +1,19 @@
+--TEST--
+OOM json_decode() nested objects
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+$depth = 1024;
+$nested = str_repeat('{"o":', $depth) . '{"o":null}' . str_repeat('}', $depth);
+$elems = '"o0":'.$nested;
+for ($i = 0; $i < 256; $i++) {
+    $elems .= ',"o'.$i.'":'.$nested;
+}
+$json = '{' . $elems . '}';
+var_dump(json_decode($json, depth: $depth+3));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 10
diff --git a/Zend/tests/oom_json_decode_005.phpt b/Zend/tests/oom_json_decode_005.phpt
new file mode 100644
index 00000000000..42c1b45d26d
--- /dev/null
+++ b/Zend/tests/oom_json_decode_005.phpt
@@ -0,0 +1,22 @@
+--TEST--
+OOM json_decode() object as arrays
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+$chunks = [];
+$chunksize = 64;
+for ($i = 1; $i < 128*1024; $i++) {
+    if (isset($chunks[$i % $chunksize])) {
+        $chunks[$i % $chunksize] .= ',"k'.$i.'":0';
+    } else {
+        $chunks[$i % $chunksize] = '"k'.$i.'":0';
+    }
+}
+$json = '{' . implode(',', $chunks) . '}';
+var_dump(json_decode($json, associative: true));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 13
diff --git a/Zend/tests/oom_json_decode_006.phpt b/Zend/tests/oom_json_decode_006.phpt
new file mode 100644
index 00000000000..b3bb9c3159a
--- /dev/null
+++ b/Zend/tests/oom_json_decode_006.phpt
@@ -0,0 +1,19 @@
+--TEST--
+OOM json_decode() nested objects as arrays
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+$depth = 1024;
+$nested = str_repeat('{"o":', $depth) . '{"o":null}' . str_repeat('}', $depth);
+$elems = '"o0":'.$nested;
+for ($i = 0; $i < 256; $i++) {
+    $elems .= ',"o'.$i.'":'.$nested;
+}
+$json = '{' . $elems . '}';
+var_dump(json_decode($json, associative: true, depth: $depth+3));
+var_dump(json_last_error_msg());
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 10
diff --git a/Zend/tests/oom_json_decode_007.phpt b/Zend/tests/oom_json_decode_007.phpt
new file mode 100644
index 00000000000..fd863398f00
--- /dev/null
+++ b/Zend/tests/oom_json_decode_007.phpt
@@ -0,0 +1,14 @@
+--TEST--
+OOM json_decode() empty objects
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+$elems = '{}'.str_repeat(',{}', 1024*1024);
+$json = '[' . $elems . ']';
+var_dump(json_decode($json));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 5
diff --git a/Zend/tests/oom_pending_at_shutdown.phpt b/Zend/tests/oom_pending_at_shutdown.phpt
new file mode 100644
index 00000000000..11f37802637
--- /dev/null
+++ b/Zend/tests/oom_pending_at_shutdown.phpt
@@ -0,0 +1,16 @@
+--TEST--
+An OOM left pending by a shutdown function is reported
+--INI--
+memory_limit=16M
+--FILE--
+<?php
+register_shutdown_function(function () {
+    echo "Shutdown function called\n";
+    $a = str_repeat('a', 6 * 1024 * 1024);
+    $GLOBALS['b'] = $a . $a;
+});
+?>
+--EXPECTF--
+Shutdown function called
+
+Fatal error: Allowed memory size of 16777216 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/Zend/tests/oom_range_001.phpt b/Zend/tests/oom_range_001.phpt
new file mode 100644
index 00000000000..97fa5077175
--- /dev/null
+++ b/Zend/tests/oom_range_001.phpt
@@ -0,0 +1,18 @@
+--TEST--
+OOM range() decreasing float
+--INI--
+memory_limit=100M
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE !== 8) {
+    die('skip 64-bit only');
+}
+?>
+--FILE--
+<?php
+
+var_dump(range(100*1024*1024+0.5, 0.5));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 104857600 bytes exhausted %S(tried to allocate %d bytes) in %s on line 3
diff --git a/Zend/tests/oom_range_002.phpt b/Zend/tests/oom_range_002.phpt
new file mode 100644
index 00000000000..3a0edebcf13
--- /dev/null
+++ b/Zend/tests/oom_range_002.phpt
@@ -0,0 +1,18 @@
+--TEST--
+OOM range() increasing float
+--INI--
+memory_limit=100M
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE !== 8) {
+    die('skip 64-bit only');
+}
+?>
+--FILE--
+<?php
+
+var_dump(range(0.5, 100*1024*1024+0.5));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 104857600 bytes exhausted %S(tried to allocate %d bytes) in %s on line 3
diff --git a/Zend/tests/oom_range_003.phpt b/Zend/tests/oom_range_003.phpt
new file mode 100644
index 00000000000..d83aec36cbd
--- /dev/null
+++ b/Zend/tests/oom_range_003.phpt
@@ -0,0 +1,18 @@
+--TEST--
+OOM range() decreasing int
+--INI--
+memory_limit=100M
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE !== 8) {
+    die('skip 64-bit only');
+}
+?>
+--FILE--
+<?php
+
+var_dump(range(100*1024*1024, 0));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 104857600 bytes exhausted %S(tried to allocate %d bytes) in %s on line 3
diff --git a/Zend/tests/oom_range_004.phpt b/Zend/tests/oom_range_004.phpt
new file mode 100644
index 00000000000..7d36c9aabfc
--- /dev/null
+++ b/Zend/tests/oom_range_004.phpt
@@ -0,0 +1,18 @@
+--TEST--
+OOM range() increasing int
+--INI--
+memory_limit=100M
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE !== 8) {
+    die('skip 64-bit only');
+}
+?>
+--FILE--
+<?php
+
+var_dump(range(0, 100*1024*1024));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 104857600 bytes exhausted %S(tried to allocate %d bytes) in %s on line 3
diff --git a/Zend/tests/oom_request_parsing.phpt b/Zend/tests/oom_request_parsing.phpt
new file mode 100644
index 00000000000..a39e562000f
--- /dev/null
+++ b/Zend/tests/oom_request_parsing.phpt
@@ -0,0 +1,130 @@
+--TEST--
+Request parsing doesn't reach hard limit
+--SKIPIF--
+<?php
+if (!getenv('TEST_PHP_CGI_EXECUTABLE')) {
+    die("skip php-cgi not available");
+}
+if (getenv('SKIP_ASAN')) {
+    die("skip ASan-instrumented realloc() is slow");
+}
+?>
+--FILE--
+<?php
+$base = __DIR__ . '/' . basename(__FILE__, '.php');
+
+$tiny = $base . '.target.php';
+file_put_contents($tiny, <<<'TARGET'
+<?php
+echo "no OOM\n";
+TARGET);
+
+$big = $base . '.big.target.php';
+file_put_contents($big, "<?php\n" . str_repeat("\$x = 1 + 2;\n", 60000) . "echo \"no OOM\\n\";\n");
+
+/* The body goes through a file rather than a pipe: some of these are several
+ * megabytes, and the child stops reading as soon as it reports the error. */
+function oom_post(string $body, string $script, array $ini): void
+{
+    $bodyFile = tempnam(sys_get_temp_dir(), 'oom_request_parsing');
+    file_put_contents($bodyFile, $body);
+
+    $cmd = [getenv('TEST_PHP_CGI_EXECUTABLE'), '-n'];
+    foreach ($ini as $entry) {
+        $cmd[] = '-d';
+        $cmd[] = $entry;
+    }
+
+    $p = proc_open(
+        $cmd,
+        [['file', $bodyFile, 'r'], STDOUT, STDERR],
+        $pipes,
+        null,
+        [
+            'REQUEST_METHOD'  => 'POST',
+            'CONTENT_TYPE'    => 'application/x-www-form-urlencoded',
+            'CONTENT_LENGTH'  => (string) strlen($body),
+            'SCRIPT_FILENAME' => $script,
+            'REDIRECT_STATUS' => '1',
+        ]
+    );
+    proc_close($p);
+    unlink($bodyFile);
+}
+
+/* Deeply nested variable names, at the default max_input_vars and
+ * max_input_nesting_level. */
+oom_post(
+    implode('&', array_fill(0, 1000, 'a[]' . str_repeat('[b]', 63) . '=c')),
+    $tiny,
+    ['memory_limit=8M', 'html_errors=0']
+);
+
+$ini = ['memory_limit=8M', 'post_max_size=1G', 'max_input_vars=10000000', 'html_errors=0'];
+
+/* One value with no '&' in it: nothing consumes it, so add_post_vars() never
+ * gets a complete variable to check and only the read loop can notice. */
+$oneValue = 'a=' . str_repeat('x', 6 * 1024 * 1024);
+
+/* Many small variables: the limit is reached while registering them. */
+$manyVars = '';
+for ($i = 0; $i < 200000; $i++) {
+    $manyVars .= ($i ? '&' : '') . "k$i=v$i";
+}
+
+/* A big value followed by many small ones: add_post_var() copies the value
+ * once with estrndup() and again when registering it, and each half fits even
+ * though the pair doesn't. */
+$mixed = 'a=' . str_repeat('x', 3 * 1024 * 1024);
+for ($i = 0; $i < 100000; $i++) {
+    $mixed .= "&k$i=v$i";
+}
+
+foreach ([$oneValue, $manyVars, $mixed] as $body) {
+    foreach ([$tiny, $big] as $script) {
+        oom_post($body, $script, $ini);
+    }
+}
+?>
+--CLEAN--
+<?php
+$base = __DIR__ . '/' . basename(__FILE__, '.clean.php');
+@unlink($base . '.target.php');
+@unlink($base . '.big.target.php');
+?>
+--EXPECTF--
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/Zend/tests/oom_soft_limit_exceeded.phpt b/Zend/tests/oom_soft_limit_exceeded.phpt
new file mode 100644
index 00000000000..ce99a34ec23
--- /dev/null
+++ b/Zend/tests/oom_soft_limit_exceeded.phpt
@@ -0,0 +1,19 @@
+--TEST--
+OOM: Soft limit exceeded
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+function f() {
+    $a = [];
+    while (true) {
+        $a[] = str_repeat('a', 1024*1024);
+    }
+}
+
+f();
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes %s (tried to allocate %d bytes) in %s on line %d
diff --git a/Zend/tests/oom_soft_limit_exceeded_fcall.phpt b/Zend/tests/oom_soft_limit_exceeded_fcall.phpt
new file mode 100644
index 00000000000..8d6d1fd8e0b
--- /dev/null
+++ b/Zend/tests/oom_soft_limit_exceeded_fcall.phpt
@@ -0,0 +1,19 @@
+--TEST--
+OOM: Soft limit exceeded in FCALL
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+function f() {
+    $a = [];
+    while (true) {
+        array_push($a, str_repeat('a', 1024*1024));
+    }
+}
+
+f();
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes %s (tried to allocate %d bytes) in %s on line %d
diff --git a/Zend/tests/oom_soft_limit_in_callback.phpt b/Zend/tests/oom_soft_limit_in_callback.phpt
new file mode 100644
index 00000000000..ccc4a9c058f
--- /dev/null
+++ b/Zend/tests/oom_soft_limit_in_callback.phpt
@@ -0,0 +1,27 @@
+--TEST--
+OOM: Soft limit exceeded in library callback
+--EXTENSIONS--
+zend_test
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+function f() {
+    zend_test_emalloc(10*1024*1024);
+}
+
+function shutdown_function() {
+    echo "Executed\n";
+}
+
+register_shutdown_function('shutdown_function');
+
+zend_test_callback(function () {
+    f();
+});
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in %s on line 4
+Executed
diff --git a/Zend/tests/oom_str_repeat.phpt b/Zend/tests/oom_str_repeat.phpt
new file mode 100644
index 00000000000..d329e13177d
--- /dev/null
+++ b/Zend/tests/oom_str_repeat.phpt
@@ -0,0 +1,12 @@
+--TEST--
+OOM str_repeat()
+--INI--
+memory_limit=100M
+--FILE--
+<?php
+
+var_dump(str_repeat('a', 300<<20));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 104857600 bytes exhausted %S(tried to allocate %d bytes) in %s on line 3
diff --git a/Zend/tests/oom_unserialize_001.phpt b/Zend/tests/oom_unserialize_001.phpt
new file mode 100644
index 00000000000..877dbceded1
--- /dev/null
+++ b/Zend/tests/oom_unserialize_001.phpt
@@ -0,0 +1,21 @@
+--TEST--
+OOM unserialize() array
+--INI--
+memory_limit=200M
+--FILE--
+<?php
+
+$chunks = [];
+$chunksize = 128;
+for ($i = 0; $i < 2*1024*1024; $i++) {
+    $chunkIdx = (int)($i / $chunksize);
+    $chunks[$chunkIdx] = ($chunks[$chunkIdx] ?? '') . sprintf('i:%d;i:%d;', $i, $i);
+}
+$payload = 'a:'.$i.':{'.implode('', $chunks).'}';
+
+ini_set('memory_limit', '100m');
+var_dump(gettype(unserialize($payload)));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 104857600 bytes exhausted %S(tried to allocate %d bytes) in %s on line 12
diff --git a/Zend/tests/oom_unserialize_002.phpt b/Zend/tests/oom_unserialize_002.phpt
new file mode 100644
index 00000000000..559ec731b67
--- /dev/null
+++ b/Zend/tests/oom_unserialize_002.phpt
@@ -0,0 +1,22 @@
+--TEST--
+OOM unserialize() array of strings
+--INI--
+memory_limit=200M
+--FILE--
+<?php
+
+$chunks = [];
+$chunksize = 128;
+for ($i = 0; $i < 1024*1024; $i++) {
+    $chunkIdx = (int)($i / $chunksize);
+    $str = base_convert($i, 10, 36);
+    $chunks[$chunkIdx] = ($chunks[$chunkIdx] ?? '') . sprintf('i:%d;s:%d:"%s";', $i, strlen($str), $str);
+}
+$payload = 'a:'.$i.':{'.implode('', $chunks).'}';
+
+ini_set('memory_limit', '100m');
+var_dump(gettype(unserialize($payload)));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 104857600 bytes exhausted %S(tried to allocate %d bytes) in %s on line 13
diff --git a/Zend/tests/oom_unserialize_003.phpt b/Zend/tests/oom_unserialize_003.phpt
new file mode 100644
index 00000000000..af627b132fc
--- /dev/null
+++ b/Zend/tests/oom_unserialize_003.phpt
@@ -0,0 +1,22 @@
+--TEST--
+OOM unserialize() array of string=>int
+--INI--
+memory_limit=200M
+--FILE--
+<?php
+
+$chunks = [];
+$chunksize = 128;
+for ($i = 0; $i < 1024*1024; $i++) {
+    $chunkIdx = (int)($i / $chunksize);
+    $str = base_convert($i, 10, 36);
+    $chunks[$chunkIdx] = ($chunks[$chunkIdx] ?? '') . sprintf('s:%d:"%s";i:%d;', strlen($str), $str, $i);
+}
+$payload = 'a:'.$i.':{'.implode('', $chunks).'}';
+
+ini_set('memory_limit', '100m');
+var_dump(gettype(unserialize($payload)));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 104857600 bytes exhausted %S(tried to allocate %d bytes) in %s on line 13
diff --git a/Zend/tests/oom_unserialize_004.phpt b/Zend/tests/oom_unserialize_004.phpt
new file mode 100644
index 00000000000..aff1d2a171a
--- /dev/null
+++ b/Zend/tests/oom_unserialize_004.phpt
@@ -0,0 +1,23 @@
+--TEST--
+OOM unserialize() array of old strings
+--INI--
+memory_limit=200M
+error_reporting=E_ALL&~E_DEPRECATED
+--FILE--
+<?php
+
+$chunks = [];
+$chunksize = 128;
+for ($i = 0; $i < 1024*1024; $i++) {
+    $chunkIdx = (int)($i / $chunksize);
+    $str = base_convert($i, 10, 36);
+    $chunks[$chunkIdx] = ($chunks[$chunkIdx] ?? '') . sprintf('i:%d;S:%d:"%s";', $i, strlen($str), $str);
+}
+$payload = 'a:'.$i.':{'.implode('', $chunks).'}';
+
+ini_set('memory_limit', '100m');
+var_dump(gettype(unserialize($payload)));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 104857600 bytes exhausted %S(tried to allocate %d bytes) in %s on line 13
diff --git a/Zend/tests/oom_unserialize_005.phpt b/Zend/tests/oom_unserialize_005.phpt
new file mode 100644
index 00000000000..3e821f0e29a
--- /dev/null
+++ b/Zend/tests/oom_unserialize_005.phpt
@@ -0,0 +1,23 @@
+--TEST--
+OOM unserialize() object
+--INI--
+memory_limit=200M
+error_reporting=E_ALL&~E_DEPRECATED
+--FILE--
+<?php
+
+$chunks = [];
+$chunksize = 128;
+for ($i = 0; $i < 1024*1024; $i++) {
+    $chunkIdx = (int)($i / $chunksize);
+    $prop = sprintf('p%d', $i);
+    $chunks[$chunkIdx] = ($chunks[$chunkIdx] ?? '') . sprintf('s:%d:"%s";i:%d;', strlen($prop), $prop, $i);
+}
+$payload = 'O:8:"stdClass":'.$i.':{'.implode('', $chunks).'}';
+
+ini_set('memory_limit', '100m');
+var_dump(gettype(unserialize($payload)));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 104857600 bytes exhausted %S(tried to allocate %d bytes) in %s on line 13
diff --git a/Zend/tests/oom_unserialize_006.phpt b/Zend/tests/oom_unserialize_006.phpt
new file mode 100644
index 00000000000..41d0e6edbd1
--- /dev/null
+++ b/Zend/tests/oom_unserialize_006.phpt
@@ -0,0 +1,22 @@
+--TEST--
+OOM unserialize() object of int=>int
+--INI--
+memory_limit=200M
+error_reporting=E_ALL&~E_DEPRECATED
+--FILE--
+<?php
+
+$chunks = [];
+$chunksize = 128;
+for ($i = 0; $i < 1024*1024; $i++) {
+    $chunkIdx = (int)($i / $chunksize);
+    $chunks[$chunkIdx] = ($chunks[$chunkIdx] ?? '') . sprintf('i:%d;i:%d;', $i, $i);
+}
+$payload = 'O:8:"stdClass":'.$i.':{'.implode('', $chunks).'}';
+
+ini_set('memory_limit', '64m');
+var_dump(gettype(unserialize($payload)));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 67108864 bytes exhausted %S(tried to allocate %d bytes) in %s on line 12
diff --git a/Zend/tests/oom_unserialize_007.phpt b/Zend/tests/oom_unserialize_007.phpt
new file mode 100644
index 00000000000..68db3da50cf
--- /dev/null
+++ b/Zend/tests/oom_unserialize_007.phpt
@@ -0,0 +1,17 @@
+--TEST--
+OOM unserialize() classname
+--INI--
+memory_limit=200M
+error_reporting=E_ALL&~E_DEPRECATED
+--FILE--
+<?php
+
+$classname = str_repeat('a', 32*1024*1024);
+$payload = 'O:'.strlen($classname).':"'.$classname.'":0:{}';
+
+ini_set('memory_limit', memory_get_usage() + 20*1024*1024);
+var_dump(gettype(unserialize($payload)));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line 7
diff --git a/Zend/tests/oom_unserialize_008.phpt b/Zend/tests/oom_unserialize_008.phpt
new file mode 100644
index 00000000000..946223bdf53
--- /dev/null
+++ b/Zend/tests/oom_unserialize_008.phpt
@@ -0,0 +1,19 @@
+--TEST--
+OOM unserialize() classname (2)
+--INI--
+memory_limit=200M
+error_reporting=E_ALL&~E_DEPRECATED
+--FILE--
+<?php
+
+// Use a classname with uppercase chars, so a lowercase copy is needed. We OOM
+// while creating the copy.
+$classname = str_repeat('A', 32*1024*1024);
+$payload = 'O:'.strlen($classname).':"'.$classname.'":0:{}';
+
+ini_set('memory_limit', memory_get_usage() + 40*1024*1024);
+var_dump(gettype(unserialize($payload)));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line 9
diff --git a/Zend/tests/oom_unserialize_009.phpt b/Zend/tests/oom_unserialize_009.phpt
new file mode 100644
index 00000000000..86001175af5
--- /dev/null
+++ b/Zend/tests/oom_unserialize_009.phpt
@@ -0,0 +1,26 @@
+--TEST--
+OOM unserialize() Serializable
+--INI--
+memory_limit=200M
+error_reporting=E_ALL&~E_DEPRECATED
+--FILE--
+<?php
+
+class C implements Serializable
+{
+    public $data;
+    public function serialize(): ?string {
+        return 'hello';
+    }
+    public function unserialize(string $data): void {
+        str_repeat('a', 200<<20);
+    }
+}
+
+$payload = 'C:1:"C":5:{hello}';
+
+var_dump(gettype(unserialize($payload)));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line 10
diff --git a/Zend/tests/oom_unserialize_011.phpt b/Zend/tests/oom_unserialize_011.phpt
new file mode 100644
index 00000000000..2710baba0d7
--- /dev/null
+++ b/Zend/tests/oom_unserialize_011.phpt
@@ -0,0 +1,22 @@
+--TEST--
+OOM unserialize() array of objects
+--INI--
+memory_limit=200M
+--FILE--
+<?php
+
+$chunks = [];
+$chunksize = 128;
+for ($i = 0; $i < 512*1024; $i++) {
+    $chunkIdx = (int)($i / $chunksize);
+    $str = base_convert($i, 10, 36);
+    $chunks[$chunkIdx] = ($chunks[$chunkIdx] ?? '') . sprintf('i:%d;O:8:"stdClass":0:{}', $i);
+}
+$payload = 'a:'.$i.':{'.implode('', $chunks).'}';
+
+ini_set('memory_limit', '64m');
+var_dump(gettype(unserialize($payload)));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 67108864 bytes exhausted %S(tried to allocate %d bytes) in %s on line 13
diff --git a/Zend/tests/oom_unserialize_012.phpt b/Zend/tests/oom_unserialize_012.phpt
new file mode 100644
index 00000000000..8f4495ce8a9
--- /dev/null
+++ b/Zend/tests/oom_unserialize_012.phpt
@@ -0,0 +1,23 @@
+--TEST--
+OOM unserialize() object fextend
+--INI--
+memory_limit=200M
+--FILE--
+<?php
+
+$chunks = [];
+$chunksize = 128;
+for ($i = 0; $i < 2*1024*1024; $i++) {
+    $chunkIdx = (int)($i / $chunksize);
+    $prop = sprintf('p%d', $i);
+    $chunks[$chunkIdx] = ($chunks[$chunkIdx] ?? '') . sprintf('s:%d:"%s";i:%d;', strlen($prop), $prop, $i);
+}
+$payload = 'O:8:"stdClass":'.$i.':{'.implode('', $chunks).'}';
+$chunks = null;
+
+ini_set('memory_limit', '100m');
+var_dump(gettype(unserialize($payload)));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 104857600 bytes exhausted %S(tried to allocate %d bytes) in %s on line 14
diff --git a/Zend/tests/oom_unserialize_013.phpt b/Zend/tests/oom_unserialize_013.phpt
new file mode 100644
index 00000000000..de73ba465b2
--- /dev/null
+++ b/Zend/tests/oom_unserialize_013.phpt
@@ -0,0 +1,28 @@
+--TEST--
+OOM unserialize() object __wakeup
+--INI--
+memory_limit=100M
+--FILE--
+<?php
+
+class C {
+    public function __wakeup() {
+        str_repeat('a', 200*1024*1024);
+    }
+}
+
+class D {
+    public function __wakeup() {
+        echo "Not executed";
+        // Would hit hard limit
+        zend_test_emalloc(200*1024*1024);
+    }
+}
+
+$payload = 'a:2:{i:0;O:1:"C":0:{}i:1;O:1:"D":0:{}}';
+
+var_dump(gettype(unserialize($payload)));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 104857600 bytes exhausted %S(tried to allocate %d bytes) in %s on line 5
diff --git a/Zend/tests/oom_unserialize_014.phpt b/Zend/tests/oom_unserialize_014.phpt
new file mode 100644
index 00000000000..fea87744437
--- /dev/null
+++ b/Zend/tests/oom_unserialize_014.phpt
@@ -0,0 +1,28 @@
+--TEST--
+OOM unserialize() object __unserialize (2)
+--INI--
+memory_limit=200M
+--FILE--
+<?php
+
+class C {
+    public function __unserialize($data) {
+    }
+}
+
+$chunks = [];
+$chunksize = 128;
+for ($i = 0; $i < 1024*1024; $i++) {
+    $chunkIdx = (int)($i / $chunksize);
+    $prop = sprintf('p%d', $i);
+    $chunks[$chunkIdx] = ($chunks[$chunkIdx] ?? '') . sprintf('s:%d:"%s";i:%d;', strlen($prop), $prop, $i);
+}
+$payload = 'O:1:"C":'.$i.':{'.implode('', $chunks).'}';
+$chunks = null;
+
+ini_set('memory_limit', '100m');
+var_dump(gettype(unserialize($payload)));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 104857600 bytes exhausted %S(tried to allocate %d bytes) in %s on line 19
diff --git a/Zend/tests/oom_unserialize_015.phpt b/Zend/tests/oom_unserialize_015.phpt
new file mode 100644
index 00000000000..b70445e7b4a
--- /dev/null
+++ b/Zend/tests/oom_unserialize_015.phpt
@@ -0,0 +1,20 @@
+--TEST--
+OOM unserialize() object __unserialize (2)
+--INI--
+memory_limit=100M
+--FILE--
+<?php
+
+class C {
+    public function __unserialize($data) {
+        str_repeat('a', 200<<20);
+    }
+}
+
+$payload = 'O:1:"C":0:{}';
+
+var_dump(gettype(unserialize($payload)));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 104857600 bytes exhausted %S(tried to allocate %d bytes) in %s on line 5
diff --git a/Zend/tests/oom_unserialize_016.phpt b/Zend/tests/oom_unserialize_016.phpt
new file mode 100644
index 00000000000..141b58109b8
--- /dev/null
+++ b/Zend/tests/oom_unserialize_016.phpt
@@ -0,0 +1,33 @@
+--TEST--
+OOM unserialize() cleans up BG(unserialize) after a delayed OOM
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+
+register_shutdown_function(function () {
+    /* Runs after the fatal error. If the var_hash of the failed call were
+     * still installed, this call would nest into it and R:1 would resolve
+     * into that call's table instead of this array. */
+    var_dump(unserialize('a:1:{i:0;R:1;}'));
+});
+
+/* 40000 one-element arrays: ~760 KB serialized, ~10 MB unserialized. */
+$n = 40000;
+$ser = 'a:' . $n . ':{';
+for ($i = 0; $i < $n; $i++) {
+    $ser .= 'i:' . $i . ';a:1:{i:0;i:0;}';
+}
+$ser .= '}';
+
+unserialize($ser);
+
+echo "not reached\n";
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+array(1) {
+  [0]=>
+  *RECURSION*
+}
diff --git a/Zend/tests/singleton_array.phpt b/Zend/tests/singleton_array.phpt
new file mode 100644
index 00000000000..44f44a5f11b
--- /dev/null
+++ b/Zend/tests/singleton_array.phpt
@@ -0,0 +1,140 @@
+--TEST--
+array_init_singleton()
+--EXTENSIONS--
+zend_test
+--FILE--
+<?php
+
+$a = zend_new_singleton_array();
+var_dump($a, count($a));
+
+/* Stays packed while growing: 1 -> 2 -> 4 -> 8 -> 16 */
+$a = zend_new_singleton_array();
+for ($i = 0; $i < 10; $i++) {
+    $a[] = $i;
+}
+var_dump($a === range(0, 9));
+
+/* First insert is a string key: goes straight to a mixed table of size 1,
+ * whose hash area is only 2 slots wide. */
+$a = zend_new_singleton_array();
+$a['foo'] = 1;
+$a['bar'] = 2;
+$a['baz'] = 3;
+var_dump($a);
+
+/* Packed -> hash conversion of a size 1 table. */
+$a = zend_new_singleton_array();
+$a[] = 'first';
+$a['key'] = 'second';
+var_dump($a);
+
+/* An out of range integer key also skips the packed representation. */
+$a = zend_new_singleton_array();
+$a[100] = 'hundred';
+$a[0] = 'zero';
+var_dump($a);
+
+/* Deletion, rehashing and reuse. */
+$a = zend_new_singleton_array();
+$a['a'] = 1;
+$a['b'] = 2;
+unset($a['a']);
+$a['c'] = 3;
+unset($a['b']);
+$a['d'] = 4;
+var_dump($a);
+
+/* Separation (zend_array_dup) of a size 1 table. */
+$a = zend_new_singleton_array();
+$a['x'] = 1;
+$b = $a;
+$b['y'] = 2;
+var_dump($a, $b);
+
+/* Sorting and renumbering. */
+$a = zend_new_singleton_array();
+$a['c'] = 3;
+$a['a'] = 1;
+$a['b'] = 2;
+asort($a);
+var_dump($a);
+sort($a);
+var_dump($a);
+
+/* Round-trips. */
+$a = zend_new_singleton_array();
+$a['only'] = 'one';
+var_dump(unserialize(serialize($a)) === $a);
+var_dump(json_decode(json_encode($a), true) === $a);
+
+/* Iteration. */
+$a = zend_new_singleton_array();
+$a[] = 'a';
+$a[] = 'b';
+foreach ($a as $k => $v) {
+    echo "$k => $v\n";
+}
+
+?>
+--EXPECT--
+array(0) {
+}
+int(0)
+bool(true)
+array(3) {
+  ["foo"]=>
+  int(1)
+  ["bar"]=>
+  int(2)
+  ["baz"]=>
+  int(3)
+}
+array(2) {
+  [0]=>
+  string(5) "first"
+  ["key"]=>
+  string(6) "second"
+}
+array(2) {
+  [100]=>
+  string(7) "hundred"
+  [0]=>
+  string(4) "zero"
+}
+array(2) {
+  ["c"]=>
+  int(3)
+  ["d"]=>
+  int(4)
+}
+array(1) {
+  ["x"]=>
+  int(1)
+}
+array(2) {
+  ["x"]=>
+  int(1)
+  ["y"]=>
+  int(2)
+}
+array(3) {
+  ["a"]=>
+  int(1)
+  ["b"]=>
+  int(2)
+  ["c"]=>
+  int(3)
+}
+array(3) {
+  [0]=>
+  int(1)
+  [1]=>
+  int(2)
+  [2]=>
+  int(3)
+}
+bool(true)
+bool(true)
+0 => a
+1 => b
diff --git a/Zend/zend.c b/Zend/zend.c
index d14b90a8d9c..a57d271d326 100644
--- a/Zend/zend.c
+++ b/Zend/zend.c
@@ -737,6 +737,8 @@ static void compiler_globals_ctor(zend_compiler_globals *compiler_globals) /* {{
 		compiler_globals->map_ptr_base = ZEND_MAP_PTR_BIASED_BASE(base);
 		memset(base, 0, compiler_globals->map_ptr_last * sizeof(void*));
 	}
+
+	compiler_globals->interned_strings_fallible = false;
 }
 /* }}} */

@@ -809,6 +811,7 @@ static void executor_globals_ctor(zend_executor_globals *executor_globals) /* {{
 	executor_globals->pid = 0;
 	executor_globals->oldact = (struct sigaction){0};
 #endif
+	memset(&executor_globals->delayed_oom, 0, sizeof(executor_globals->delayed_oom));
 }
 /* }}} */

@@ -1307,6 +1310,8 @@ ZEND_API void zend_deactivate(void) /* {{{ */
 		zend_map_ptr_reset();
 	}

+	zend_mm_oom_info_dtor(&EG(delayed_oom));
+
 #if GC_BENCH
 	fprintf(stderr, "GC Statistics\n");
 	fprintf(stderr, "-------------\n");
@@ -1396,7 +1401,7 @@ ZEND_API ZEND_COLD void zend_error_zstr_at(
 	}

 	/* Report about uncaught exception in case of fatal errors */
-	if (EG(exception)) {
+	if (EG(exception) && EXPECTED(EG(delayed_oom).state != ZEND_OOM_STATE_HANDLING)) {
 		zend_execute_data *ex;
 		const zend_op *opline;

@@ -1842,6 +1847,9 @@ ZEND_API zend_result zend_execute_scripts(int type, zval *retval, int file_count
 		}
 		if (op_array) {
 			zend_execute(op_array, retval);
+			if (ZEND_HAS_OOM()) {
+				zend_handle_delayed_oom();
+			}
 			zend_exception_restore();
 			if (UNEXPECTED(EG(exception))) {
 				if (Z_TYPE(EG(user_exception_handler)) != IS_UNDEF) {
diff --git a/Zend/zend_API.c b/Zend/zend_API.c
index 6aeb2d653fa..98c44d60dee 100644
--- a/Zend/zend_API.c
+++ b/Zend/zend_API.c
@@ -1726,6 +1726,18 @@ ZEND_API void object_init(zval *arg) /* {{{ */
 }
 /* }}} */

+ZEND_API bool object_finit(zval *arg)
+{
+	zend_object *obj = zend_objects_fnew(zend_standard_class_def);
+
+	if (UNEXPECTED(obj == NULL)) {
+		ZVAL_NULL(arg);
+		return false;
+	}
+	ZVAL_OBJ(arg, obj);
+	return true;
+}
+
 ZEND_API void add_assoc_long_ex(zval *arg, const char *key, size_t key_len, zend_long n) /* {{{ */
 {
 	zval tmp;
diff --git a/Zend/zend_API.h b/Zend/zend_API.h
index b48030327ce..200f129326b 100644
--- a/Zend/zend_API.h
+++ b/Zend/zend_API.h
@@ -523,7 +523,45 @@ ZEND_API const char *zend_get_type_by_const(int type);

 #define array_init(arg)				ZVAL_ARR((arg), zend_new_array(0))
 #define array_init_size(arg, size)	ZVAL_ARR((arg), zend_new_array(size))
+#define array_init_singleton(arg)	ZVAL_ARR((arg), zend_new_array_singleton())
+
+/* Fallible array_init()/object_init(): on failure return false and leave arg as
+ * IS_NULL, so arg stays safe to destroy. */
+static zend_always_inline bool _array_finit(zval *arg, size_t size)
+{
+	zend_array *ht = zend_fnew_array(size);
+	if (UNEXPECTED(ht == NULL)) {
+		ZVAL_NULL(arg);
+		return false;
+	}
+	ZVAL_ARR(arg, ht);
+	return true;
+}
+
+static zend_always_inline bool _array_finit_ex(zval *arg, size_t size, bool packed)
+{
+	zend_array *ht = zend_fnew_array(size);
+	if (UNEXPECTED(ht == NULL)) {
+		ZVAL_NULL(arg);
+		return false;
+	}
+	if (UNEXPECTED(zend_hash_real_finit(ht, packed, /* fallible */ true) == FAILURE)) {
+		efree(ht);
+		ZVAL_NULL(arg);
+		return false;
+	}
+	ZVAL_ARR(arg, ht);
+	return true;
+}
+
+#define array_finit(arg)			_array_finit((arg), 0)
+#define array_finit_size(arg, size)	_array_finit((arg), (size))
+#define array_finit_size_ex(arg, size, packed)  _array_finit_ex((arg), (size), (packed))
+#define array_finit_size_packed(arg, size)	    _array_finit_ex((arg), (size), /* packed */ true)
+#define array_finit_size_mixed(arg, size)	    _array_finit_ex((arg), (size), /* packed */ false)
+
 ZEND_API void object_init(zval *arg);
+ZEND_API bool object_finit(zval *arg);
 ZEND_API zend_result object_init_ex(zval *arg, zend_class_entry *ce);
 ZEND_API zend_result object_and_properties_init(zval *arg, zend_class_entry *ce, HashTable *properties);
 ZEND_API void object_properties_init(zend_object *object, zend_class_entry *class_type);
@@ -846,11 +884,26 @@ static zend_always_inline bool zend_char_has_nul_byte(const char *s, size_t know
 		ZVAL_NEW_STR(z, zend_string_init(s, l, 0));		\
 	} while (0)

+/* Fallible variant of ZVAL_STRINGL() to be used when 's' is not managed by zend_alloc */
+static zend_always_inline bool ZVAL_FSTRINGL(zval *z, const char *s, size_t l) {
+	zend_string *str = zend_string_finit(s, l, /* persistent */ false, /* fallible */ true);
+	if (UNEXPECTED(!str)) {
+		ZVAL_INTERNED_STR(z, ZSTR_EMPTY_ALLOC());
+		return false;
+	}
+	ZVAL_NEW_STR(z, str);
+	return true;
+}
+
 #define ZVAL_STRING(z, s) do {					\
 		const char *_s = (s);					\
 		ZVAL_STRINGL(z, _s, strlen(_s));		\
 	} while (0)

+static zend_always_inline bool ZVAL_FSTRING(zval *z, const char *s) {
+	return ZVAL_FSTRINGL(z, s, strlen(s));
+}
+
 #define ZVAL_EMPTY_STRING(z) do {				\
 		ZVAL_INTERNED_STR(z, ZSTR_EMPTY_ALLOC());		\
 	} while (0)
@@ -911,6 +964,8 @@ static zend_always_inline bool zend_char_has_nul_byte(const char *s, size_t know
 #define RETVAL_STRINGL(s, l)			ZVAL_STRINGL(return_value, s, l)
 #define RETVAL_STRING_FAST(s)			ZVAL_STRING_FAST(return_value, s)
 #define RETVAL_STRINGL_FAST(s, l)		ZVAL_STRINGL_FAST(return_value, s, l)
+#define RETVAL_FSTRING(s)				ZVAL_FSTRING(return_value, s)
+#define RETVAL_FSTRINGL(s, l)			ZVAL_FSTRINGL(return_value, s, l)
 #define RETVAL_EMPTY_STRING()			ZVAL_EMPTY_STRING(return_value)
 #define RETVAL_CHAR(c)		            ZVAL_CHAR(return_value, c)
 #define RETVAL_RES(r)					ZVAL_RES(return_value, r)
@@ -937,6 +992,8 @@ static zend_always_inline bool zend_char_has_nul_byte(const char *s, size_t know
 #define RETURN_STRINGL(s, l) 			do { RETVAL_STRINGL(s, l); return; } while (0)
 #define RETURN_STRING_FAST(s) 			do { RETVAL_STRING_FAST(s); return; } while (0)
 #define RETURN_STRINGL_FAST(s, l)		do { RETVAL_STRINGL_FAST(s, l); return; } while (0)
+#define RETURN_FSTRING(s) 			    do { RETVAL_FSTRING(s); return; } while (0)
+#define RETURN_FSTRINGL(s, l)			do { RETVAL_FSTRINGL(s, l); return; } while (0)
 #define RETURN_EMPTY_STRING() 			do { RETVAL_EMPTY_STRING(); return; } while (0)
 #define RETURN_CHAR(c)		            do { RETVAL_CHAR(c); return; } while (0)
 #define RETURN_RES(r)					do { RETVAL_RES(r); return; } while (0)
@@ -951,6 +1008,7 @@ static zend_always_inline bool zend_char_has_nul_byte(const char *s, size_t know
 #define RETURN_FALSE					do { RETVAL_FALSE; return; } while (0)
 #define RETURN_TRUE						do { RETVAL_TRUE; return; } while (0)
 #define RETURN_THROWS()					do { ZEND_ASSERT(EG(exception)); (void) return_value; return; } while (0)
+#define RETURN_OOM()                    do { ZEND_ASSERT(ZEND_HAS_OOM()); (void) return_value; return; } while (0)

 #define HASH_OF(p) (Z_TYPE_P(p)==IS_ARRAY ? Z_ARRVAL_P(p) : ((Z_TYPE_P(p)==IS_OBJECT ? Z_OBJ_HT_P(p)->get_properties(Z_OBJ_P(p)) : NULL)))
 #define ZVAL_IS_NULL(z) (Z_TYPE_P(z) == IS_NULL)
diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c
index 06e9d613432..1ec7d7ca423 100644
--- a/Zend/zend_alloc.c
+++ b/Zend/zend_alloc.c
@@ -53,11 +53,14 @@

 #include "zend.h"
 #include "zend_alloc.h"
+#include "zend_API.h"
 #include "zend_globals.h"
 #include "zend_operators.h"
 #include "zend_multiply.h"
 #include "zend_bitset.h"
 #include "zend_mmap.h"
+#include "zend_string.h"
+#include "zend_exceptions.h"
 #include <signal.h>

 #ifdef HAVE_UNISTD_H
@@ -191,6 +194,10 @@ typedef zend_mm_bitset zend_mm_page_map[ZEND_MM_PAGE_MAP_LEN];     /* 64B */

 #define ZEND_MM_BINS 30

+#if ZEND_MM_LIMIT
+#   define ZEND_MM_LIMIT_MAX           ((size_t)Z_L(-1) >> 1)
+#endif
+
 typedef struct  _zend_mm_page      zend_mm_page;
 typedef struct  _zend_mm_bin       zend_mm_bin;
 typedef struct  _zend_mm_free_slot zend_mm_free_slot;
@@ -198,6 +205,7 @@ typedef struct  _zend_mm_chunk     zend_mm_chunk;
 typedef struct  _zend_mm_huge_list zend_mm_huge_list;

 static bool zend_mm_use_huge_pages = false;
+static bool zend_mm_max_allocs_reached = false;

 /*
  * Memory is retrieved from OS by chunks of fixed size 2MB.
@@ -251,8 +259,11 @@ struct _zend_mm_heap {
 #endif
 #if ZEND_MM_LIMIT
 	size_t             limit;                   /* memory limit */
+	size_t             hard_limit;
+	zend_long          hard_limit_percent;
 	int                overflow;                /* memory overflow flag */
 #endif
+	bool               fallible;

 	zend_mm_huge_list *huge_list;               /* list of huge allocated blocks */

@@ -278,6 +289,7 @@ struct _zend_mm_heap {
 		} debug;
 	} custom_heap;
 	HashTable *tracked_allocs;
+	uint64_t max_allocs;
 #endif
 };

@@ -368,31 +380,160 @@ static ZEND_COLD ZEND_NORETURN void zend_mm_panic(const char *message)
 	abort();
 }

-static ZEND_COLD ZEND_NORETURN void zend_mm_safe_error(zend_mm_heap *heap,
-	const char *format,
-	size_t limit,
-#if ZEND_DEBUG
-	const char *filename,
-	uint32_t lineno,
-#endif
-	size_t size)
+static ZEND_COLD ZEND_NORETURN void zend_out_of_memory(void);
+
+static ZEND_COLD void zend_mm_delayed_verror(const char *format, va_list args)
 {
+	if (ZEND_HAS_OOM()) {
+		return;
+	}

-	heap->overflow = 1;
-	zend_try {
-		zend_error_noreturn(E_ERROR,
-			format,
-			limit,
-#if ZEND_DEBUG
-			filename,
-			lineno,
-#endif
-			size);
-	} zend_catch {
-	}  zend_end_try();
-	heap->overflow = 0;
-	zend_bailout();
-	exit(1);
+	zend_string *script_filename = NULL;
+	uint32_t script_lineno = 0;
+
+	if (zend_is_executing()) {
+		script_filename = zend_get_executed_filename_ex();
+		script_lineno = zend_get_executed_lineno();
+	} else if (zend_is_compiling()) {
+		script_filename = zend_get_compiled_filename();
+		script_lineno = zend_get_compiled_lineno();
+	}
+
+	if (script_filename == NULL) {
+		script_filename = ZSTR_KNOWN(ZEND_STR_UNKNOWN_CAPITALIZED);
+		script_lineno = 0;
+	}
+
+	char buf[1024];
+	vsnprintf(buf, sizeof(buf), format, args);
+
+	EG(delayed_oom).message = zend_string_init(buf, strlen(buf), /* persistent */ true);
+	GC_MAKE_PERSISTENT_LOCAL(EG(delayed_oom).message);
+	zend_string_addref(script_filename);
+	EG(delayed_oom).filename = script_filename;
+	EG(delayed_oom).lineno = script_lineno;
+	EG(delayed_oom).state = ZEND_OOM_STATE_PENDING;
+
+	zend_atomic_bool_store_ex(&EG(vm_interrupt), true);
+}
+
+ZEND_ATTRIBUTE_FORMAT(printf, 1, 2)
+static ZEND_COLD void zend_mm_delayed_error(const char *format, ...)
+{
+	va_list args;
+	va_start(args, format);
+	zend_mm_delayed_verror(format, args);
+	va_end(args);
+}
+
+ZEND_ATTRIBUTE_FORMAT(printf, 2, 3)
+ZEND_API ZEND_COLD void zend_mm_delayed_error_or_panic(bool delay, const char *format, ...)
+{
+	va_list args;
+	va_start(args, format);
+
+	if (delay) {
+		zend_mm_delayed_verror(format, args);
+	} else {
+		char buf[1024];
+		vsnprintf(buf, sizeof(buf), format, args);
+		zend_mm_panic(buf);
+	}
+
+	va_end(args);
+}
+
+ZEND_COLD void zend_mm_oom_info_dtor(zend_oom_info *info)
+{
+	if (info->message) {
+		zend_string_release(info->message);
+		info->message = NULL;
+	}
+	if (info->filename) {
+		zend_string_release(info->filename);
+		info->filename = NULL;
+	}
+}
+
+#ifdef ZEND_MM_LIMIT
+static ZEND_COLD ZEND_NORETURN void zend_mm_hard_limit_error(zend_mm_heap *heap,
+		size_t add_size ZEND_FILE_LINE_DC)
+{
+	zend_string *script_filename = NULL;
+	uint32_t script_lineno = 0;
+
+	if (zend_is_executing()) {
+		script_filename = zend_get_executed_filename_ex();
+		script_lineno = zend_get_executed_lineno();
+	} else if (zend_is_compiling()) {
+		script_filename = zend_get_compiled_filename();
+		script_lineno = zend_get_compiled_lineno();
+	}
+
+	if (script_filename == NULL) {
+		script_filename = ZSTR_KNOWN(ZEND_STR_UNKNOWN_CAPITALIZED);
+		script_lineno = 0;
+	}
+
+	fprintf(stderr,
+# if ZEND_DEBUG
+			"Hard memory limit of %zu bytes exhausted at %s:%d (tried to allocate %zu bytes) in %s on line %d\n",
+# else
+			"Hard memory limit of %zu bytes exhausted (tried to allocate %zu bytes) in %s on line %d\n",
+# endif
+			heap->hard_limit
+			ZEND_FILE_LINE_RELAY_CC,
+			add_size,
+			ZSTR_VAL(script_filename),
+			script_lineno);
+
+	/* See http://support.microsoft.com/kb/190351 */
+# ifdef ZEND_WIN32
+	fflush(stderr);
+# endif
+
+	_exit(137);
+}
+#endif /* ZEND_MM_LIMIT */
+
+/* Returns FAILURE if allocation should fail (in case the heap is fallible).
+ * When the soft limit is to be exceeded, a delayed OOM error is queued.
+ * When the hard limit is to be exceeded, the function does not return.
+ */
+static zend_always_inline zend_result zend_mm_check_limit(zend_mm_heap *heap, size_t add_size, bool gc, bool *did_gc ZEND_FILE_LINE_DC)
+{
+#if ZEND_MM_LIMIT
+	/* Is the soft limit exceeded or going to be exceeded? */
+	if (UNEXPECTED(heap->limit < heap->real_size || add_size > heap->limit - heap->real_size)) {
+		if (gc && zend_mm_gc(heap)) {
+			if (did_gc) {
+				*did_gc = true;
+			}
+		}
+		ZEND_ASSERT(heap->hard_limit >= heap->limit && heap->hard_limit >= heap->real_size);
+		if (!heap->fallible && add_size > heap->hard_limit - heap->real_size) {
+			zend_mm_hard_limit_error(heap, add_size ZEND_FILE_LINE_RELAY_CC);
+		}
+		/* Check soft limit again as gc may have reduced memory usage */
+		if (!gc || (heap->limit < heap->real_size || add_size > heap->limit - heap->real_size)) {
+			if (!ZEND_HAS_OOM()) {
+				zend_mm_delayed_error(
+# if ZEND_DEBUG
+						"Allowed memory size of %zu bytes exhausted at %s:%d (tried to allocate %zu bytes)",
+# else
+						"Allowed memory size of %zu bytes exhausted (tried to allocate %zu bytes)",
+# endif
+						heap->limit
+						ZEND_FILE_LINE_RELAY_CC,
+						add_size);
+			}
+
+			return heap->fallible ? FAILURE : SUCCESS;
+		}
+	}
+#endif /* ZEND_MM_LIMIT */
+
+	return SUCCESS;
 }

 #ifdef _WIN32
@@ -1005,20 +1146,13 @@ static void *zend_mm_alloc_pages(zend_mm_heap *heap, uint32_t pages_count ZEND_F
 				chunk = heap->cached_chunks;
 				heap->cached_chunks = chunk->next;
 			} else {
-#if ZEND_MM_LIMIT
-				if (UNEXPECTED(ZEND_MM_CHUNK_SIZE > heap->limit - heap->real_size)) {
-					if (zend_mm_gc(heap)) {
-						goto get_chunk;
-					} else if (heap->overflow == 0) {
-#if ZEND_DEBUG
-						zend_mm_safe_error(heap, "Allowed memory size of %zu bytes exhausted at %s:%d (tried to allocate %zu bytes)", heap->limit, __zend_filename, __zend_lineno, size);
-#else
-						zend_mm_safe_error(heap, "Allowed memory size of %zu bytes exhausted (tried to allocate %zu bytes)", heap->limit, ZEND_MM_PAGE_SIZE * pages_count);
-#endif
-						return NULL;
-					}
+				bool did_gc = false;
+				if (UNEXPECTED(zend_mm_check_limit(heap, ZEND_MM_CHUNK_SIZE, /* gc */ true, &did_gc ZEND_FILE_LINE_RELAY_CC) == FAILURE)) {
+					return NULL;
+				} else if (did_gc) {
+					goto get_chunk;
 				}
-#endif
+
 				chunk = (zend_mm_chunk*)zend_mm_chunk_alloc(heap, ZEND_MM_CHUNK_SIZE, ZEND_MM_CHUNK_SIZE);
 				if (UNEXPECTED(chunk == NULL)) {
 					/* insufficient memory */
@@ -1026,13 +1160,7 @@ static void *zend_mm_alloc_pages(zend_mm_heap *heap, uint32_t pages_count ZEND_F
 					    (chunk = (zend_mm_chunk*)zend_mm_chunk_alloc(heap, ZEND_MM_CHUNK_SIZE, ZEND_MM_CHUNK_SIZE)) != NULL) {
 						/* pass */
 					} else {
-#if !ZEND_MM_LIMIT
-						zend_mm_safe_error(heap, "Out of memory");
-#elif ZEND_DEBUG
-						zend_mm_safe_error(heap, "Out of memory (allocated %zu bytes) at %s:%d (tried to allocate %zu bytes)", heap->real_size, __zend_filename, __zend_lineno, size);
-#else
-						zend_mm_safe_error(heap, "Out of memory (allocated %zu bytes) (tried to allocate %zu bytes)", heap->real_size, ZEND_MM_PAGE_SIZE * pages_count);
-#endif
+						zend_out_of_memory();
 						return NULL;
 					}
 				}
@@ -1090,6 +1218,9 @@ static zend_always_inline void *zend_mm_alloc_large_ex(zend_mm_heap *heap, size_
 #else
 	void *ptr = zend_mm_alloc_pages(heap, pages_count ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
 #endif
+	if (UNEXPECTED(ptr == NULL)) {
+		return NULL;
+	}
 #if ZEND_MM_STAT
 	do {
 		size_t size = heap->size + pages_count * ZEND_MM_PAGE_SIZE;
@@ -1255,6 +1386,11 @@ static zend_never_inline void *zend_mm_alloc_small_slow(zend_mm_heap *heap, uint
 #endif
 	if (UNEXPECTED(bin == NULL)) {
 		/* insufficient memory */
+#if ZEND_MM_STAT
+		/* heap->size was incremented by the caller. Decrement here to preserve
+		 * the tail call. */
+		heap->size -= bin_data_size[bin_num];
+#endif
 		return NULL;
 	}

@@ -1376,30 +1512,34 @@ static zend_always_inline void *zend_mm_alloc_heap(zend_mm_heap *heap, size_t si
 	size = MAX(size, 1);
 	size = ZEND_MM_ALIGNED_SIZE(size) + ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info));
 	if (UNEXPECTED(size < real_size)) {
-		zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (%zu + %zu)", ZEND_MM_ALIGNED_SIZE(real_size), ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info)));
+		zend_mm_delayed_error_or_panic(heap->fallible, "Possible integer overflow in memory allocation (%zu + %zu)", ZEND_MM_ALIGNED_SIZE(real_size), ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info)));
 		return NULL;
 	}
 #endif
 	if (EXPECTED(size <= ZEND_MM_MAX_SMALL_SIZE)) {
 		ptr = zend_mm_alloc_small(heap, ZEND_MM_SMALL_SIZE_TO_BIN(size) ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
 #if ZEND_DEBUG
-		dbg = zend_mm_get_debug_info(heap, ptr);
-		dbg->size = real_size;
-		dbg->filename = __zend_filename;
-		dbg->orig_filename = __zend_orig_filename;
-		dbg->lineno = __zend_lineno;
-		dbg->orig_lineno = __zend_orig_lineno;
+		if (EXPECTED(ptr != NULL)) {
+			dbg = zend_mm_get_debug_info(heap, ptr);
+			dbg->size = real_size;
+			dbg->filename = __zend_filename;
+			dbg->orig_filename = __zend_orig_filename;
+			dbg->lineno = __zend_lineno;
+			dbg->orig_lineno = __zend_orig_lineno;
+		}
 #endif
 		return ptr;
 	} else if (EXPECTED(size <= ZEND_MM_MAX_LARGE_SIZE)) {
 		ptr = zend_mm_alloc_large(heap, size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
 #if ZEND_DEBUG
-		dbg = zend_mm_get_debug_info(heap, ptr);
-		dbg->size = real_size;
-		dbg->filename = __zend_filename;
-		dbg->orig_filename = __zend_orig_filename;
-		dbg->lineno = __zend_lineno;
-		dbg->orig_lineno = __zend_orig_lineno;
+		if (EXPECTED(ptr != NULL)) {
+			dbg = zend_mm_get_debug_info(heap, ptr);
+			dbg->size = real_size;
+			dbg->filename = __zend_filename;
+			dbg->orig_filename = __zend_orig_filename;
+			dbg->lineno = __zend_lineno;
+			dbg->orig_lineno = __zend_orig_lineno;
+		}
 #endif
 		return ptr;
 	} else {
@@ -1472,10 +1612,16 @@ static zend_never_inline void *zend_mm_realloc_slow(zend_mm_heap *heap, void *pt
 		size_t orig_peak = heap->peak;
 #endif
 		ret = zend_mm_alloc_heap(heap, size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
-		memcpy(ret, ptr, copy_size);
-		zend_mm_free_heap(heap, ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
+		if (EXPECTED(ret != NULL)) {
+			memcpy(ret, ptr, copy_size);
+			zend_mm_free_heap(heap, ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
+		} else {
+			/* original block is left untouched */
+		}
 #if ZEND_MM_STAT
-		heap->peak = MAX(orig_peak, heap->size);
+		if (EXPECTED(ret != NULL)) {
+			heap->peak = MAX(orig_peak, heap->size);
+		}
 	} while (0);
 #endif
 	return ret;
@@ -1531,20 +1677,10 @@ static zend_never_inline void *zend_mm_realloc_huge(zend_mm_heap *heap, void *pt
 				return ptr;
 			}
 		} else /* if (new_size > old_size) */ {
-#if ZEND_MM_LIMIT
-			if (UNEXPECTED(new_size - old_size > heap->limit - heap->real_size)) {
-				if (zend_mm_gc(heap) && new_size - old_size <= heap->limit - heap->real_size) {
-					/* pass */
-				} else if (heap->overflow == 0) {
-#if ZEND_DEBUG
-					zend_mm_safe_error(heap, "Allowed memory size of %zu bytes exhausted at %s:%d (tried to allocate %zu bytes)", heap->limit, __zend_filename, __zend_lineno, size);
-#else
-					zend_mm_safe_error(heap, "Allowed memory size of %zu bytes exhausted (tried to allocate %zu bytes)", heap->limit, size);
-#endif
-					return NULL;
-				}
+			if (UNEXPECTED(zend_mm_check_limit(heap, new_size - old_size, /* gc */ true, NULL ZEND_FILE_LINE_RELAY_CC) == FAILURE)) {
+				return NULL;
 			}
-#endif
+
 			/* try to map tail right after this block */
 			if (zend_mm_chunk_extend(heap, ptr, old_size, new_size)) {
 #if ZEND_MM_STAT || ZEND_MM_LIMIT
@@ -1608,6 +1744,9 @@ static zend_always_inline void *zend_mm_realloc_heap(zend_mm_heap *heap, void *p
 					if (old_bin_num > 0 && size < bin_data_size[old_bin_num - 1]) {
 						/* truncation */
 						ret = zend_mm_alloc_small(heap, ZEND_MM_SMALL_SIZE_TO_BIN(size) ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
+						if (UNEXPECTED(ret == NULL)) {
+							goto failed;
+						}
 						copy_size = use_copy_size ? MIN(size, copy_size) : size;
 						memcpy(ret, ptr, copy_size);
 						zend_mm_free_small(heap, ptr, old_bin_num);
@@ -1623,6 +1762,9 @@ static zend_always_inline void *zend_mm_realloc_heap(zend_mm_heap *heap, void *p
 						size_t orig_peak = heap->peak;
 #endif
 						ret = zend_mm_alloc_small(heap, ZEND_MM_SMALL_SIZE_TO_BIN(size) ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
+						if (UNEXPECTED(ret == NULL)) {
+							goto failed;
+						}
 						copy_size = use_copy_size ? MIN(old_size, copy_size) : old_size;
 						memcpy(ret, ptr, copy_size);
 						zend_mm_free_small(heap, ptr, old_bin_num);
@@ -1644,6 +1786,10 @@ static zend_always_inline void *zend_mm_realloc_heap(zend_mm_heap *heap, void *p
 				dbg->orig_lineno = __zend_orig_lineno;
 #endif
 				return ret;
+
+failed:
+				/* original block is left untouched */
+				return NULL;
 			}  while (0);

 		} else /* if (info & ZEND_MM_IS_LARGE_RUN) */ {
@@ -1731,6 +1877,8 @@ static void zend_mm_add_huge_block(zend_mm_heap *heap, void *ptr, size_t size, s
 static void zend_mm_add_huge_block(zend_mm_heap *heap, void *ptr, size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
 #endif
 {
+	bool was_fallible = heap->fallible;
+	heap->fallible = false;
 	zend_mm_huge_list *list = (zend_mm_huge_list*)zend_mm_alloc_heap(heap, sizeof(zend_mm_huge_list) ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
 	list->ptr = ptr;
 	list->size = size;
@@ -1743,6 +1891,7 @@ static void zend_mm_add_huge_block(zend_mm_heap *heap, void *ptr, size_t size ZE
 	list->dbg.orig_lineno = __zend_orig_lineno;
 #endif
 	heap->huge_list = list;
+	heap->fallible = was_fallible;
 }

 static size_t zend_mm_del_huge_block(zend_mm_heap *heap, void *ptr ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
@@ -1820,23 +1969,14 @@ static void *zend_mm_alloc_huge(zend_mm_heap *heap, size_t size ZEND_FILE_LINE_D
 	void *ptr;

 	if (UNEXPECTED(new_size < size)) {
-		zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (%zu + %zu)", size, alignment);
+		zend_mm_delayed_error_or_panic(heap->fallible, "Possible integer overflow in memory allocation (%zu + %zu)", size, alignment);
+		return NULL;
 	}

-#if ZEND_MM_LIMIT
-	if (UNEXPECTED(new_size > heap->limit - heap->real_size)) {
-		if (zend_mm_gc(heap) && new_size <= heap->limit - heap->real_size) {
-			/* pass */
-		} else if (heap->overflow == 0) {
-#if ZEND_DEBUG
-			zend_mm_safe_error(heap, "Allowed memory size of %zu bytes exhausted at %s:%d (tried to allocate %zu bytes)", heap->limit, __zend_filename, __zend_lineno, size);
-#else
-			zend_mm_safe_error(heap, "Allowed memory size of %zu bytes exhausted (tried to allocate %zu bytes)", heap->limit, size);
-#endif
-			return NULL;
-		}
+	if (UNEXPECTED(zend_mm_check_limit(heap, new_size, /* gc */ true, NULL ZEND_FILE_LINE_RELAY_CC) == FAILURE)) {
+		return NULL;
 	}
-#endif
+
 	ptr = zend_mm_chunk_alloc(heap, new_size, ZEND_MM_CHUNK_SIZE);
 	if (UNEXPECTED(ptr == NULL)) {
 		/* insufficient memory */
@@ -1844,13 +1984,7 @@ static void *zend_mm_alloc_huge(zend_mm_heap *heap, size_t size ZEND_FILE_LINE_D
 		    (ptr = zend_mm_chunk_alloc(heap, new_size, ZEND_MM_CHUNK_SIZE)) != NULL) {
 			/* pass */
 		} else {
-#if !ZEND_MM_LIMIT
-			zend_mm_safe_error(heap, "Out of memory");
-#elif ZEND_DEBUG
-			zend_mm_safe_error(heap, "Out of memory (allocated %zu bytes) at %s:%d (tried to allocate %zu bytes)", heap->real_size, __zend_filename, __zend_lineno, size);
-#else
-			zend_mm_safe_error(heap, "Out of memory (allocated %zu bytes) (tried to allocate %zu bytes)", heap->real_size, size);
-#endif
+			zend_out_of_memory();
 			return NULL;
 		}
 	}
@@ -1934,9 +2068,12 @@ static zend_mm_heap *zend_mm_init(void)
 	heap->peak = 0;
 #endif
 #if ZEND_MM_LIMIT
-	heap->limit = (size_t)Z_L(-1) >> 1;
+	heap->limit = ZEND_MM_LIMIT_MAX;
+	heap->hard_limit = ZEND_MM_LIMIT_MAX;
+	heap->hard_limit_percent = -1;
 	heap->overflow = 0;
 #endif
+	heap->fallible = false;
 #if ZEND_MM_CUSTOM
 	heap->use_custom_heap = ZEND_MM_CUSTOM_HEAP_NONE;
 #endif
@@ -2268,6 +2405,7 @@ void zend_mm_shutdown(zend_mm_heap *heap, bool full, bool silent)
 			if (silent) {
 				tracked_free_all();
 			}
+			heap->max_allocs = 0;
 			zend_hash_clean(heap->tracked_allocs);
 			if (full) {
 				zend_hash_destroy(heap->tracked_allocs);
@@ -2641,33 +2779,54 @@ ZEND_API size_t ZEND_FASTCALL _zend_mem_block_size(void *ptr ZEND_FILE_LINE_DC Z
 	return zend_mm_size(AG(mm_heap), ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
 }

+#define ZEND_MM_SAFE_ADDRESS_GUARDED(dest, nmemb, size, offset) do { \
+		size_t __nmemb = (nmemb); \
+		size_t __size = (size); \
+		size_t __offset = (offset); \
+		bool overflow; \
+		(dest) = zend_safe_address(__nmemb, __size, __offset, &overflow); \
+		if (UNEXPECTED(overflow)) { \
+			zend_mm_delayed_error_or_panic(AG(mm_heap)->fallible, "Possible integer overflow in memory allocation (%zu * %zu + %zu)", __nmemb, __size, __offset); \
+			return NULL; \
+		} \
+	} while (0)
+
 ZEND_API void* ZEND_FASTCALL _safe_emalloc(size_t nmemb, size_t size, size_t offset ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
 {
-	return _emalloc(zend_safe_address_guarded(nmemb, size, offset) ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
+	size_t actual_size;
+	ZEND_MM_SAFE_ADDRESS_GUARDED(actual_size, nmemb, size, offset);
+	return _emalloc(actual_size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
 }

 ZEND_API void* ZEND_FASTCALL _safe_malloc(size_t nmemb, size_t size, size_t offset)
 {
-	return pemalloc(zend_safe_address_guarded(nmemb, size, offset), 1);
+	size_t actual_size;
+	ZEND_MM_SAFE_ADDRESS_GUARDED(actual_size, nmemb, size, offset);
+	return pemalloc(actual_size, 1);
 }

 ZEND_API void* ZEND_FASTCALL _safe_erealloc(void *ptr, size_t nmemb, size_t size, size_t offset ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
 {
-	return _erealloc(ptr, zend_safe_address_guarded(nmemb, size, offset) ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
+	size_t actual_size;
+	ZEND_MM_SAFE_ADDRESS_GUARDED(actual_size, nmemb, size, offset);
+	return _erealloc(ptr, actual_size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
 }

 ZEND_API void* ZEND_FASTCALL _safe_realloc(void *ptr, size_t nmemb, size_t size, size_t offset)
 {
-	return perealloc(ptr, zend_safe_address_guarded(nmemb, size, offset), 1);
+	size_t actual_size;
+	ZEND_MM_SAFE_ADDRESS_GUARDED(actual_size, nmemb, size, offset);
+	return perealloc(ptr, actual_size, 1);
 }

 ZEND_API void* ZEND_FASTCALL _ecalloc(size_t nmemb, size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
 {
 	void *p;
+	size_t actual_size;
+	ZEND_MM_SAFE_ADDRESS_GUARDED(actual_size, nmemb, size, 0);

-	size = zend_safe_address_guarded(nmemb, size, 0);
-	p = _emalloc(size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
-	memset(p, 0, size);
+	p = _emalloc(actual_size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
+	memset(p, 0, actual_size);
 	return p;
 }

@@ -2678,7 +2837,8 @@ ZEND_API char* ZEND_FASTCALL _estrdup(const char *s ZEND_FILE_LINE_DC ZEND_FILE_

 	length = strlen(s);
 	if (UNEXPECTED(length + 1 == 0)) {
-		zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (1 * %zu + 1)", length);
+		zend_mm_delayed_error_or_panic(AG(mm_heap)->fallible, "Possible integer overflow in memory allocation (1 * %zu + 1)", length);
+		return NULL;
 	}
 	p = (char *) _emalloc(length + 1 ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
 	memcpy(p, s, length+1);
@@ -2690,7 +2850,8 @@ ZEND_API char* ZEND_FASTCALL _estrndup(const char *s, size_t length ZEND_FILE_LI
 	char *p;

 	if (UNEXPECTED(length + 1 == 0)) {
-		zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (1 * %zu + 1)", length);
+		zend_mm_delayed_error_or_panic(AG(mm_heap)->fallible, "Possible integer overflow in memory allocation (1 * %zu + 1)", length);
+		return NULL;
 	}
 	p = (char *) _emalloc(length + 1 ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
 	memcpy(p, s, length);
@@ -2698,14 +2859,13 @@ ZEND_API char* ZEND_FASTCALL _estrndup(const char *s, size_t length ZEND_FILE_LI
 	return p;
 }

-static ZEND_COLD ZEND_NORETURN void zend_out_of_memory(void);
-
 ZEND_API char* ZEND_FASTCALL zend_strndup(const char *s, size_t length)
 {
 	char *p;

 	if (UNEXPECTED(length + 1 == 0)) {
-		zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (1 * %zu + 1)", length);
+		zend_mm_delayed_error_or_panic(AG(mm_heap)->fallible, "Possible integer overflow in memory allocation (1 * %zu + 1)", length);
+		return NULL;
 	}
 	p = (char *) malloc(length + 1);
 	if (UNEXPECTED(p == NULL)) {
@@ -2718,11 +2878,119 @@ ZEND_API char* ZEND_FASTCALL zend_strndup(const char *s, size_t length)
 	return p;
 }

+/* Fallible allocation */
+
+#define ZEND_MM_FALLIBLE_FORWARD(expr) do {             \
+		bool __was_fallible = AG(mm_heap)->fallible;    \
+		AG(mm_heap)->fallible = true;                   \
+		void *__result = (expr);                        \
+		AG(mm_heap)->fallible = __was_fallible;         \
+		return __result;                                \
+	} while (0)
+
+ZEND_API void* ZEND_FASTCALL _femalloc(size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
+{
+	ZEND_MM_FALLIBLE_FORWARD(_emalloc(size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC));
+}
+
+ZEND_API void* ZEND_FASTCALL _fecalloc(size_t nmemb, size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
+{
+	void *p = _safe_femalloc(nmemb, size, 0 ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
+
+	if (!p) {
+		return NULL;
+	}
+
+	memset(p, 0, nmemb * size);
+	return p;
+}
+
+ZEND_API void* ZEND_FASTCALL _ferealloc(void *ptr, size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
+{
+	ZEND_MM_FALLIBLE_FORWARD(_erealloc(ptr, size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC));
+}
+
+ZEND_API void* ZEND_FASTCALL _ferealloc2(void *ptr, size_t size, size_t copy_size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
+{
+	ZEND_MM_FALLIBLE_FORWARD(_erealloc2(ptr, size, copy_size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC));
+}
+
+ZEND_API void* ZEND_FASTCALL _safe_femalloc(size_t nmemb, size_t size, size_t offset ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
+{
+	ZEND_MM_FALLIBLE_FORWARD(_safe_emalloc(nmemb, size, offset ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC));
+}
+
+ZEND_API void* ZEND_FASTCALL _safe_ferealloc(void *ptr, size_t nmemb, size_t size, size_t offset ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
+{
+	ZEND_MM_FALLIBLE_FORWARD(_safe_erealloc(ptr, nmemb, size, offset ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC));
+}
+
+#if ZEND_MM_LIMIT
+static size_t scale_by_percent(size_t value, zend_long percent)
+{
+	if (percent <= 0) {
+		return 0;
+	}
+
+	size_t p = (size_t)percent;
+	size_t q = value / 100;
+	size_t r = value % 100;
+
+	if (q > SIZE_MAX / p) {
+		return SIZE_MAX;
+	}
+
+	size_t hi = q * p;
+	size_t lo = r * (p / 100) + r * (p % 100) / 100;
+
+	if (hi > SIZE_MAX - lo) {
+		return SIZE_MAX;
+	}
+	return hi + lo;
+}
+
+static void zend_mm_update_hard_limit(zend_mm_heap *heap)
+{
+	ZEND_ASSERT(heap->real_size <= heap->limit);
+
+	zend_long percent = heap->hard_limit_percent;
+
+	if (percent == -1) {
+		/* For memory limit >= 1G, use a fixed 1G buffer */
+		if (heap->limit > 1*1024*1024*1024) {
+			size_t offset = 1*1024*1024*1024;
+			if (ZEND_MM_LIMIT_MAX - heap->limit < offset) {
+				heap->hard_limit = ZEND_MM_LIMIT_MAX;
+			} else {
+				heap->hard_limit = heap->limit + offset;
+			}
+			ZEND_ASSERT(heap->hard_limit >= heap->real_size);
+			return;
+		}
+		percent = 200;
+	}
+
+	ZEND_ASSERT(percent >= 100);
+
+	size_t hard_limit = scale_by_percent(heap->limit, percent);
+	if (hard_limit > ZEND_MM_LIMIT_MAX) {
+		hard_limit = ZEND_MM_LIMIT_MAX;
+	}
+
+	heap->hard_limit = hard_limit;
+	ZEND_ASSERT(heap->hard_limit >= heap->real_size);
+}
+#endif
+
 ZEND_API zend_result zend_set_memory_limit(size_t memory_limit)
 {
 #if ZEND_MM_LIMIT
 	zend_mm_heap *heap = AG(mm_heap);

+	if (memory_limit > ZEND_MM_LIMIT_MAX) {
+		memory_limit = ZEND_MM_LIMIT_MAX;
+	}
+
 	if (UNEXPECTED(memory_limit < heap->real_size)) {
 		if (memory_limit >= heap->real_size - heap->cached_chunks_count * ZEND_MM_CHUNK_SIZE) {
 			/* free some cached chunks to fit into new memory limit */
@@ -2733,11 +3001,34 @@ ZEND_API zend_result zend_set_memory_limit(size_t memory_limit)
 				heap->cached_chunks_count--;
 				heap->real_size -= ZEND_MM_CHUNK_SIZE;
 			} while (memory_limit < heap->real_size);
+			heap->limit = memory_limit;
+			zend_mm_update_hard_limit(heap);
 			return SUCCESS;
 		}
 		return FAILURE;
 	}
-	AG(mm_heap)->limit = memory_limit;
+	heap->limit = memory_limit;
+	zend_mm_update_hard_limit(heap);
+#endif
+	return SUCCESS;
+}
+
+ZEND_API zend_result zend_set_memory_hard_limit_percent(zend_long percent)
+{
+#if ZEND_MM_LIMIT
+	zend_mm_heap *heap = AG(mm_heap);
+
+	if (percent < 100 && percent != -1) {
+		return FAILURE;
+	}
+
+	if (heap->real_size > heap->limit) {
+		/* Do not update memory limits while the soft limit is exceeded, as this
+		 * could break invariant hard_limit >= real_size. */
+		return FAILURE;
+	}
+	heap->hard_limit_percent = percent;
+	zend_mm_update_hard_limit(heap);
 #endif
 	return SUCCESS;
 }
@@ -2784,6 +3075,19 @@ ZEND_API void zend_memory_reset_peak_usage(void)
 #endif
 }

+ZEND_API void zend_mm_set_max_allocs(uint64_t max_allocs)
+{
+#if ZEND_MM_CUSTOM
+	AG(mm_heap)->max_allocs = max_allocs;
+	zend_mm_max_allocs_reached = false;
+#endif
+}
+
+ZEND_API bool zend_mm_max_allocs_hit(void)
+{
+	return zend_mm_max_allocs_reached;
+}
+
 ZEND_API void shutdown_memory_manager(bool silent, bool full_shutdown)
 {
 	zend_mm_shutdown(AG(mm_heap), full_shutdown, silent);
@@ -2811,24 +3115,50 @@ static zend_always_inline zval *tracked_get_size_zv(zend_mm_heap *heap, void *pt
 	return size_zv;
 }

-static zend_always_inline void tracked_check_limit(zend_mm_heap *heap, size_t add_size) {
-	if (add_size > heap->limit - heap->size && !heap->overflow) {
+static zend_always_inline zend_result tracked_check_limit(zend_mm_heap *heap, size_t add_size ZEND_FILE_LINE_DC) {
+	return zend_mm_check_limit(heap, add_size, /* gc */ false, NULL ZEND_FILE_LINE_RELAY_CC);
+}
+
+/* ZEND_MM_MAX_ALLOCS fails the n-th allocation of the request in the same way
+ * as the memory limit would, so that the OOM handling can be exercised at an
+ * arbitrary allocation rather than only at the ones large enough to reach the
+ * limit. */
+static zend_never_inline ZEND_COLD zend_result tracked_max_allocs_error(zend_mm_heap *heap, size_t add_size ZEND_FILE_LINE_DC) {
+	zend_mm_max_allocs_reached = true;
+
+	if (!ZEND_HAS_OOM()) {
+		zend_mm_delayed_error(
 #if ZEND_DEBUG
-		zend_mm_safe_error(heap,
-			"Allowed memory size of %zu bytes exhausted at %s:%d (tried to allocate %zu bytes)",
-			heap->limit, "file", 0, add_size);
+				"Allowed memory size of %zu bytes exhausted at %s:%d (tried to allocate %zu bytes)",
 #else
-		zend_mm_safe_error(heap,
-			"Allowed memory size of %zu bytes exhausted (tried to allocate %zu bytes)",
-			heap->limit, add_size);
+				"Allowed memory size of %zu bytes exhausted (tried to allocate %zu bytes)",
 #endif
+				heap->limit
+				ZEND_FILE_LINE_RELAY_CC,
+				add_size);
 	}
+
+	return heap->fallible ? FAILURE : SUCCESS;
+}
+
+static zend_always_inline zend_result tracked_check_max_allocs(zend_mm_heap *heap, size_t add_size ZEND_FILE_LINE_DC) {
+	if (UNEXPECTED(heap->max_allocs && !--heap->max_allocs)) {
+		return tracked_max_allocs_error(heap, add_size ZEND_FILE_LINE_RELAY_CC);
+	}
+
+	return SUCCESS;
 }

 static void *tracked_malloc(size_t size)
 {
 	zend_mm_heap *heap = AG(mm_heap);
-	tracked_check_limit(heap, size);
+	if (UNEXPECTED(tracked_check_limit(heap, size ZEND_FILE_LINE_CC) == FAILURE)) {
+		return NULL;
+	}
+
+	if (UNEXPECTED(tracked_check_max_allocs(heap, size ZEND_FILE_LINE_CC) == FAILURE)) {
+		return NULL;
+	}

 	void *ptr = malloc(size);
 	if (!ptr) {
@@ -2864,7 +3194,13 @@ static void *tracked_realloc(void *ptr, size_t new_size) {
 	}

 	if (new_size > old_size) {
-		tracked_check_limit(heap, new_size - old_size);
+		if (UNEXPECTED(tracked_check_limit(heap, new_size - old_size ZEND_FILE_LINE_CC) == FAILURE)) {
+			return NULL;
+		}
+
+		if (UNEXPECTED(tracked_check_max_allocs(heap, new_size - old_size ZEND_FILE_LINE_CC) == FAILURE)) {
+			return NULL;
+		}
 	}

 	/* Delete information about old allocation only after checking the memory limit. */
@@ -2900,8 +3236,11 @@ static void alloc_globals_ctor(zend_alloc_globals *alloc_globals)
 		zend_mm_heap *mm_heap = alloc_globals->mm_heap = malloc(sizeof(zend_mm_heap));
 		memset(mm_heap, 0, sizeof(zend_mm_heap));
 		mm_heap->use_custom_heap = ZEND_MM_CUSTOM_HEAP_STD;
-		mm_heap->limit = (size_t)Z_L(-1) >> 1;
+		mm_heap->limit = ZEND_MM_LIMIT_MAX;
+		mm_heap->hard_limit = ZEND_MM_LIMIT_MAX;
+		mm_heap->hard_limit_percent = -1;
 		mm_heap->overflow = 0;
+		mm_heap->fallible = false;

 		if (!tracked) {
 			/* Use system allocator. */
@@ -2964,6 +3303,16 @@ ZEND_API zend_mm_heap *zend_mm_get_heap(void)
 	return AG(mm_heap);
 }

+ZEND_API bool zend_mm_is_fallible(void)
+{
+	return AG(mm_heap)->fallible;
+}
+
+ZEND_API void zend_mm_set_fallible(bool fallible)
+{
+	AG(mm_heap)->fallible = fallible;
+}
+
 ZEND_API bool zend_mm_is_custom_heap(zend_mm_heap *new_heap)
 {
 #if ZEND_MM_CUSTOM
@@ -3089,8 +3438,9 @@ ZEND_API zend_mm_heap *zend_mm_startup_ex(const zend_mm_handlers *handlers, void
 	heap->peak = 0;
 #endif
 #if ZEND_MM_LIMIT
-	heap->limit = (size_t)Z_L(-1) >> 1;
+	heap->limit = ZEND_MM_LIMIT_MAX;
 	heap->overflow = 0;
+	heap->fallible = false;
 #endif
 #if ZEND_MM_CUSTOM
 	heap->use_custom_heap = 0;
@@ -3118,6 +3468,60 @@ ZEND_API zend_mm_heap *zend_mm_startup_ex(const zend_mm_handlers *handlers, void
 #endif
 }

+ZEND_API void ZEND_FASTCALL zend_handle_delayed_oom(void)
+{
+	zend_oom_info *info = &EG(delayed_oom);
+	zend_execute_data *execute_data = EG(current_execute_data);
+
+	if (info->state != ZEND_OOM_STATE_PENDING) {
+		return;
+	}
+	info->state = ZEND_OOM_STATE_HANDLING;
+
+	AG(mm_heap)->overflow = 1;
+
+	/* message was allocated persistently, use emalloc() now as error_get_last()
+	 * may expose it to userland. */
+	zend_string *message = zend_string_init(ZSTR_VAL(info->message), ZSTR_LEN(info->message), 0);
+	zend_string_release(info->message);
+	info->message = message;
+
+	if (execute_data) {
+		/* An exception may already be in flight, and zend_throw_unwind_exit()
+		 * does not allow that */
+		EG(exception) = NULL;
+
+		/* Object allocation should succeed unless it trips the hard limit */
+		zend_throw_unwind_exit();
+	}
+
+	zend_try {
+		zend_error_zstr_at(E_ERROR, info->filename, info->lineno, info->message);
+	} zend_catch {
+	} zend_end_try();
+
+	AG(mm_heap)->overflow = 0;
+
+	/* Bailout discards EG(current_execute_data). Would be better to avoid the bailout here. */
+	EG(current_execute_data) = execute_data;
+
+	/* This function may be called from the VM interrupt handler: opline is not
+	 * executed yet. Set opline_before_exception to NULL so
+	 * ZEND_HANDLE_EXCEPTION does not use it. */
+	EG(opline_before_exception) = NULL;
+
+	zend_mm_oom_info_dtor(info);
+	info->state = ZEND_OOM_STATE_HANDLED;
+
+	/* No userland frame to unwind: there is nothing to restore afterwards
+	 * and no exception to throw, so catching the bailout here would just
+	 * let the request carry on past a fatal error. Let it propagate and
+	 * abort the request, the way an immediate OOM does. */
+	if (!execute_data) {
+		zend_bailout();
+	}
+}
+
 ZEND_API void * __zend_malloc(size_t len)
 {
 	void *tmp = malloc(len);
@@ -3130,10 +3534,11 @@ ZEND_API void * __zend_malloc(size_t len)
 ZEND_API void * __zend_calloc(size_t nmemb, size_t len)
 {
 	void *tmp;
+	size_t actual_size;
+	ZEND_MM_SAFE_ADDRESS_GUARDED(actual_size, nmemb, len, 0);

-	len = zend_safe_address_guarded(nmemb, len, 0);
-	tmp = __zend_malloc(len);
-	memset(tmp, 0, len);
+	tmp = __zend_malloc(actual_size);
+	memset(tmp, 0, actual_size);
 	return tmp;
 }

diff --git a/Zend/zend_alloc.h b/Zend/zend_alloc.h
index 578d4c78cc5..6d1d9c2f17a 100644
--- a/Zend/zend_alloc.h
+++ b/Zend/zend_alloc.h
@@ -37,6 +37,20 @@
 #define ZEND_MM_ALIGNED_SIZE_EX(size, alignment) \
 	(((size) + ((alignment) - 1)) & ~((alignment) - 1))

+typedef enum _zend_oom_state {
+	ZEND_OOM_STATE_NONE = 0,
+	ZEND_OOM_STATE_PENDING,
+	ZEND_OOM_STATE_HANDLING,
+	ZEND_OOM_STATE_HANDLED
+} zend_oom_state;
+
+typedef struct _zend_oom_info {
+	zend_oom_state state;
+	uint32_t lineno;
+	zend_string *message;
+	zend_string *filename;
+} zend_oom_info;
+
 typedef struct _zend_leak_info {
 	void *addr;
 	size_t size;
@@ -46,6 +60,8 @@ typedef struct _zend_leak_info {
 	uint32_t orig_lineno;
 } zend_leak_info;

+#define ZEND_HAS_OOM() (UNEXPECTED(EG(delayed_oom).state != ZEND_OOM_STATE_NONE))
+
 #if ZEND_DEBUG
 typedef struct _zend_mm_debug_info {
 	size_t             size;
@@ -214,7 +230,40 @@ ZEND_API ZEND_ATTRIBUTE_MALLOC char * __zend_strdup(const char *s);
 #define perealloc2_recoverable_rel(ptr, size, copy_size, persistent) ((persistent)?realloc((ptr), (size)):erealloc2_recoverable_rel((ptr), (size), (copy_size)))
 #define pestrdup_rel(s, persistent) ((persistent)?strdup(s):estrdup_rel(s))

+/* Fallible allocation.
+ * These functions return NULL if allocation would exceed memory limit. */
+
+ZEND_API ZEND_ATTRIBUTE_MALLOC void*  ZEND_FASTCALL _femalloc(size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC) ZEND_ATTRIBUTE_ALLOC_SIZE(1);
+ZEND_API ZEND_ATTRIBUTE_MALLOC void*  ZEND_FASTCALL _fecalloc(size_t nmemb, size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC) ZEND_ATTRIBUTE_ALLOC_SIZE2(1,2);
+ZEND_API void*  ZEND_FASTCALL _ferealloc(void *ptr, size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC) ZEND_ATTRIBUTE_ALLOC_SIZE(2);
+ZEND_API void*  ZEND_FASTCALL _ferealloc2(void *ptr, size_t size, size_t copy_size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC) ZEND_ATTRIBUTE_ALLOC_SIZE(2);
+ZEND_API ZEND_ATTRIBUTE_MALLOC void*  ZEND_FASTCALL _safe_femalloc(size_t nmemb, size_t size, size_t offset ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
+ZEND_API void*  ZEND_FASTCALL _safe_ferealloc(void *ptr, size_t nmemb, size_t size, size_t offset ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
+
+#define femalloc(size, fallible) \
+	((fallible)?_femalloc((size) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC):_emalloc((size) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC))
+#define fecalloc(nmemb, size, fallible) \
+	((fallible)?_fecalloc((nmemb), (size) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC):_ecalloc((nmemb), (size) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC))
+#define ferealloc(ptr, size, fallible) \
+	((fallible)?_ferealloc((ptr), (size) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC):_erealloc((ptr), (size) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC))
+#define ferealloc2(ptr, size, copy_size, fallible) \
+	((fallible)?_ferealloc2((ptr), (size), (copy_size) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC):_erealloc2((ptr), (size), (copy_size) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC))
+#define safe_femalloc(nmemb, size, offset, fallible) \
+	((fallible)?_safe_femalloc((nmemb), (size), (offset) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC):_safe_emalloc((nmemb), (size), (offset) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC))
+#define safe_ferealloc(ptr, nmemb, size, offset, fallible) \
+	((fallible)?_safe_ferealloc((ptr), (nmemb), (size), (offset) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC):_safe_erealloc((ptr), (nmemb), (size), (offset) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC))
+
+#define fpemalloc(size, persistent, fallible) \
+	((persistent)?__zend_malloc(size):femalloc(size, fallible))
+#define fperealloc(ptr, size, persistent, fallible) ((persistent)?__zend_realloc((ptr), (size)):ferealloc((ptr), (size), (fallible)))
+#define fperealloc2(ptr, size, copy_size, persistent, fallible) ((persistent)?__zend_realloc((ptr), (size)):ferealloc2((ptr), (size), (copy_size), (fallible)))
+#define safe_fpemalloc(nmemb, size, offset, persistent, fallible) \
+	((persistent)?_safe_malloc(nmemb, size, offset):safe_femalloc(nmemb, size, offset, fallible))
+#define safe_fperealloc(ptr, nmemb, size, offset, persistent, fallible) \
+	((persistent)?_safe_realloc(ptr, nmemb, size, offset):safe_ferealloc(ptr, nmemb, size, offset, fallible))
+
 ZEND_API zend_result zend_set_memory_limit(size_t memory_limit);
+ZEND_API zend_result zend_set_memory_hard_limit_percent(zend_long percent);
 ZEND_API bool zend_alloc_in_memory_limit_error_reporting(void);

 ZEND_API void start_memory_manager(void);
@@ -267,6 +316,9 @@ ZEND_API zend_mm_heap *zend_mm_get_heap(void);

 ZEND_API size_t zend_mm_gc(zend_mm_heap *heap);

+ZEND_API bool zend_mm_is_fallible(void);
+ZEND_API void zend_mm_set_fallible(bool fallible);
+
 #define ZEND_MM_CUSTOM_HEAP_NONE  0
 #define ZEND_MM_CUSTOM_HEAP_STD   1
 #define ZEND_MM_CUSTOM_HEAP_DEBUG 2
@@ -310,6 +362,17 @@ struct _zend_mm_storage {
 ZEND_API zend_mm_storage *zend_mm_get_storage(zend_mm_heap *heap);
 ZEND_API zend_mm_heap *zend_mm_startup_ex(const zend_mm_handlers *handlers, void *data, size_t data_size);

+ZEND_API ZEND_COLD void zend_mm_delayed_error_or_panic(bool delay, const char *format, ...) ZEND_ATTRIBUTE_FORMAT(printf, 2, 3);
+ZEND_API void ZEND_FASTCALL zend_handle_delayed_oom(void);
+ZEND_COLD void zend_mm_oom_info_dtor(zend_oom_info *info);
+
+/* Exit status of a process that stopped because of ZEND_MM_MAX_ALLOCS, so that
+ * it can be told apart from an ordinary fatal error. */
+#define ZEND_MM_MAX_ALLOCS_EXIT_STATUS 121
+
+ZEND_API void zend_mm_set_max_allocs(uint64_t max_allocs);
+ZEND_API bool zend_mm_max_allocs_hit(void);
+
 /*

 // The following example shows how to use zend_mm_heap API with custom storage
diff --git a/Zend/zend_attributes.c b/Zend/zend_attributes.c
index 2471a6739e3..9a764b7b226 100644
--- a/Zend/zend_attributes.c
+++ b/Zend/zend_attributes.c
@@ -114,6 +114,12 @@ ZEND_METHOD(SensitiveParameter, __construct)
 	ZEND_PARSE_PARAMETERS_NONE();
 }

+void zend_sensitive_parameter_value_init(zend_object *obj, zval *value)
+{
+	ZEND_ASSERT(obj->ce == zend_ce_sensitive_parameter_value);
+	zend_update_property(zend_ce_sensitive_parameter_value, obj, "value", strlen("value"), value);
+}
+
 ZEND_METHOD(SensitiveParameterValue, __construct)
 {
 	zval *value;
@@ -122,7 +128,7 @@ ZEND_METHOD(SensitiveParameterValue, __construct)
 		Z_PARAM_ZVAL(value)
 	ZEND_PARSE_PARAMETERS_END();

-	zend_update_property(zend_ce_sensitive_parameter_value, Z_OBJ_P(ZEND_THIS), "value", strlen("value"), value);
+	zend_sensitive_parameter_value_init(Z_OBJ_P(ZEND_THIS), value);
 }

 ZEND_METHOD(SensitiveParameterValue, getValue)
diff --git a/Zend/zend_attributes.h b/Zend/zend_attributes.h
index 746155f5147..78e3db1f001 100644
--- a/Zend/zend_attributes.h
+++ b/Zend/zend_attributes.h
@@ -122,4 +122,6 @@ static zend_always_inline zend_attribute *zend_add_class_constant_attribute(zend
 void zend_register_attribute_ce(void);
 void zend_attributes_shutdown(void);

+void zend_sensitive_parameter_value_init(zend_object *obj, zval *value);
+
 #endif
diff --git a/Zend/zend_builtin_functions.c b/Zend/zend_builtin_functions.c
index 898318102c1..d52fd32f06d 100644
--- a/Zend/zend_builtin_functions.c
+++ b/Zend/zend_builtin_functions.c
@@ -1534,7 +1534,7 @@ ZEND_FUNCTION(get_defined_constants)
 }
 /* }}} */

-static void debug_backtrace_get_args(zend_execute_data *call, zval *arg_array) /* {{{ */
+static bool debug_backtrace_get_args(zend_execute_data *call, zval *arg_array) /* {{{ */
 {
 	uint32_t num_args = ZEND_CALL_NUM_ARGS(call);

@@ -1542,8 +1542,9 @@ static void debug_backtrace_get_args(zend_execute_data *call, zval *arg_array) /
 		uint32_t i = 0;
 		zval *p = ZEND_CALL_ARG(call, 1);

-		array_init_size(arg_array, num_args);
-		zend_hash_real_init_packed(Z_ARRVAL_P(arg_array));
+		if (!array_finit_size_packed(arg_array, num_args)) {
+			return false;
+		}
 		ZEND_HASH_FILL_PACKED(Z_ARRVAL_P(arg_array)) {
 			if (call->func->type == ZEND_USER_FUNCTION) {
 				uint32_t first_extra_arg = MIN(num_args, call->func->op_array.num_args);
@@ -1576,8 +1577,13 @@ static void debug_backtrace_get_args(zend_execute_data *call, zval *arg_array) /
 						if (is_sensitive) {
 							zval redacted_arg;
 							object_init_ex(&redacted_arg, zend_ce_sensitive_parameter_value);
-							zend_call_method_with_1_params(Z_OBJ_P(&redacted_arg), zend_ce_sensitive_parameter_value, &zend_ce_sensitive_parameter_value->constructor, "__construct", NULL, &original_arg);
+							zend_sensitive_parameter_value_init(Z_OBJ(redacted_arg), &original_arg);
 							ZEND_HASH_FILL_SET(&redacted_arg);
+							if (ZEND_HAS_OOM()) {
+								zval_ptr_dtor(&redacted_arg);
+								ZEND_HASH_FILL_FINISH();
+								goto oom;
+							}
 						} else {
 							Z_TRY_ADDREF_P(&original_arg);
 							ZEND_HASH_FILL_SET(&original_arg);
@@ -1608,8 +1614,13 @@ static void debug_backtrace_get_args(zend_execute_data *call, zval *arg_array) /
 						if (is_sensitive) {
 							zval redacted_arg;
 							object_init_ex(&redacted_arg, zend_ce_sensitive_parameter_value);
-							zend_call_method_with_1_params(Z_OBJ_P(&redacted_arg), zend_ce_sensitive_parameter_value, &zend_ce_sensitive_parameter_value->constructor, "__construct", NULL, &original_arg);
+							zend_sensitive_parameter_value_init(Z_OBJ(redacted_arg), &original_arg);
 							ZEND_HASH_FILL_SET(&redacted_arg);
+							if (ZEND_HAS_OOM()) {
+								zval_ptr_dtor(&redacted_arg);
+								ZEND_HASH_FILL_FINISH();
+								goto oom;
+							}
 						} else {
 							Z_TRY_ADDREF_P(&original_arg);
 							ZEND_HASH_FILL_SET(&original_arg);
@@ -1648,8 +1659,13 @@ static void debug_backtrace_get_args(zend_execute_data *call, zval *arg_array) /
 				if (is_sensitive) {
 					zval redacted_arg;
 					object_init_ex(&redacted_arg, zend_ce_sensitive_parameter_value);
-					zend_call_method_with_1_params(Z_OBJ_P(&redacted_arg), zend_ce_sensitive_parameter_value, &zend_ce_sensitive_parameter_value->constructor, "__construct", NULL, &original_arg);
+					zend_sensitive_parameter_value_init(Z_OBJ(redacted_arg), &original_arg);
 					ZEND_HASH_FILL_SET(&redacted_arg);
+					if (ZEND_HAS_OOM()) {
+						zval_ptr_dtor(&redacted_arg);
+						ZEND_HASH_FILL_FINISH();
+						goto oom;
+					}
 				} else {
 					Z_TRY_ADDREF_P(&original_arg);
 					ZEND_HASH_FILL_SET(&original_arg);
@@ -1672,9 +1688,18 @@ static void debug_backtrace_get_args(zend_execute_data *call, zval *arg_array) /
 		ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(call->extra_named_params, name, arg) {
 			ZVAL_DEREF(arg);
 			Z_TRY_ADDREF_P(arg);
-			zend_hash_add_new(Z_ARRVAL_P(arg_array), name, arg);
+			if (UNEXPECTED(zend_hash_fadd_new(Z_ARRVAL_P(arg_array), name, arg) == HASH_NOMEM)) {
+				goto oom;
+			}
 		} ZEND_HASH_FOREACH_END();
 	}
+
+	return true;
+
+oom:
+	zval_ptr_dtor(arg_array);
+	ZVAL_EMPTY_ARRAY(arg_array);
+	return false;
 }
 /* }}} */

@@ -1830,7 +1855,11 @@ ZEND_API void zend_fetch_debug_backtrace(zval *return_value, int skip_last, int
 			if ((options & DEBUG_BACKTRACE_IGNORE_ARGS) == 0 &&
 				func->type != ZEND_EVAL_CODE) {

-				debug_backtrace_get_args(call, &tmp);
+				if (UNEXPECTED(!debug_backtrace_get_args(call, &tmp))) {
+					ZEND_ASSERT(ZEND_HAS_OOM());
+					zend_array_destroy(stack_frame);
+					return;
+				}
 				_zend_hash_append_ex(stack_frame, ZSTR_KNOWN(ZEND_STR_ARGS), &tmp, 1);
 			}
 		} else {
@@ -1890,9 +1919,16 @@ ZEND_API void zend_fetch_debug_backtrace(zval *return_value, int skip_last, int
 		}

 		ZVAL_ARR(&tmp, stack_frame);
-		zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &tmp);
+		if (UNEXPECTED(zend_hash_next_index_finsert_new(Z_ARRVAL_P(return_value), &tmp) == HASH_NOMEM)) {
+			zend_array_destroy(stack_frame);
+			return;
+		}

 skip_frame:
+		if (ZEND_HAS_OOM()) {
+			return;
+		}
+
 		if (UNEXPECTED(ZEND_CALL_KIND(call) == ZEND_CALL_TOP_FUNCTION)
 		 && !fake_frame
 		 && prev
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index bea764453b8..b818ed180eb 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -412,6 +412,7 @@ void init_compiler(void) /* {{{ */
 	CG(delayed_autoloads) = NULL;
 	CG(unlinked_uses) = NULL;
 	CG(current_linking_class) = NULL;
+	CG(interned_strings_fallible) = false;
 }
 /* }}} */

diff --git a/Zend/zend_exceptions.c b/Zend/zend_exceptions.c
index d2547ced6f7..54202ecbc19 100644
--- a/Zend/zend_exceptions.c
+++ b/Zend/zend_exceptions.c
@@ -102,7 +102,9 @@ void zend_exception_set_previous(zend_object *exception, zend_object *add_previo
 		return;
 	}

-	if (exception == add_previous || zend_is_unwind_exit(add_previous) || zend_is_graceful_exit(add_previous)) {
+	if (exception == add_previous
+	 || zend_is_unwind_exit(exception) || zend_is_graceful_exit(exception)
+	 || zend_is_unwind_exit(add_previous) || zend_is_graceful_exit(add_previous)) {
 		OBJ_RELEASE(add_previous);
 		return;
 	}
diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c
index 7b7d618c37d..4777d5d68bc 100644
--- a/Zend/zend_execute.c
+++ b/Zend/zend_execute.c
@@ -4342,6 +4342,18 @@ static void cleanup_unfinished_calls(zend_execute_data *execute_data, uint32_t o
 }
 /* }}} */

+/* Same as cleanup_unfinished_calls(), but only free the call frames. */
+static void free_unfinished_calls(zend_execute_data *execute_data)
+{
+	zend_execute_data *call = EX(call);
+
+	while (UNEXPECTED(call != NULL)) {
+		EX(call) = call->prev_execute_data;
+		zend_vm_stack_free_call_frame(call);
+		call = EX(call);
+	}
+}
+
 static const zend_live_range *find_live_range(const zend_op_array *op_array, uint32_t op_num, uint32_t var_num) /* {{{ */
 {
 	int i;
diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c
index fa126f959b3..2bf572bf2a3 100644
--- a/Zend/zend_execute_API.c
+++ b/Zend/zend_execute_API.c
@@ -393,10 +393,24 @@ ZEND_API void zend_shutdown_executor_values(bool fast_shutdown)
 		}
 #endif
 	} else {
+		/* may be used by zend_error_zstr_at before init_executor */
+		ZVAL_UNDEF(&EG(user_error_handler));
+		ZVAL_UNDEF(&EG(user_exception_handler));
+		memset(&EG(user_error_handlers_error_reporting), 0, sizeof(EG(user_error_handlers_error_reporting)));
+		memset(&EG(user_error_handlers), 0, sizeof(EG(user_error_handlers)));
+		memset(&EG(user_exception_handlers), 0, sizeof(EG(user_exception_handlers)));
+
 		zend_hash_discard(EG(zend_constants), EG(persistent_constants_count));
 	}

 	zend_objects_store_free_object_storage(&EG(objects_store), fast_shutdown);
+
+	/* may be used by zend_error_zstr_at before init_executor */
+	EG(record_errors) = false;
+	EG(num_errors) = 0;
+	EG(errors) = NULL;
+	EG(error_handling) = EH_NORMAL;
+	EG(exception) = NULL;
 }

 void shutdown_executor(void) /* {{{ */
@@ -431,6 +445,8 @@ void shutdown_executor(void) /* {{{ */
 		 */
 		zend_hash_discard(EG(function_table), EG(persistent_functions_count));
 		zend_hash_discard(EG(class_table), EG(persistent_classes_count));
+		EG(objects_store).object_buckets = NULL;
+		EG(objects_store).top = 1;
 	} else {
 		zend_vm_stack_destroy();

@@ -761,7 +777,11 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_
 	if (!EG(active)) {
 		return FAILURE; /* executor is already inactive */
 	}
-
+	if (ZEND_HAS_OOM()) {
+		/* Callers expect SUCCESS || EG(exception), so let's throw right now */
+		zend_handle_delayed_oom();
+		return FAILURE;
+	}
 	if (EG(exception)) {
 		if (fci_cache) {
 			zend_release_fcall_info_cache(fci_cache);
@@ -965,7 +985,7 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_
 		}

 #if ZEND_DEBUG
-		if (!EG(exception) && call->func) {
+		if (!EG(exception) && !ZEND_HAS_OOM() && call->func) {
 			if (should_throw) {
 				zend_internal_call_arginfo_violation(call->func);
 			}
@@ -986,21 +1006,24 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_
 			zval_ptr_dtor(fci->retval);
 			ZVAL_UNDEF(fci->retval);
 		}
+	}

-		/* This flag is regularly checked while running user functions, but not internal
-		 * So see whether interrupt flag was set while the function was running... */
-		if (zend_atomic_bool_exchange_ex(&EG(vm_interrupt), false)) {
-			if (zend_atomic_bool_load_ex(&EG(timed_out))) {
-				zend_timeout();
-			} else if (zend_interrupt_function) {
-				zend_interrupt_function(EG(current_execute_data));
-			}
+	if (UNEXPECTED(zend_atomic_bool_exchange_ex(&EG(vm_interrupt), false))) {
+		if (zend_atomic_bool_load_ex(&EG(timed_out))) {
+			zend_timeout();
+		} else if (ZEND_HAS_OOM()) {
+			zend_handle_delayed_oom();
+			zval_ptr_dtor(fci->retval);
+			ZVAL_UNDEF(fci->retval);
+		} else if (zend_interrupt_function) {
+			zend_interrupt_function(EG(current_execute_data));
 		}
+	}

-		if (UNEXPECTED(ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS)) {
-			OBJ_RELEASE(Z_OBJ(call->This));
-		}
+	if (UNEXPECTED(ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS)) {
+		OBJ_RELEASE(Z_OBJ(call->This));
 	}
+
 	EG(fake_scope) = orig_fake_scope;

 	zend_vm_stack_free_call_frame(call);
diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h
index 368127b10a6..ce0030f28ad 100644
--- a/Zend/zend_globals.h
+++ b/Zend/zend_globals.h
@@ -26,6 +26,7 @@

 #include "zend_globals_macros.h"

+#include "zend_alloc.h"
 #include "zend_atomic.h"
 #include "zend_stack.h"
 #include "zend_ptr_stack.h"
@@ -120,6 +121,7 @@ struct _zend_compiler_globals {
 	bool multibyte;
 	bool detect_unicode;
 	bool encoding_declared;
+	bool interned_strings_fallible;

 	zend_ast *ast;
 	zend_arena *ast_arena;
@@ -280,6 +282,8 @@ struct _zend_executor_globals {
 #endif

 	void *reserved[ZEND_MAX_RESERVED_RESOURCES];
+
+	zend_oom_info delayed_oom;
 };

 #define EG_FLAGS_INITIAL				(0)
diff --git a/Zend/zend_hash.c b/Zend/zend_hash.c
index 35d3736bf68..58e3832fe55 100644
--- a/Zend/zend_hash.c
+++ b/Zend/zend_hash.c
@@ -81,7 +81,10 @@ static void _zend_is_inconsistent(const HashTable *ht, const char *file, int lin

 #define ZEND_HASH_IF_FULL_DO_RESIZE(ht)				\
 	if ((ht)->nNumUsed >= (ht)->nTableSize) {		\
-		zend_hash_do_resize(ht);					\
+		zend_result _resize_status = zend_hash_do_fresize(ht, flag & HASH_FALLIBLE); \
+		if (ZEND_CONST_COND(flag & HASH_FALLIBLE, true) && UNEXPECTED(_resize_status == FAILURE)) { \
+			return HASH_NOMEM; \
+		} \
 	}

 ZEND_API void *zend_hash_str_find_ptr_lc(const HashTable *ht, const char *str, size_t len) {
@@ -106,9 +109,9 @@ ZEND_API void *zend_hash_find_ptr_lc(const HashTable *ht, zend_string *key) {
 	return result;
 }

-static void ZEND_FASTCALL zend_hash_do_resize(HashTable *ht);
+static zend_result ZEND_FASTCALL zend_hash_do_fresize(HashTable *ht, bool fallible);

-static zend_always_inline uint32_t zend_hash_check_size(uint32_t nSize)
+static zend_always_inline uint32_t zend_hash_check_size(uint32_t nSize, bool fallible)
 {
 #ifdef ZEND_WIN32
 	unsigned long index;
@@ -119,7 +122,8 @@ static zend_always_inline uint32_t zend_hash_check_size(uint32_t nSize)
 	if (nSize <= HT_MIN_SIZE) {
 		return HT_MIN_SIZE;
 	} else if (UNEXPECTED(nSize >= HT_MAX_SIZE)) {
-		zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (%u * %zu + %zu)", nSize, sizeof(Bucket), sizeof(Bucket));
+		zend_mm_delayed_error_or_panic(fallible, "Possible integer overflow in memory allocation (%u * %zu + %zu)", nSize, sizeof(Bucket), sizeof(Bucket));
+		return 0;
 	}

 #ifdef ZEND_WIN32
@@ -143,25 +147,34 @@ static zend_always_inline uint32_t zend_hash_check_size(uint32_t nSize)
 #endif
 }

-static zend_always_inline void zend_hash_real_init_packed_ex(HashTable *ht)
+static zend_always_inline zend_result zend_hash_real_finit_packed_ex(HashTable *ht, bool fallible)
 {
 	void *data;

 	if (UNEXPECTED(GC_FLAGS(ht) & IS_ARRAY_PERSISTENT)) {
-		data = pemalloc(HT_PACKED_SIZE_EX(ht->nTableSize, HT_MIN_MASK), 1);
+		data = fpemalloc(HT_PACKED_SIZE_EX(ht->nTableSize, HT_MIN_MASK), true, fallible);
 	} else if (EXPECTED(ht->nTableSize == HT_MIN_SIZE)) {
 		/* Use specialized API with constant allocation amount for a particularly common case. */
-		data = emalloc(HT_PACKED_SIZE_EX(HT_MIN_SIZE, HT_MIN_MASK));
+		data = femalloc(HT_PACKED_SIZE_EX(HT_MIN_SIZE, HT_MIN_MASK), fallible);
 	} else {
-		data = emalloc(HT_PACKED_SIZE_EX(ht->nTableSize, HT_MIN_MASK));
+		data = femalloc(HT_PACKED_SIZE_EX(ht->nTableSize, HT_MIN_MASK), fallible);
+	}
+	if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!data)) {
+		return FAILURE;
 	}
 	HT_SET_DATA_ADDR(ht, data);
 	/* Don't overwrite iterator count. */
 	ht->u.v.flags = HASH_FLAG_PACKED | HASH_FLAG_STATIC_KEYS;
 	HT_HASH_RESET_PACKED(ht);
+	return SUCCESS;
 }

-static zend_always_inline void zend_hash_real_init_mixed_ex(HashTable *ht)
+static zend_always_inline void zend_hash_real_init_packed_ex(HashTable *ht)
+{
+	zend_hash_real_finit_packed_ex(ht, /* fallible */ false);
+}
+
+static zend_always_inline zend_result zend_hash_real_finit_mixed_ex(HashTable *ht, bool fallible)
 {
 	void *data;
 	uint32_t nSize = ht->nTableSize;
@@ -169,9 +182,15 @@ static zend_always_inline void zend_hash_real_init_mixed_ex(HashTable *ht)
 	ZEND_ASSERT(HT_SIZE_TO_MASK(nSize));

 	if (UNEXPECTED(GC_FLAGS(ht) & IS_ARRAY_PERSISTENT)) {
-		data = pemalloc(HT_SIZE_EX(nSize, HT_SIZE_TO_MASK(nSize)), 1);
+		data = fpemalloc(HT_SIZE_EX(nSize, HT_SIZE_TO_MASK(nSize)), true, fallible);
+		if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!data)) {
+			return FAILURE;
+		}
 	} else if (EXPECTED(nSize == HT_MIN_SIZE)) {
-		data = emalloc(HT_SIZE_EX(HT_MIN_SIZE, HT_SIZE_TO_MASK(HT_MIN_SIZE)));
+		data = femalloc(HT_SIZE_EX(HT_MIN_SIZE, HT_SIZE_TO_MASK(HT_MIN_SIZE)), fallible);
+		if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!data)) {
+			return FAILURE;
+		}
 		ht->nTableMask = HT_SIZE_TO_MASK(HT_MIN_SIZE);
 		HT_SET_DATA_ADDR(ht, data);
 		/* Don't overwrite iterator count. */
@@ -211,27 +230,42 @@ static zend_always_inline void zend_hash_real_init_mixed_ex(HashTable *ht)
 		HT_HASH_EX(data, 14) = -1;
 		HT_HASH_EX(data, 15) = -1;
 #endif
-		return;
+		return SUCCESS;
 	} else {
-		data = emalloc(HT_SIZE_EX(nSize, HT_SIZE_TO_MASK(nSize)));
+		data = femalloc(HT_SIZE_EX(nSize, HT_SIZE_TO_MASK(nSize)), fallible);
+		if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!data)) {
+			return FAILURE;
+		}
 	}
 	ht->nTableMask = HT_SIZE_TO_MASK(nSize);
 	HT_SET_DATA_ADDR(ht, data);
 	HT_FLAGS(ht) = HASH_FLAG_STATIC_KEYS;
 	HT_HASH_RESET(ht);
+
+	return SUCCESS;
 }

-static zend_always_inline void zend_hash_real_init_ex(HashTable *ht, bool packed)
+static zend_always_inline void zend_hash_real_init_mixed_ex(HashTable *ht)
+{
+	zend_hash_real_finit_mixed_ex(ht, /* fallible */ false);
+}
+
+static zend_always_inline zend_result zend_hash_real_finit_ex(HashTable *ht, bool packed, bool fallible)
 {
 	HT_ASSERT_RC1(ht);
 	ZEND_ASSERT(HT_FLAGS(ht) & HASH_FLAG_UNINITIALIZED);
 	if (packed) {
-		zend_hash_real_init_packed_ex(ht);
+		return zend_hash_real_finit_packed_ex(ht, fallible);
 	} else {
-		zend_hash_real_init_mixed_ex(ht);
+		return zend_hash_real_finit_mixed_ex(ht, fallible);
 	}
 }

+static zend_always_inline void zend_hash_real_init_ex(HashTable *ht, bool packed)
+{
+	zend_hash_real_finit_ex(ht, packed, /* fallible */ false);
+}
+
 static const uint32_t uninitialized_bucket[-HT_MIN_MASK] =
 	{HT_INVALID_IDX, HT_INVALID_IDX};

@@ -249,7 +283,7 @@ ZEND_API const HashTable zend_empty_array = {
 	.pDestructor = ZVAL_PTR_DTOR
 };

-static zend_always_inline void _zend_hash_init_int(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor, bool persistent)
+static zend_always_inline zend_result _zend_hash_finit_int(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor, bool persistent, bool fallible)
 {
 	GC_SET_REFCOUNT(ht, 1);
 	GC_TYPE_INFO(ht) = GC_ARRAY | (persistent ? ((GC_PERSISTENT|GC_NOT_COLLECTABLE) << GC_FLAGS_SHIFT) : 0);
@@ -261,7 +295,16 @@ static zend_always_inline void _zend_hash_init_int(HashTable *ht, uint32_t nSize
 	ht->nInternalPointer = 0;
 	ht->nNextFreeElement = ZEND_LONG_MIN;
 	ht->pDestructor = pDestructor;
-	ht->nTableSize = zend_hash_check_size(nSize);
+	ht->nTableSize = zend_hash_check_size(nSize, fallible);
+	if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!ht->nTableSize)) {
+		return FAILURE;
+	}
+	return SUCCESS;
+}
+
+static zend_always_inline void _zend_hash_init_int(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor, bool persistent)
+{
+	_zend_hash_finit_int(ht, nSize, pDestructor, persistent, /* fallible */ false);
 }

 ZEND_API void ZEND_FASTCALL _zend_hash_init(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor, bool persistent)
@@ -276,6 +319,26 @@ ZEND_API HashTable* ZEND_FASTCALL _zend_new_array_0(void)
 	return ht;
 }

+ZEND_API HashTable* ZEND_FASTCALL _zend_fnew_array_0(void)
+{
+	HashTable *ht = femalloc(sizeof(HashTable), /* fallible */ true);
+	if (UNEXPECTED(ht == NULL)) {
+		return NULL;
+	}
+	if (UNEXPECTED(_zend_hash_finit_int(ht, HT_MIN_SIZE, ZVAL_PTR_DTOR, /* persistent */ false, /* fallible */ true) == FAILURE)) {
+		efree(ht);
+		return NULL;
+	}
+	return ht;
+}
+
+ZEND_API HashTable* ZEND_FASTCALL zend_new_array_singleton(void)
+{
+	HashTable *ht = _zend_new_array_0();
+	ht->nTableSize = 1;
+	return ht;
+}
+
 ZEND_API HashTable* ZEND_FASTCALL _zend_new_array(uint32_t nSize)
 {
 	HashTable *ht = emalloc(sizeof(HashTable));
@@ -283,6 +346,20 @@ ZEND_API HashTable* ZEND_FASTCALL _zend_new_array(uint32_t nSize)
 	return ht;
 }

+ZEND_API HashTable* ZEND_FASTCALL _zend_fnew_array(uint32_t nSize)
+{
+	HashTable *ht = femalloc(sizeof(HashTable), /* fallible */ true);
+	if (UNEXPECTED(ht == NULL)) {
+		return NULL;
+	}
+
+	if (UNEXPECTED(_zend_hash_finit_int(ht, nSize, ZVAL_PTR_DTOR, /* persistent */ false, /* fallible */ true) == FAILURE)) {
+		efree(ht);
+		return NULL;
+	}
+	return ht;
+}
+
 ZEND_API HashTable* ZEND_FASTCALL zend_new_pair(zval *val1, zval *val2)
 {
 	zval *zv;
@@ -298,15 +375,26 @@ ZEND_API HashTable* ZEND_FASTCALL zend_new_pair(zval *val1, zval *val2)
 	return ht;
 }

-ZEND_API void ZEND_FASTCALL zend_hash_packed_grow(HashTable *ht)
+ZEND_API zend_result ZEND_FASTCALL zend_hash_packed_fgrow(HashTable *ht, bool fallible)
 {
 	HT_ASSERT_RC1(ht);
 	if (ht->nTableSize >= HT_MAX_SIZE) {
-		zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (%u * %zu + %zu)", ht->nTableSize * 2, sizeof(Bucket), sizeof(Bucket));
+		zend_mm_delayed_error_or_panic(fallible, "Possible integer overflow in memory allocation (%u * %zu + %zu)", ht->nTableSize * 2, sizeof(Bucket), sizeof(Bucket));
+		return FAILURE;
 	}
 	uint32_t newTableSize = ht->nTableSize * 2;
-	HT_SET_DATA_ADDR(ht, perealloc2(HT_GET_DATA_ADDR(ht), HT_PACKED_SIZE_EX(newTableSize, HT_MIN_MASK), HT_PACKED_USED_SIZE(ht), GC_FLAGS(ht) & IS_ARRAY_PERSISTENT));
+	void *newData = fperealloc2(HT_GET_DATA_ADDR(ht), HT_PACKED_SIZE_EX(newTableSize, HT_MIN_MASK), HT_PACKED_USED_SIZE(ht), GC_FLAGS(ht) & IS_ARRAY_PERSISTENT, fallible);
+	if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!newData)) {
+		return FAILURE;
+	}
+	HT_SET_DATA_ADDR(ht, newData);
 	ht->nTableSize = newTableSize;
+	return SUCCESS;
+}
+
+ZEND_API void ZEND_FASTCALL zend_hash_packed_grow(HashTable *ht)
+{
+	zend_hash_packed_fgrow(ht, /* fallible */ false);
 }

 ZEND_API void ZEND_FASTCALL zend_hash_real_init(HashTable *ht, bool packed)
@@ -317,6 +405,14 @@ ZEND_API void ZEND_FASTCALL zend_hash_real_init(HashTable *ht, bool packed)
 	zend_hash_real_init_ex(ht, packed);
 }

+ZEND_API zend_result ZEND_FASTCALL zend_hash_real_finit(HashTable *ht, bool packed, bool fallible)
+{
+	IS_CONSISTENT(ht);
+
+	HT_ASSERT_RC1(ht);
+	return zend_hash_real_finit_ex(ht, packed, fallible);
+}
+
 ZEND_API void ZEND_FASTCALL zend_hash_real_init_packed(HashTable *ht)
 {
 	IS_CONSISTENT(ht);
@@ -325,6 +421,15 @@ ZEND_API void ZEND_FASTCALL zend_hash_real_init_packed(HashTable *ht)
 	zend_hash_real_init_packed_ex(ht);
 }

+/* Fallible variant of zend_hash_real_init_packed */
+ZEND_API zend_result ZEND_FASTCALL zend_hash_real_finit_packed(HashTable *ht, bool fallible)
+{
+	IS_CONSISTENT(ht);
+
+	HT_ASSERT_RC1(ht);
+	return zend_hash_real_finit_packed_ex(ht, fallible);
+}
+
 ZEND_API void ZEND_FASTCALL zend_hash_real_init_mixed(HashTable *ht)
 {
 	IS_CONSISTENT(ht);
@@ -333,7 +438,16 @@ ZEND_API void ZEND_FASTCALL zend_hash_real_init_mixed(HashTable *ht)
 	zend_hash_real_init_mixed_ex(ht);
 }

-ZEND_API void ZEND_FASTCALL zend_hash_packed_to_hash(HashTable *ht)
+/* Fallible variant of zend_hash_real_init_mixed */
+ZEND_API zend_result ZEND_FASTCALL zend_hash_real_finit_mixed(HashTable *ht, bool fallible)
+{
+	IS_CONSISTENT(ht);
+
+	HT_ASSERT_RC1(ht);
+	return zend_hash_real_finit_mixed_ex(ht, fallible);
+}
+
+ZEND_API zend_result ZEND_FASTCALL zend_hash_fconvert_packed_to_hash(HashTable *ht, bool fallible)
 {
 	void *new_data, *old_data = HT_GET_DATA_ADDR(ht);
 	zval *src = ht->arPacked;
@@ -345,7 +459,10 @@ ZEND_API void ZEND_FASTCALL zend_hash_packed_to_hash(HashTable *ht)

 	HT_ASSERT_RC1(ht);
 	// Alloc before assign to avoid inconsistencies on OOM
-	new_data = pemalloc(HT_SIZE_EX(nSize, HT_SIZE_TO_MASK(nSize)), GC_FLAGS(ht) & IS_ARRAY_PERSISTENT);
+	new_data = fpemalloc(HT_SIZE_EX(nSize, HT_SIZE_TO_MASK(nSize)), GC_FLAGS(ht) & IS_ARRAY_PERSISTENT, fallible);
+	if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!new_data)) {
+		return FAILURE;
+	}
 	HT_FLAGS(ht) &= ~HASH_FLAG_PACKED;
 	ht->nTableMask = HT_SIZE_TO_MASK(ht->nTableSize);
 	HT_SET_DATA_ADDR(ht, new_data);
@@ -359,6 +476,12 @@ ZEND_API void ZEND_FASTCALL zend_hash_packed_to_hash(HashTable *ht)
 	}
 	pefree(old_data, GC_FLAGS(ht) & IS_ARRAY_PERSISTENT);
 	zend_hash_rehash(ht);
+	return SUCCESS;
+}
+
+ZEND_API void ZEND_FASTCALL zend_hash_packed_to_hash(HashTable *ht)
+{
+	zend_hash_fconvert_packed_to_hash(ht, /* fallible */ false);
 }

 ZEND_API void ZEND_FASTCALL zend_hash_to_packed(HashTable *ht)
@@ -383,25 +506,39 @@ ZEND_API void ZEND_FASTCALL zend_hash_to_packed(HashTable *ht)
 	pefree(old_data, GC_FLAGS(ht) & IS_ARRAY_PERSISTENT);
 }

-ZEND_API void ZEND_FASTCALL zend_hash_extend(HashTable *ht, uint32_t nSize, bool packed)
+static zend_always_inline zend_result ZEND_FASTCALL _zend_hash_fextend(HashTable *ht, uint32_t nSize, bool packed, bool fallible)
 {
 	HT_ASSERT_RC1(ht);

-	if (nSize == 0) return;
+	if (nSize == 0) return SUCCESS;

 	ZEND_ASSERT(HT_SIZE_TO_MASK(nSize));

 	if (UNEXPECTED(HT_FLAGS(ht) & HASH_FLAG_UNINITIALIZED)) {
 		if (nSize > ht->nTableSize) {
-			ht->nTableSize = zend_hash_check_size(nSize);
+			uint32_t newTableSize = zend_hash_check_size(nSize, fallible);
+			if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!newTableSize)) {
+				return FAILURE;
+			}
+			ht->nTableSize = newTableSize;
+		}
+		zend_result status = zend_hash_real_finit(ht, packed, fallible);
+		if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(status == FAILURE)) {
+			return FAILURE;
 		}
-		zend_hash_real_init(ht, packed);
 	} else {
 		if (packed) {
 			ZEND_ASSERT(HT_IS_PACKED(ht));
 			if (nSize > ht->nTableSize) {
-				uint32_t newTableSize = zend_hash_check_size(nSize);
-				HT_SET_DATA_ADDR(ht, perealloc2(HT_GET_DATA_ADDR(ht), HT_PACKED_SIZE_EX(newTableSize, HT_MIN_MASK), HT_PACKED_USED_SIZE(ht), GC_FLAGS(ht) & IS_ARRAY_PERSISTENT));
+				uint32_t newTableSize = zend_hash_check_size(nSize, fallible);
+				if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!newTableSize)) {
+					return FAILURE;
+				}
+				void *new_data = fperealloc2(HT_GET_DATA_ADDR(ht), HT_PACKED_SIZE_EX(newTableSize, HT_MIN_MASK), HT_PACKED_USED_SIZE(ht), GC_FLAGS(ht) & IS_ARRAY_PERSISTENT, fallible);
+				if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!new_data)) {
+					return FAILURE;
+				}
+				HT_SET_DATA_ADDR(ht, new_data);
 				ht->nTableSize = newTableSize;
 			}
 		} else {
@@ -409,8 +546,14 @@ ZEND_API void ZEND_FASTCALL zend_hash_extend(HashTable *ht, uint32_t nSize, bool
 			if (nSize > ht->nTableSize) {
 				void *new_data, *old_data = HT_GET_DATA_ADDR(ht);
 				Bucket *old_buckets = ht->arData;
-				nSize = zend_hash_check_size(nSize);
-				new_data = pemalloc(HT_SIZE_EX(nSize, HT_SIZE_TO_MASK(nSize)), GC_FLAGS(ht) & IS_ARRAY_PERSISTENT);
+				nSize = zend_hash_check_size(nSize, fallible);
+				if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!nSize)) {
+					return FAILURE;
+				}
+				new_data = fpemalloc(HT_SIZE_EX(nSize, HT_SIZE_TO_MASK(nSize)), GC_FLAGS(ht) & IS_ARRAY_PERSISTENT, fallible);
+				if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!new_data)) {
+					return FAILURE;
+				}
 				ht->nTableSize = nSize;
 				ht->nTableMask = HT_SIZE_TO_MASK(ht->nTableSize);
 				HT_SET_DATA_ADDR(ht, new_data);
@@ -420,6 +563,18 @@ ZEND_API void ZEND_FASTCALL zend_hash_extend(HashTable *ht, uint32_t nSize, bool
 			}
 		}
 	}
+
+	return SUCCESS;
+}
+
+ZEND_API zend_result ZEND_FASTCALL zend_hash_fextend(HashTable *ht, uint32_t nSize, bool packed)
+{
+	return _zend_hash_fextend(ht, nSize, packed, /* fallible */ true);
+}
+
+ZEND_API void ZEND_FASTCALL zend_hash_extend(HashTable *ht, uint32_t nSize, bool packed)
+{
+	_zend_hash_fextend(ht, nSize, packed, /* fallible */ false);
 }

 ZEND_API void ZEND_FASTCALL zend_hash_discard(HashTable *ht, uint32_t nNumUsed)
@@ -759,10 +914,16 @@ static zend_always_inline zval *_zend_hash_add_or_update_i(HashTable *ht, zend_s

 	if (UNEXPECTED(HT_FLAGS(ht) & (HASH_FLAG_UNINITIALIZED|HASH_FLAG_PACKED))) {
 		if (EXPECTED(HT_FLAGS(ht) & HASH_FLAG_UNINITIALIZED)) {
-			zend_hash_real_init_mixed(ht);
+			zend_result status = zend_hash_real_finit_mixed(ht, flag & HASH_FALLIBLE);
+			if (ZEND_CONST_COND(flag & HASH_FALLIBLE, true) && UNEXPECTED(status == FAILURE)) {
+				return HASH_NOMEM;
+			}
 			goto add_to_hash;
 		} else {
-			zend_hash_packed_to_hash(ht);
+			zend_result status = zend_hash_fconvert_packed_to_hash(ht, flag & HASH_FALLIBLE);
+			if (ZEND_CONST_COND(flag & HASH_FALLIBLE, true) && UNEXPECTED(status == FAILURE)) {
+				return HASH_NOMEM;
+			}
 		}
 	} else if ((flag & HASH_ADD_NEW) == 0 || ZEND_DEBUG) {
 		p = zend_hash_find_bucket(ht, key);
@@ -931,6 +1092,11 @@ ZEND_API zval* ZEND_FASTCALL zend_hash_update(HashTable *ht, zend_string *key, z
 	return _zend_hash_add_or_update_i(ht, key, pData, HASH_UPDATE);
 }

+ZEND_API zval* ZEND_FASTCALL zend_hash_fupdate(HashTable *ht, zend_string *key, zval *pData)
+{
+	return _zend_hash_add_or_update_i(ht, key, pData, HASH_UPDATE | HASH_FALLIBLE);
+}
+
 ZEND_API zval* ZEND_FASTCALL zend_hash_update_ind(HashTable *ht, zend_string *key, zval *pData)
 {
 	return _zend_hash_add_or_update_i(ht, key, pData, HASH_UPDATE | HASH_UPDATE_INDIRECT);
@@ -941,11 +1107,21 @@ ZEND_API zval* ZEND_FASTCALL zend_hash_add_new(HashTable *ht, zend_string *key,
 	return _zend_hash_add_or_update_i(ht, key, pData, HASH_ADD_NEW);
 }

+ZEND_API zval* ZEND_FASTCALL zend_hash_fadd_new(HashTable *ht, zend_string *key, zval *pData)
+{
+	return _zend_hash_add_or_update_i(ht, key, pData, HASH_ADD_NEW | HASH_FALLIBLE);
+}
+
 ZEND_API zval* ZEND_FASTCALL zend_hash_lookup(HashTable *ht, zend_string *key)
 {
 	return _zend_hash_add_or_update_i(ht, key, NULL, HASH_LOOKUP);
 }

+ZEND_API zval* ZEND_FASTCALL zend_hash_flookup(HashTable *ht, zend_string *key)
+{
+	return _zend_hash_add_or_update_i(ht, key, NULL, HASH_LOOKUP | HASH_FALLIBLE);
+}
+
 ZEND_API zval* ZEND_FASTCALL zend_hash_str_add_or_update(HashTable *ht, const char *str, size_t len, zval *pData, uint32_t flag)
 {
 	if (flag == HASH_ADD) {
@@ -1044,7 +1220,10 @@ static zend_always_inline zval *_zend_hash_index_add_or_update_i(HashTable *ht,
 				ZVAL_COPY_VALUE(zv, pData);
 				return zv;
 			} else { /* we have to keep the order :( */
-				goto convert_to_hash;
+				zend_result status = zend_hash_fconvert_packed_to_hash(ht, flag & HASH_FALLIBLE);
+				if (ZEND_CONST_COND(flag & HASH_FALLIBLE, true) && UNEXPECTED(status == FAILURE)) {
+					return HASH_NOMEM;
+				}
 			}
 		} else if (EXPECTED(h < ht->nTableSize)) {
 add_to_packed:
@@ -1070,21 +1249,34 @@ static zend_always_inline zval *_zend_hash_index_add_or_update_i(HashTable *ht,
 			return zv;
 		} else if ((h >> 1) < ht->nTableSize &&
 		           (ht->nTableSize >> 1) < ht->nNumOfElements) {
-			zend_hash_packed_grow(ht);
+			zend_result status = zend_hash_packed_fgrow(ht, flag & HASH_FALLIBLE);
+			if (ZEND_CONST_COND(flag & HASH_FALLIBLE, true) && UNEXPECTED(status == FAILURE)) {
+				return HASH_NOMEM;
+			}
 			goto add_to_packed;
 		} else {
+			uint32_t origSize = ht->nTableSize;
 			if (ht->nNumUsed >= ht->nTableSize) {
 				ht->nTableSize += ht->nTableSize;
 			}
-convert_to_hash:
-			zend_hash_packed_to_hash(ht);
+			zend_result status = zend_hash_fconvert_packed_to_hash(ht, flag & HASH_FALLIBLE);
+			if (ZEND_CONST_COND(flag & HASH_FALLIBLE, true) && UNEXPECTED(status == FAILURE)) {
+				ht->nTableSize = origSize;
+				return HASH_NOMEM;
+			}
 		}
 	} else if (HT_FLAGS(ht) & HASH_FLAG_UNINITIALIZED) {
 		if (h < ht->nTableSize) {
-			zend_hash_real_init_packed_ex(ht);
+			zend_result status = zend_hash_real_finit_packed_ex(ht, flag & HASH_FALLIBLE);
+			if (ZEND_CONST_COND(flag & HASH_FALLIBLE, true) && UNEXPECTED(status == FAILURE)) {
+				return HASH_NOMEM;
+			}
 			goto add_to_packed;
 		}
-		zend_hash_real_init_mixed(ht);
+		zend_result status = zend_hash_real_finit_mixed(ht, flag & HASH_FALLIBLE);
+		if (ZEND_CONST_COND(flag & HASH_FALLIBLE, true) && UNEXPECTED(status == FAILURE)) {
+			return HASH_NOMEM;
+		}
 	} else {
 		if ((flag & HASH_ADD_NEW) == 0 || ZEND_DEBUG) {
 			p = zend_hash_index_find_bucket(ht, h);
@@ -1153,21 +1345,41 @@ ZEND_API zval* ZEND_FASTCALL zend_hash_index_update(HashTable *ht, zend_ulong h,
 	return _zend_hash_index_add_or_update_i(ht, h, pData, HASH_UPDATE);
 }

+ZEND_API zval* ZEND_FASTCALL zend_hash_index_fupdate(HashTable *ht, zend_ulong h, zval *pData)
+{
+	return _zend_hash_index_add_or_update_i(ht, h, pData, HASH_UPDATE | HASH_FALLIBLE);
+}
+
 ZEND_API zval* ZEND_FASTCALL zend_hash_next_index_insert(HashTable *ht, zval *pData)
 {
 	return _zend_hash_index_add_or_update_i(ht, ht->nNextFreeElement, pData, HASH_ADD | HASH_ADD_NEXT);
 }

+ZEND_API zval* ZEND_FASTCALL zend_hash_next_index_finsert(HashTable *ht, zval *pData)
+{
+	return _zend_hash_index_add_or_update_i(ht, ht->nNextFreeElement, pData, HASH_ADD | HASH_ADD_NEXT | HASH_FALLIBLE);
+}
+
 ZEND_API zval* ZEND_FASTCALL zend_hash_next_index_insert_new(HashTable *ht, zval *pData)
 {
 	return _zend_hash_index_add_or_update_i(ht, ht->nNextFreeElement, pData, HASH_ADD | HASH_ADD_NEW | HASH_ADD_NEXT);
 }

+ZEND_API zval* ZEND_FASTCALL zend_hash_next_index_finsert_new(HashTable *ht, zval *pData)
+{
+	return _zend_hash_index_add_or_update_i(ht, ht->nNextFreeElement, pData, HASH_ADD | HASH_ADD_NEW | HASH_ADD_NEXT | HASH_FALLIBLE);
+}
+
 ZEND_API zval* ZEND_FASTCALL zend_hash_index_lookup(HashTable *ht, zend_ulong h)
 {
 	return _zend_hash_index_add_or_update_i(ht, h, NULL, HASH_LOOKUP);
 }

+ZEND_API zval* ZEND_FASTCALL zend_hash_index_flookup(HashTable *ht, zend_ulong h)
+{
+	return _zend_hash_index_add_or_update_i(ht, h, NULL, HASH_LOOKUP | HASH_FALLIBLE);
+}
+
 ZEND_API zval* ZEND_FASTCALL zend_hash_set_bucket_key(HashTable *ht, Bucket *b, zend_string *key)
 {
 	uint32_t nIndex;
@@ -1229,9 +1441,8 @@ ZEND_API zval* ZEND_FASTCALL zend_hash_set_bucket_key(HashTable *ht, Bucket *b,
 	return &b->val;
 }

-static void ZEND_FASTCALL zend_hash_do_resize(HashTable *ht)
+static zend_result ZEND_FASTCALL zend_hash_do_fresize(HashTable *ht, bool fallible)
 {
-
 	IS_CONSISTENT(ht);
 	HT_ASSERT_RC1(ht);

@@ -1245,7 +1456,10 @@ static void ZEND_FASTCALL zend_hash_do_resize(HashTable *ht)

 		ZEND_ASSERT(HT_SIZE_TO_MASK(nSize));

-		new_data = pemalloc(HT_SIZE_EX(nSize, HT_SIZE_TO_MASK(nSize)), GC_FLAGS(ht) & IS_ARRAY_PERSISTENT);
+		new_data = fpemalloc(HT_SIZE_EX(nSize, HT_SIZE_TO_MASK(nSize)), GC_FLAGS(ht) & IS_ARRAY_PERSISTENT, fallible);
+		if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!new_data)) {
+			return FAILURE;
+		}
 		ht->nTableSize = nSize;
 		ht->nTableMask = HT_SIZE_TO_MASK(ht->nTableSize);
 		HT_SET_DATA_ADDR(ht, new_data);
@@ -1253,8 +1467,10 @@ static void ZEND_FASTCALL zend_hash_do_resize(HashTable *ht)
 		pefree(old_data, GC_FLAGS(ht) & IS_ARRAY_PERSISTENT);
 		zend_hash_rehash(ht);
 	} else {
-		zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (%u * %zu + %zu)", ht->nTableSize * 2, sizeof(Bucket) + sizeof(uint32_t), sizeof(Bucket));
+		zend_mm_delayed_error_or_panic(fallible, "Possible integer overflow in memory allocation (%u * %zu + %zu)", ht->nTableSize * 2, sizeof(Bucket) + sizeof(uint32_t), sizeof(Bucket));
+		return FAILURE;
 	}
+	return SUCCESS;
 }

 ZEND_API void ZEND_FASTCALL zend_hash_rehash(HashTable *ht)
diff --git a/Zend/zend_hash.h b/Zend/zend_hash.h
index 12a633438b3..5042f7a684a 100644
--- a/Zend/zend_hash.h
+++ b/Zend/zend_hash.h
@@ -34,6 +34,7 @@
 #define HASH_ADD_NEW			(1<<3)
 #define HASH_ADD_NEXT			(1<<4)
 #define HASH_LOOKUP				(1<<5)
+#define HASH_FALLIBLE			(1<<6) /* Use fallible allocation. Return FAILURE or HASH_NOMEM when allocation fails (for functions with zend_result or zval* types, respectively). */

 #define HASH_FLAG_CONSISTENCY      ((1<<0) | (1<<1))
 #define HASH_FLAG_PACKED           (1<<2)
@@ -88,6 +89,7 @@ extern ZEND_API const HashTable zend_empty_array;
 		Z_TYPE_INFO_P(__z) = IS_ARRAY; \
 	} while (0)

+#define HASH_NOMEM ((zval*)-1)

 typedef struct _zend_hash_key {
 	zend_ulong h;
@@ -115,6 +117,11 @@ ZEND_API void ZEND_FASTCALL zend_hash_extend(HashTable *ht, uint32_t nSize, bool
 ZEND_API void ZEND_FASTCALL zend_hash_discard(HashTable *ht, uint32_t nNumUsed);
 ZEND_API void ZEND_FASTCALL zend_hash_packed_grow(HashTable *ht);

+ZEND_API zend_result ZEND_FASTCALL zend_hash_real_finit(HashTable *ht, bool packed, bool fallible);
+ZEND_API zend_result ZEND_FASTCALL zend_hash_real_finit_packed(HashTable *ht, bool fallible);
+ZEND_API zend_result ZEND_FASTCALL zend_hash_real_finit_mixed(HashTable *ht, bool fallible);
+ZEND_API zend_result ZEND_FASTCALL zend_hash_fextend(HashTable *ht, uint32_t nSize, bool packed);
+
 /* additions/updates/changes */
 ZEND_API zval* ZEND_FASTCALL zend_hash_add_or_update(HashTable *ht, zend_string *key, zval *pData, uint32_t flag);
 ZEND_API zval* ZEND_FASTCALL zend_hash_update(HashTable *ht, zend_string *key,zval *pData);
@@ -122,6 +129,9 @@ ZEND_API zval* ZEND_FASTCALL zend_hash_update_ind(HashTable *ht, zend_string *ke
 ZEND_API zval* ZEND_FASTCALL zend_hash_add(HashTable *ht, zend_string *key,zval *pData);
 ZEND_API zval* ZEND_FASTCALL zend_hash_add_new(HashTable *ht, zend_string *key,zval *pData);

+ZEND_API zval* ZEND_FASTCALL zend_hash_fupdate(HashTable *ht, zend_string *key,zval *pData);
+ZEND_API zval* ZEND_FASTCALL zend_hash_fadd_new(HashTable *ht, zend_string *key,zval *pData);
+
 ZEND_API zval* ZEND_FASTCALL zend_hash_str_add_or_update(HashTable *ht, const char *key, size_t len, zval *pData, uint32_t flag);
 ZEND_API zval* ZEND_FASTCALL zend_hash_str_update(HashTable *ht, const char *key, size_t len, zval *pData);
 ZEND_API zval* ZEND_FASTCALL zend_hash_str_update_ind(HashTable *ht, const char *key, size_t len, zval *pData);
@@ -135,6 +145,10 @@ ZEND_API zval* ZEND_FASTCALL zend_hash_index_update(HashTable *ht, zend_ulong h,
 ZEND_API zval* ZEND_FASTCALL zend_hash_next_index_insert(HashTable *ht, zval *pData);
 ZEND_API zval* ZEND_FASTCALL zend_hash_next_index_insert_new(HashTable *ht, zval *pData);

+ZEND_API zval* ZEND_FASTCALL zend_hash_index_fupdate(HashTable *ht, zend_ulong h, zval *pData);
+ZEND_API zval* ZEND_FASTCALL zend_hash_next_index_finsert(HashTable *ht, zval *pData);
+ZEND_API zval* ZEND_FASTCALL zend_hash_next_index_finsert_new(HashTable *ht, zval *pData);
+
 ZEND_API zval* ZEND_FASTCALL zend_hash_index_add_empty_element(HashTable *ht, zend_ulong h);
 ZEND_API zval* ZEND_FASTCALL zend_hash_add_empty_element(HashTable *ht, zend_string *key);
 ZEND_API zval* ZEND_FASTCALL zend_hash_str_add_empty_element(HashTable *ht, const char *key, size_t len);
@@ -214,6 +228,9 @@ static zend_always_inline zval *zend_hash_find_ex(const HashTable *ht, zend_stri
 ZEND_API zval* ZEND_FASTCALL zend_hash_lookup(HashTable *ht, zend_string *key);
 ZEND_API zval* ZEND_FASTCALL zend_hash_index_lookup(HashTable *ht, zend_ulong h);

+ZEND_API zval* ZEND_FASTCALL zend_hash_flookup(HashTable *ht, zend_string *key);
+ZEND_API zval* ZEND_FASTCALL zend_hash_index_flookup(HashTable *ht, zend_ulong h);
+
 #define ZEND_HASH_INDEX_LOOKUP(_ht, _h, _ret) do { \
 		if (EXPECTED(HT_FLAGS(_ht) & HASH_FLAG_PACKED)) { \
 			if (EXPECTED((zend_ulong)(_h) < (zend_ulong)(_ht)->nNumUsed)) { \
@@ -331,12 +348,25 @@ ZEND_API void ZEND_FASTCALL zend_hash_rehash(HashTable *ht);
 	: \
 		_zend_new_array((size)) \
 	)
+# define zend_fnew_array(size) \
+	(__builtin_constant_p(size) ? \
+		((((uint32_t)(size)) <= HT_MIN_SIZE) ? \
+			_zend_fnew_array_0() \
+		: \
+			_zend_fnew_array((size)) \
+		) \
+	: \
+		_zend_fnew_array((size)) \
+	)
 #else
 # define zend_new_array(size) \
 	_zend_new_array(size)
+# define zend_fnew_array(size) \
+	_zend_fnew_array(size)
 #endif

 ZEND_API HashTable* ZEND_FASTCALL _zend_new_array_0(void);
+ZEND_API HashTable* ZEND_FASTCALL zend_new_array_singleton(void);
 ZEND_API HashTable* ZEND_FASTCALL _zend_new_array(uint32_t size);
 ZEND_API HashTable* ZEND_FASTCALL zend_new_pair(zval *val1, zval *val2);
 ZEND_API uint32_t zend_array_count(HashTable *ht);
@@ -347,6 +377,9 @@ ZEND_API void ZEND_FASTCALL zend_symtable_clean(HashTable *ht);
 ZEND_API HashTable* ZEND_FASTCALL zend_symtable_to_proptable(HashTable *ht);
 ZEND_API HashTable* ZEND_FASTCALL zend_proptable_to_symtable(HashTable *ht, bool always_duplicate);

+ZEND_API HashTable* ZEND_FASTCALL _zend_fnew_array_0(void);
+ZEND_API HashTable* ZEND_FASTCALL _zend_fnew_array(uint32_t size);
+
 ZEND_API bool ZEND_FASTCALL _zend_handle_numeric_str_ex(const char *key, size_t length, zend_ulong *idx);

 ZEND_API uint32_t     ZEND_FASTCALL zend_hash_iterator_add(HashTable *ht, HashPosition pos);
@@ -489,6 +522,16 @@ static zend_always_inline zval *zend_symtable_update(HashTable *ht, zend_string
 	}
 }

+static zend_always_inline zval *zend_symtable_fupdate(HashTable *ht, zend_string *key, zval *pData)
+{
+	zend_ulong idx;
+
+	if (ZEND_HANDLE_NUMERIC(key, idx)) {
+		return zend_hash_index_fupdate(ht, idx, pData);
+	} else {
+		return zend_hash_fupdate(ht, key, pData);
+	}
+}

 static zend_always_inline zval *zend_symtable_update_ind(HashTable *ht, zend_string *key, zval *pData)
 {
@@ -501,7 +544,6 @@ static zend_always_inline zval *zend_symtable_update_ind(HashTable *ht, zend_str
 	}
 }

-
 static zend_always_inline zend_result zend_symtable_del(HashTable *ht, zend_string *key)
 {
 	zend_ulong idx;
@@ -809,16 +851,28 @@ static zend_always_inline void *zend_hash_index_add_new_ptr(HashTable *ht, zend_
 	return zv ? Z_PTR_P(zv) : NULL;
 }

-static zend_always_inline void *zend_hash_index_update_ptr(HashTable *ht, zend_ulong h, void *pData)
+static zend_always_inline void *zend_hash_index_fupdate_ptr(HashTable *ht, zend_ulong h, void *pData, bool fallible)
 {
 	zval tmp, *zv;

 	ZVAL_PTR(&tmp, pData);
-	zv = zend_hash_index_update(ht, h, &tmp);
+	if (fallible) {
+		zv = zend_hash_index_fupdate(ht, h, &tmp);
+		if (UNEXPECTED(zv == HASH_NOMEM)) {
+			return HASH_NOMEM;
+		}
+	} else {
+		zv = zend_hash_index_update(ht, h, &tmp);
+	}
 	ZEND_ASSUME(Z_PTR_P(zv));
 	return Z_PTR_P(zv);
 }

+static zend_always_inline void *zend_hash_index_update_ptr(HashTable *ht, zend_ulong h, void *pData)
+{
+	return zend_hash_index_fupdate_ptr(ht, h, pData, /* fallible */ false);
+}
+
 static zend_always_inline void *zend_hash_index_add_mem(HashTable *ht, zend_ulong h, void *pData, size_t size)
 {
 	zval tmp, *zv;
diff --git a/Zend/zend_objects.c b/Zend/zend_objects.c
index aeaecac539a..8e35e56bf0d 100644
--- a/Zend/zend_objects.c
+++ b/Zend/zend_objects.c
@@ -96,6 +96,10 @@ ZEND_API void zend_objects_destroy_object(zend_object *object)
 	zend_function *destructor = object->ce->destructor;

 	if (destructor) {
+		if (ZEND_HAS_OOM()) {
+			return;
+		}
+
 		zend_object *old_exception;
 		const zend_op *old_opline_before_exception;

@@ -190,6 +194,18 @@ ZEND_API zend_object* ZEND_FASTCALL zend_objects_new(zend_class_entry *ce)
 	return object;
 }

+ZEND_API zend_object* ZEND_FASTCALL zend_objects_fnew(zend_class_entry *ce)
+{
+	zend_object *object = femalloc(sizeof(zend_object) + zend_object_properties_size(ce), /* fallible */ true);
+	if (UNEXPECTED(!object)) {
+		return NULL;
+	}
+
+	_zend_object_std_init(object, ce);
+	object->handlers = &std_object_handlers;
+	return object;
+}
+
 ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object, zend_object *old_object)
 {
 	if (old_object->ce->default_properties_count) {
diff --git a/Zend/zend_objects.h b/Zend/zend_objects.h
index 91d388154dd..a312d1b3871 100644
--- a/Zend/zend_objects.h
+++ b/Zend/zend_objects.h
@@ -25,6 +25,7 @@
 BEGIN_EXTERN_C()
 ZEND_API void ZEND_FASTCALL zend_object_std_init(zend_object *object, zend_class_entry *ce);
 ZEND_API zend_object* ZEND_FASTCALL zend_objects_new(zend_class_entry *ce);
+ZEND_API zend_object* ZEND_FASTCALL zend_objects_fnew(zend_class_entry *ce);
 ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object, zend_object *old_object);

 ZEND_API void zend_object_std_dtor(zend_object *object);
diff --git a/Zend/zend_operators.c b/Zend/zend_operators.c
index e1d7141b586..a75bc9499af 100644
--- a/Zend/zend_operators.c
+++ b/Zend/zend_operators.c
@@ -2854,7 +2854,7 @@ ZEND_API char* ZEND_FASTCALL zend_str_toupper_dup_ex(const char *source, size_t
 }
 /* }}} */

-ZEND_API zend_string* ZEND_FASTCALL zend_string_tolower_ex(zend_string *str, bool persistent) /* {{{ */
+static zend_always_inline zend_string* ZEND_FASTCALL _zend_string_ftolower_ex(zend_string *str, bool persistent, bool fallible)
 {
 	size_t length = ZSTR_LEN(str);
 	unsigned char *p = (unsigned char *) ZSTR_VAL(str);
@@ -2865,7 +2865,10 @@ ZEND_API zend_string* ZEND_FASTCALL zend_string_tolower_ex(zend_string *str, boo
 	while (p + BLOCKCONV_STRIDE <= end) {
 		BLOCKCONV_LOAD(p);
 		if (BLOCKCONV_FOUND()) {
-			zend_string *res = zend_string_alloc(length, persistent);
+			zend_string *res = zend_string_falloc(length, persistent, fallible);
+			if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!res)) {
+				return NULL;
+			}
 			memcpy(ZSTR_VAL(res), ZSTR_VAL(str), p - (unsigned char *) ZSTR_VAL(str));
 			unsigned char *q = (unsigned char*) ZSTR_VAL(res) + (p - (unsigned char*) ZSTR_VAL(str));

@@ -2886,7 +2889,10 @@ ZEND_API zend_string* ZEND_FASTCALL zend_string_tolower_ex(zend_string *str, boo

 	while (p < end) {
 		if (*p != zend_tolower_ascii(*p)) {
-			zend_string *res = zend_string_alloc(length, persistent);
+			zend_string *res = zend_string_falloc(length, persistent, fallible);
+			if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!res)) {
+				return NULL;
+			}
 			memcpy(ZSTR_VAL(res), ZSTR_VAL(str), p - (unsigned char*) ZSTR_VAL(str));

 			unsigned char *q = (unsigned char*) ZSTR_VAL(res) + (p - (unsigned char*) ZSTR_VAL(str));
@@ -2901,8 +2907,18 @@ ZEND_API zend_string* ZEND_FASTCALL zend_string_tolower_ex(zend_string *str, boo

 	return zend_string_copy(str);
 }
+
+ZEND_API zend_string* ZEND_FASTCALL zend_string_tolower_ex(zend_string *str, bool persistent) /* {{{ */
+{
+	return _zend_string_ftolower_ex(str, persistent, /* fallible */ false);
+}
 /* }}} */

+ZEND_API zend_string* ZEND_FASTCALL zend_string_ftolower_ex(zend_string *str, bool persistent)
+{
+	return _zend_string_ftolower_ex(str, persistent, /* fallible */ true);
+}
+
 ZEND_API zend_string* ZEND_FASTCALL zend_string_toupper_ex(zend_string *str, bool persistent) /* {{{ */
 {
 	size_t length = ZSTR_LEN(str);
diff --git a/Zend/zend_operators.h b/Zend/zend_operators.h
index ce8b7f5e0a2..2fcff485412 100644
--- a/Zend/zend_operators.h
+++ b/Zend/zend_operators.h
@@ -452,6 +452,8 @@ ZEND_API char*        ZEND_FASTCALL zend_str_toupper_dup_ex(const char *source,
 ZEND_API zend_string* ZEND_FASTCALL zend_string_tolower_ex(zend_string *str, bool persistent);
 ZEND_API zend_string* ZEND_FASTCALL zend_string_toupper_ex(zend_string *str, bool persistent);

+ZEND_API zend_string* ZEND_FASTCALL zend_string_ftolower_ex(zend_string *str, bool persistent);
+
 static zend_always_inline zend_string* zend_string_tolower(zend_string *str) {
 	return zend_string_tolower_ex(str, false);
 }
@@ -459,6 +461,10 @@ static zend_always_inline zend_string* zend_string_toupper(zend_string *str) {
 	return zend_string_toupper_ex(str, false);
 }

+static zend_always_inline zend_string* zend_string_ftolower(zend_string *str) {
+	return zend_string_ftolower_ex(str, false);
+}
+
 ZEND_API int ZEND_FASTCALL zend_binary_zval_strcmp(zval *s1, zval *s2);
 ZEND_API int ZEND_FASTCALL zend_binary_zval_strncmp(zval *s1, zval *s2, zval *s3);
 ZEND_API int ZEND_FASTCALL zend_binary_strcmp(const char *s1, size_t len1, const char *s2, size_t len2);
diff --git a/Zend/zend_smart_str.c b/Zend/zend_smart_str.c
index e6002dcf1d5..cdd9caf134e 100644
--- a/Zend/zend_smart_str.c
+++ b/Zend/zend_smart_str.c
@@ -26,32 +26,72 @@
 #define SMART_STR_NEW_LEN(len) \
 	(ZEND_MM_ALIGNED_SIZE_EX(len + SMART_STR_OVERHEAD, SMART_STR_PAGE) - SMART_STR_OVERHEAD)

-ZEND_API void ZEND_FASTCALL smart_str_erealloc(smart_str *str, size_t len)
+static zend_always_inline bool i_smart_str_erealloc(smart_str *str, size_t len, bool fallible)
 {
 	if (UNEXPECTED(!str->s)) {
-		str->a = len <= SMART_STR_START_LEN
+		size_t a = len <= SMART_STR_START_LEN
 				? SMART_STR_START_LEN
 				: SMART_STR_NEW_LEN(len);
-		str->s = zend_string_alloc(str->a, 0);
+		str->s = zend_string_falloc(a, /* persistent */ false, fallible);
+		if (ZEND_CONST_COND(fallible, true) && !str->s) {
+			return false;
+		}
+		str->a = a;
 		ZSTR_LEN(str->s) = 0;
 	} else {
-		str->a = SMART_STR_NEW_LEN(len);
-		str->s = (zend_string *) erealloc2(str->s, str->a + _ZSTR_HEADER_SIZE + 1, _ZSTR_HEADER_SIZE + ZSTR_LEN(str->s));
+		size_t a = SMART_STR_NEW_LEN(len);
+		zend_string *s = (zend_string *) ferealloc2(str->s, a + _ZSTR_HEADER_SIZE + 1, _ZSTR_HEADER_SIZE + ZSTR_LEN(str->s), fallible);
+		if (ZEND_CONST_COND(fallible, true) && !s) {
+			return false;
+		}
+		str->a = a;
+		str->s = s;
 	}
+	return true;
 }

-ZEND_API void ZEND_FASTCALL smart_str_realloc(smart_str *str, size_t len)
+ZEND_API void ZEND_FASTCALL smart_str_erealloc(smart_str *str, size_t len)
+{
+	i_smart_str_erealloc(str, len, /* fallible */ false);
+}
+
+ZEND_API bool ZEND_FASTCALL smart_str_ferealloc(smart_str *str, size_t len, bool fallible)
+{
+	return i_smart_str_erealloc(str, len, fallible);
+}
+
+static zend_always_inline bool i_smart_str_realloc(smart_str *str, size_t len, bool fallible)
 {
 	if (UNEXPECTED(!str->s)) {
-		str->a = len <= SMART_STR_START_LEN
+		size_t a = len <= SMART_STR_START_LEN
 				? SMART_STR_START_LEN
 				: SMART_STR_NEW_LEN(len);
-		str->s = zend_string_alloc(str->a, 1);
+		str->s = zend_string_falloc(a, /* persistent */ true, fallible);
+		if (ZEND_CONST_COND(fallible, true) && !str->s) {
+			return false;
+		}
+		str->a = a;
 		ZSTR_LEN(str->s) = 0;
 	} else {
-		str->a = SMART_STR_NEW_LEN(len);
-		str->s = (zend_string *) perealloc(str->s, str->a + _ZSTR_HEADER_SIZE + 1, 1);
+		size_t a = SMART_STR_NEW_LEN(len);
+		zend_string *s = (zend_string *) safe_fperealloc(str->s, 1, a, _ZSTR_HEADER_SIZE + 1, /* persistent */ true, fallible);
+		if (ZEND_CONST_COND(fallible, true) && !s) {
+			return false;
+		}
+		str->a = a;
+		str->s = s;
 	}
+	return true;
+}
+
+ZEND_API void ZEND_FASTCALL smart_str_realloc(smart_str *str, size_t len)
+{
+	i_smart_str_realloc(str, len, /* fallible */ false);
+}
+
+ZEND_API bool ZEND_FASTCALL smart_str_frealloc(smart_str *str, size_t len, bool fallible)
+{
+	return i_smart_str_realloc(str, len, fallible);
 }

 /* Windows uses VK_ESCAPE instead of \e */
diff --git a/Zend/zend_smart_str.h b/Zend/zend_smart_str.h
index d8b72905d02..ce799bcd780 100644
--- a/Zend/zend_smart_str.h
+++ b/Zend/zend_smart_str.h
@@ -24,7 +24,9 @@
 BEGIN_EXTERN_C()

 ZEND_API void ZEND_FASTCALL smart_str_erealloc(smart_str *str, size_t len);
+ZEND_API bool ZEND_FASTCALL smart_str_ferealloc(smart_str *str, size_t len, bool fallible);
 ZEND_API void ZEND_FASTCALL smart_str_realloc(smart_str *str, size_t len);
+ZEND_API bool ZEND_FASTCALL smart_str_frealloc(smart_str *str, size_t len, bool fallible);
 ZEND_API void ZEND_FASTCALL smart_str_append_escaped(smart_str *str, const char *s, size_t l);
 /* If zero_fraction is true, then a ".0" will be added to numbers that would not otherwise
  * have a fractional part and look like integers. */
@@ -36,7 +38,7 @@ ZEND_API void ZEND_FASTCALL smart_str_append_escaped_truncated(smart_str *str, z
 ZEND_API void ZEND_FASTCALL smart_str_append_scalar(smart_str *str, zval *value, size_t truncate);
 END_EXTERN_C()

-static zend_always_inline size_t smart_str_alloc(smart_str *str, size_t len, bool persistent) {
+static zend_always_inline size_t smart_str_falloc(smart_str *str, size_t len, bool persistent, bool fallible) {
 	if (UNEXPECTED(!str->s)) {
 		goto do_smart_str_realloc;
 	} else {
@@ -44,22 +46,44 @@ static zend_always_inline size_t smart_str_alloc(smart_str *str, size_t len, boo
 		if (UNEXPECTED(len >= str->a)) {
 do_smart_str_realloc:
 			if (persistent) {
-				smart_str_realloc(str, len);
+				bool success = smart_str_frealloc(str, len, fallible);
+				if (ZEND_CONST_COND(fallible, true) && !success) {
+					ZEND_ASSERT(fallible);
+					return (size_t)-1;
+				}
 			} else {
-				smart_str_erealloc(str, len);
+				bool success = smart_str_ferealloc(str, len, fallible);
+				if (ZEND_CONST_COND(fallible, true) && !success) {
+					ZEND_ASSERT(fallible);
+					return (size_t)-1;
+				}
 			}
 		}
 	}
 	return len;
 }

-static zend_always_inline char* smart_str_extend_ex(smart_str *dest, size_t len, bool persistent) {
-	size_t new_len = smart_str_alloc(dest, len, persistent);
+static zend_always_inline size_t smart_str_alloc(smart_str *str, size_t len, bool persistent) {
+	size_t result = smart_str_falloc(str, len, persistent, /* fallible */ false);
+	ZEND_ASSERT(result != (size_t)-1);
+	return result;
+}
+
+static zend_always_inline char* smart_str_fextend_ex(smart_str *dest, size_t len, bool persistent, bool fallible) {
+	size_t new_len = smart_str_falloc(dest, len, persistent, fallible);
+	if (ZEND_CONST_COND(fallible, true) && new_len == (size_t)-1) {
+		return NULL;
+	}
+	ZEND_ASSERT(new_len != (size_t)-1);
 	char *ret = ZSTR_VAL(dest->s) + ZSTR_LEN(dest->s);
 	ZSTR_LEN(dest->s) = new_len;
 	return ret;
 }

+static zend_always_inline char* smart_str_extend_ex(smart_str *dest, size_t len, bool persistent) {
+	return smart_str_fextend_ex(dest, len, persistent, /* fallible */ false);
+}
+
 static zend_always_inline char* smart_str_extend(smart_str *dest, size_t length)
 {
 	return smart_str_extend_ex(dest, length, false);
@@ -125,10 +149,19 @@ static zend_always_inline void smart_str_appendc_ex(smart_str *dest, char ch, bo
 	ZSTR_LEN(dest->s) = new_len;
 }

-static zend_always_inline void smart_str_appendl_ex(smart_str *dest, const char *str, size_t len, bool persistent) {
-	size_t new_len = smart_str_alloc(dest, len, persistent);
+static zend_always_inline bool smart_str_fappendl_ex(smart_str *dest, const char *str, size_t len, bool persistent, bool fallible) {
+	size_t new_len = smart_str_falloc(dest, len, persistent, fallible);
+	if (ZEND_CONST_COND(fallible, true) && new_len == (size_t)-1) {
+		return false;
+	}
+	ZEND_ASSERT(new_len != (size_t)-1);
 	memcpy(ZSTR_VAL(dest->s) + ZSTR_LEN(dest->s), str, len);
 	ZSTR_LEN(dest->s) = new_len;
+	return true;
+}
+
+static zend_always_inline void smart_str_appendl_ex(smart_str *dest, const char *str, size_t len, bool persistent) {
+	smart_str_fappendl_ex(dest, str, len, persistent, /* fallible */ false);
 }

 static zend_always_inline void smart_str_append_ex(smart_str *dest, const zend_string *src, bool persistent) {
diff --git a/Zend/zend_string.c b/Zend/zend_string.c
index c90af6e2888..5952a68bb96 100644
--- a/Zend/zend_string.c
+++ b/Zend/zend_string.c
@@ -129,6 +129,18 @@ ZEND_API void zend_interned_strings_dtor(void)
 	zend_known_strings = NULL;
 }

+ZEND_API zend_string* ZEND_FASTCALL zend_string_finit_existing_interned(const char *str, size_t len, bool permanent, bool fallible)
+{
+	bool was_fallible = CG(interned_strings_fallible);
+	CG(interned_strings_fallible) = fallible;
+
+	zend_string *ret = zend_string_init_existing_interned(str, len, permanent);
+
+	CG(interned_strings_fallible) = was_fallible;
+
+	return ret;
+}
+
 static zend_always_inline zend_string *zend_interned_string_ht_lookup_ex(zend_ulong h, const char *str, size_t size, HashTable *interned_strings)
 {
 	uint32_t nIndex;
@@ -284,7 +296,10 @@ static zend_string* ZEND_FASTCALL zend_string_init_existing_interned_permanent(c
 	}

 	ZEND_ASSERT(permanent);
-	ret = zend_string_init(str, size, permanent);
+	ret = zend_string_finit(str, size, permanent, CG(interned_strings_fallible));
+	if (UNEXPECTED(!ret)) {
+		return NULL;
+	}
 	ZSTR_H(ret) = h;
 	return ret;
 }
@@ -333,7 +348,10 @@ static zend_string* ZEND_FASTCALL zend_string_init_existing_interned_request(con
 	}

 	ZEND_ASSERT(!permanent);
-	ret = zend_string_init(str, size, permanent);
+	ret = zend_string_finit(str, size, permanent, CG(interned_strings_fallible));
+	if (UNEXPECTED(!ret)) {
+		return NULL;
+	}
 	ZSTR_H(ret) = h;
 	return ret;
 }
diff --git a/Zend/zend_string.h b/Zend/zend_string.h
index df31a636561..aa88c188d78 100644
--- a/Zend/zend_string.h
+++ b/Zend/zend_string.h
@@ -36,6 +36,7 @@ ZEND_API extern zend_string_init_existing_interned_func_t zend_string_init_exist
 ZEND_API zend_ulong ZEND_FASTCALL zend_string_hash_func(zend_string *str);
 ZEND_API zend_ulong ZEND_FASTCALL zend_hash_func(const char *str, size_t len);
 ZEND_API zend_string* ZEND_FASTCALL zend_interned_string_find_permanent(zend_string *str);
+ZEND_API zend_string* ZEND_FASTCALL zend_string_finit_existing_interned(const char *str, size_t len, bool permanent, bool fallible);

 ZEND_API zend_string *zend_string_concat2(
 	const char *str1, size_t str1_len,
@@ -147,9 +148,13 @@ static zend_always_inline uint32_t zend_string_delref(zend_string *s)
 	return 1;
 }

-static zend_always_inline zend_string *zend_string_alloc(size_t len, bool persistent)
+/* Fallible variant of zend_string_alloc */
+static zend_always_inline zend_string *zend_string_falloc(size_t len, bool persistent, bool fallible)
 {
-	zend_string *ret = (zend_string *)pemalloc(ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent);
+	zend_string *ret = (zend_string *)fpemalloc(ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent, fallible);
+	if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(ret == NULL)) {
+		return ret;
+	}

 	GC_SET_REFCOUNT(ret, 1);
 	GC_TYPE_INFO(ret) = GC_STRING | ((persistent ? IS_STR_PERSISTENT : 0) << GC_FLAGS_SHIFT);
@@ -158,9 +163,18 @@ static zend_always_inline zend_string *zend_string_alloc(size_t len, bool persis
 	return ret;
 }

-static zend_always_inline zend_string *zend_string_safe_alloc(size_t n, size_t m, size_t l, bool persistent)
+static zend_always_inline zend_string *zend_string_alloc(size_t len, bool persistent)
+{
+	return zend_string_falloc(len, persistent, /* fallible */ false);
+}
+
+/* Fallible variant of zend_string_safe_alloc */
+static zend_always_inline zend_string *zend_string_safe_falloc(size_t n, size_t m, size_t l, bool persistent, bool fallible)
 {
-	zend_string *ret = (zend_string *)safe_pemalloc(n, m, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(l)), persistent);
+	zend_string *ret = (zend_string *)safe_fpemalloc(n, m, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(l)), persistent, fallible);
+	if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(ret == NULL)) {
+		return ret;
+	}

 	GC_SET_REFCOUNT(ret, 1);
 	GC_TYPE_INFO(ret) = GC_STRING | ((persistent ? IS_STR_PERSISTENT : 0) << GC_FLAGS_SHIFT);
@@ -169,6 +183,11 @@ static zend_always_inline zend_string *zend_string_safe_alloc(size_t n, size_t m
 	return ret;
 }

+static zend_always_inline zend_string *zend_string_safe_alloc(size_t n, size_t m, size_t l, bool persistent)
+{
+	return zend_string_safe_falloc(n, m, l, persistent, /* fallible */ false);
+}
+
 static zend_always_inline zend_string *zend_string_init(const char *str, size_t len, bool persistent)
 {
 	zend_string *ret = zend_string_alloc(len, persistent);
@@ -178,10 +197,22 @@ static zend_always_inline zend_string *zend_string_init(const char *str, size_t
 	return ret;
 }

-static zend_always_inline zend_string *zend_string_init_fast(const char *str, size_t len)
+static zend_always_inline zend_string *zend_string_finit(const char *str, size_t len, bool persistent, bool fallible)
+{
+	zend_string *ret = zend_string_safe_falloc(len, 1, 0, persistent, fallible);
+	if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(ret == NULL)) {
+		return NULL;
+	}
+
+	memcpy(ZSTR_VAL(ret), str, len);
+	ZSTR_VAL(ret)[len] = '\0';
+	return ret;
+}
+
+static zend_always_inline zend_string *zend_string_finit_fast(const char *str, size_t len, bool fallible)
 {
 	if (len > 1) {
-		return zend_string_init(str, len, 0);
+		return zend_string_finit(str, len, 0, fallible);
 	} else if (len == 0) {
 		return zend_empty_string;
 	} else /* if (len == 1) */ {
@@ -189,6 +220,11 @@ static zend_always_inline zend_string *zend_string_init_fast(const char *str, si
 	}
 }

+static zend_always_inline zend_string *zend_string_init_fast(const char *str, size_t len)
+{
+	return zend_string_finit_fast(str, len, /* fallible */ false);
+}
+
 static zend_always_inline zend_string *zend_string_copy(zend_string *s)
 {
 	if (!ZSTR_IS_INTERNED(s)) {
@@ -219,19 +255,25 @@ static zend_always_inline zend_string *zend_string_separate(zend_string *s, bool
 	return s;
 }

-static zend_always_inline zend_string *zend_string_realloc(zend_string *s, size_t len, bool persistent)
+static zend_always_inline zend_string *zend_string_frealloc(zend_string *s, size_t len, bool persistent, bool fallible)
 {
 	zend_string *ret;

 	if (!ZSTR_IS_INTERNED(s)) {
 		if (EXPECTED(GC_REFCOUNT(s) == 1)) {
-			ret = (zend_string *)perealloc(s, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent);
+			ret = (zend_string *)safe_fperealloc(s, 1, len, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(0)), persistent, fallible);
+			if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!ret)) {
+				return NULL;
+			}
 			ZSTR_LEN(ret) = len;
 			zend_string_forget_hash_val(ret);
 			return ret;
 		}
 	}
-	ret = zend_string_alloc(len, persistent);
+	ret = zend_string_falloc(len, persistent, fallible);
+	if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!ret)) {
+		return NULL;
+	}
 	memcpy(ZSTR_VAL(ret), ZSTR_VAL(s), MIN(len, ZSTR_LEN(s)) + 1);
 	if (!ZSTR_IS_INTERNED(s)) {
 		GC_DELREF(s);
@@ -239,20 +281,31 @@ static zend_always_inline zend_string *zend_string_realloc(zend_string *s, size_
 	return ret;
 }

-static zend_always_inline zend_string *zend_string_extend(zend_string *s, size_t len, bool persistent)
+static zend_always_inline zend_string *zend_string_realloc(zend_string *s, size_t len, bool persistent)
+{
+	return zend_string_frealloc(s, len, persistent, /* fallible */ false);
+}
+
+static zend_always_inline zend_string *zend_string_fextend(zend_string *s, size_t len, bool persistent, bool fallible)
 {
 	zend_string *ret;

 	ZEND_ASSERT(len >= ZSTR_LEN(s));
 	if (!ZSTR_IS_INTERNED(s)) {
 		if (EXPECTED(GC_REFCOUNT(s) == 1)) {
-			ret = (zend_string *)perealloc(s, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent);
+			ret = (zend_string *)safe_fperealloc(s, 1, len, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(0)), persistent, fallible);
+			if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!ret)) {
+				return NULL;
+			}
 			ZSTR_LEN(ret) = len;
 			zend_string_forget_hash_val(ret);
 			return ret;
 		}
 	}
-	ret = zend_string_alloc(len, persistent);
+	ret = zend_string_falloc(len, persistent, fallible);
+	if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!ret)) {
+		return NULL;
+	}
 	memcpy(ZSTR_VAL(ret), ZSTR_VAL(s), ZSTR_LEN(s) + 1);
 	if (!ZSTR_IS_INTERNED(s)) {
 		GC_DELREF(s);
@@ -260,20 +313,31 @@ static zend_always_inline zend_string *zend_string_extend(zend_string *s, size_t
 	return ret;
 }

-static zend_always_inline zend_string *zend_string_truncate(zend_string *s, size_t len, bool persistent)
+static zend_always_inline zend_string *zend_string_extend(zend_string *s, size_t len, bool persistent)
+{
+	return zend_string_fextend(s, len, persistent, /* fallible */ false);
+}
+
+static zend_always_inline zend_string *zend_string_ftruncate(zend_string *s, size_t len, bool persistent, bool fallible)
 {
 	zend_string *ret;

 	ZEND_ASSERT(len <= ZSTR_LEN(s));
 	if (!ZSTR_IS_INTERNED(s)) {
 		if (EXPECTED(GC_REFCOUNT(s) == 1)) {
-			ret = (zend_string *)perealloc(s, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent);
+			ret = (zend_string *)safe_fperealloc(s, 1, len, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(0)), persistent, fallible);
+			if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!ret)) {
+				return NULL;
+			}
 			ZSTR_LEN(ret) = len;
 			zend_string_forget_hash_val(ret);
 			return ret;
 		}
 	}
-	ret = zend_string_alloc(len, persistent);
+	ret = zend_string_falloc(len, persistent, fallible);
+	if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(!ret)) {
+		return NULL;
+	}
 	memcpy(ZSTR_VAL(ret), ZSTR_VAL(s), len + 1);
 	if (!ZSTR_IS_INTERNED(s)) {
 		GC_DELREF(s);
@@ -281,19 +345,30 @@ static zend_always_inline zend_string *zend_string_truncate(zend_string *s, size
 	return ret;
 }

-static zend_always_inline zend_string *zend_string_safe_realloc(zend_string *s, size_t n, size_t m, size_t l, bool persistent)
+static zend_always_inline zend_string *zend_string_truncate(zend_string *s, size_t len, bool persistent)
+{
+	return zend_string_ftruncate(s, len, persistent, /*fallible */ false);
+}
+
+static zend_always_inline zend_string *zend_string_safe_frealloc(zend_string *s, size_t n, size_t m, size_t l, bool persistent, bool fallible)
 {
 	zend_string *ret;

 	if (!ZSTR_IS_INTERNED(s)) {
 		if (GC_REFCOUNT(s) == 1) {
-			ret = (zend_string *)safe_perealloc(s, n, m, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(l)), persistent);
+			ret = (zend_string *)safe_fperealloc(s, n, m, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(l)), persistent, fallible);
+			if (ZEND_CONST_COND(fallible, true) && !ret) {
+				return NULL;
+			}
 			ZSTR_LEN(ret) = (n * m) + l;
 			zend_string_forget_hash_val(ret);
 			return ret;
 		}
 	}
-	ret = zend_string_safe_alloc(n, m, l, persistent);
+	ret = zend_string_safe_falloc(n, m, l, persistent, fallible);
+	if (ZEND_CONST_COND(fallible, true) && !ret) {
+		return NULL;
+	}
 	memcpy(ZSTR_VAL(ret), ZSTR_VAL(s), MIN((n * m) + l, ZSTR_LEN(s)) + 1);
 	if (!ZSTR_IS_INTERNED(s)) {
 		GC_DELREF(s);
@@ -301,6 +376,11 @@ static zend_always_inline zend_string *zend_string_safe_realloc(zend_string *s,
 	return ret;
 }

+static zend_always_inline zend_string *zend_string_safe_realloc(zend_string *s, size_t n, size_t m, size_t l, bool persistent)
+{
+	return zend_string_safe_frealloc(s, n, m, l, persistent, /* fallible */ false);
+}
+
 static zend_always_inline void zend_string_free(zend_string *s)
 {
 	if (!ZSTR_IS_INTERNED(s)) {
diff --git a/Zend/zend_types.h b/Zend/zend_types.h
index e6505d8b62a..914a1479904 100644
--- a/Zend/zend_types.h
+++ b/Zend/zend_types.h
@@ -467,17 +467,21 @@ struct _zend_array {
 # define HT_HASH_RESET(ht) do { \
 		char *p = (char*)&HT_HASH(ht, (ht)->nTableMask); \
 		size_t size = HT_HASH_SIZE((ht)->nTableMask); \
-		__m128i xmm0 = _mm_setzero_si128(); \
-		xmm0 = _mm_cmpeq_epi8(xmm0, xmm0); \
-		ZEND_ASSERT(size >= 64 && ((size & 0x3f) == 0)); \
-		do { \
-			_mm_storeu_si128((__m128i*)p, xmm0); \
-			_mm_storeu_si128((__m128i*)(p+16), xmm0); \
-			_mm_storeu_si128((__m128i*)(p+32), xmm0); \
-			_mm_storeu_si128((__m128i*)(p+48), xmm0); \
-			p += 64; \
-			size -= 64; \
-		} while (size != 0); \
+		if (UNEXPECTED(size < 64)) { \
+			memset(p, HT_INVALID_IDX, size); \
+		} else { \
+			__m128i xmm0 = _mm_setzero_si128(); \
+			xmm0 = _mm_cmpeq_epi8(xmm0, xmm0); \
+			ZEND_ASSERT(size >= 64 && ((size & 0x3f) == 0)); \
+			do { \
+				_mm_storeu_si128((__m128i*)p, xmm0); \
+				_mm_storeu_si128((__m128i*)(p+16), xmm0); \
+				_mm_storeu_si128((__m128i*)(p+32), xmm0); \
+				_mm_storeu_si128((__m128i*)(p+48), xmm0); \
+				p += 64; \
+				size -= 64; \
+			} while (size != 0); \
+		} \
 	} while (0)
 #else
 # define HT_HASH_RESET(ht) \
diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h
index 26cb95c8f94..5fd5ecf443b 100644
--- a/Zend/zend_vm_def.h
+++ b/Zend/zend_vm_def.h
@@ -3333,6 +3333,7 @@ ZEND_VM_HANDLER(56, ZEND_ROPE_END, TMP, CONST|TMPVAR|CV, NUM)
 	size_t len = 0;
 	char *target;

+	SAVE_OPLINE();
 	rope = (zend_string**)EX_VAR(opline->op1.var);
 	if (OP2_TYPE == IS_CONST) {
 		var = GET_OP2_ZVAL_PTR(BP_VAR_R);
@@ -3951,7 +3952,7 @@ ZEND_VM_HOT_HANDLER(129, ZEND_DO_ICALL, ANY, ANY, SPEC(RETVAL,OBSERVER))
 	fbc->internal_function.handler(call, ret);

 #if ZEND_DEBUG
-	if (!EG(exception) && call->func) {
+	if (!EG(exception) && !ZEND_HAS_OOM() && call->func) {
 		if (should_throw) {
 			zend_internal_call_arginfo_violation(call->func);
 		}
@@ -4072,7 +4073,7 @@ ZEND_VM_HOT_HANDLER(131, ZEND_DO_FCALL_BY_NAME, ANY, ANY, SPEC(RETVAL,OBSERVER))
 		fbc->internal_function.handler(call, ret);

 #if ZEND_DEBUG
-		if (!EG(exception) && call->func) {
+		if (!EG(exception) && !ZEND_HAS_OOM() && call->func) {
 			if (should_throw) {
 				zend_internal_call_arginfo_violation(call->func);
 			}
@@ -4184,7 +4185,7 @@ ZEND_VM_HOT_HANDLER(60, ZEND_DO_FCALL, ANY, ANY, SPEC(RETVAL,OBSERVER))
 		}

 #if ZEND_DEBUG
-		if (!EG(exception) && call->func && !(call->func->common.fn_flags & ZEND_ACC_FAKE_CLOSURE)) {
+		if (!EG(exception) && !ZEND_HAS_OOM() && call->func && !(call->func->common.fn_flags & ZEND_ACC_FAKE_CLOSURE)) {
 			if (should_throw) {
 				zend_internal_call_arginfo_violation(call->func);
 			}
@@ -6179,6 +6180,7 @@ ZEND_VM_HANDLER(71, ZEND_INIT_ARRAY, CONST|TMP|VAR|CV|UNUSED, CONST|TMPVAR|UNUSE
 	uint32_t size;
 	USE_OPLINE

+	SAVE_OPLINE();
 	array = EX_VAR(opline->result.var);
 	if (OP1_TYPE != IS_UNUSED) {
 		size = opline->extended_value >> ZEND_ARRAY_SIZE_SHIFT;
@@ -7620,7 +7622,7 @@ ZEND_VM_HOT_NOCONST_HANDLER(198, ZEND_JMP_NULL, CONST|TMP|VAR|CV, JMP_ADDR)
 	uint32_t short_circuiting_type = opline->extended_value & ZEND_SHORT_CIRCUITING_CHAIN_MASK;
 	if (EXPECTED(short_circuiting_type == ZEND_SHORT_CIRCUITING_CHAIN_EXPR)) {
 		ZVAL_NULL(result);
-		if (OP1_TYPE == IS_CV
+		if (OP1_TYPE == IS_CV
 			&& UNEXPECTED(Z_TYPE_P(val) == IS_UNDEF)
 			&& (opline->extended_value & ZEND_JMP_NULL_BP_VAR_IS) == 0
 		) {
@@ -7933,6 +7935,14 @@ ZEND_VM_HELPER(zend_dispatch_try_catch_finally_helper, ANY, ANY, uint32_t try_ca
 ZEND_VM_HANDLER(149, ZEND_HANDLE_EXCEPTION, ANY, ANY)
 {
 	const zend_op *throw_op = EG(opline_before_exception);
+
+	/* Exception was thrown before executing any op, or between two ops (see
+	 * zend_handle_delayed_oom()). */
+	if (UNEXPECTED(!throw_op)) {
+		free_unfinished_calls(execute_data);
+		ZEND_VM_DISPATCH_TO_HELPER(zend_dispatch_try_catch_finally_helper, try_catch_offset, -1, op_num, -1);
+	}
+
 	uint32_t throw_op_num = throw_op - EX(func)->op_array.opcodes;
 	int i, current_try_catch_offset = -1;

@@ -8777,7 +8787,7 @@ ZEND_VM_HANDLER(158, ZEND_CALL_TRAMPOLINE, ANY, ANY, SPEC(OBSERVER))
 		}

 #if ZEND_DEBUG
-		if (!EG(exception) && call->func) {
+		if (!EG(exception) && !ZEND_HAS_OOM() && call->func) {
 			if (should_throw) {
 				zend_internal_call_arginfo_violation(call->func);
 			}
@@ -9975,6 +9985,9 @@ ZEND_VM_HELPER(zend_interrupt_helper, ANY, ANY)
 	SAVE_OPLINE();
 	if (zend_atomic_bool_load_ex(&EG(timed_out))) {
 		zend_timeout();
+	} else if (ZEND_HAS_OOM()) {
+		zend_handle_delayed_oom();
+		HANDLE_EXCEPTION();
 	} else if (zend_interrupt_function) {
 		zend_interrupt_function(execute_data);
 		if (EG(exception)) {
diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h
index 39bdd2e80b8..0c2938b37d8 100644
Binary files a/Zend/zend_vm_execute.h and b/Zend/zend_vm_execute.h differ
diff --git a/ext/bz2/bz2.c b/ext/bz2/bz2.c
index 8e55bf92bed..706725a71ae 100644
--- a/ext/bz2/bz2.c
+++ b/ext/bz2/bz2.c
@@ -462,7 +462,10 @@ PHP_FUNCTION(bzcompress)
 	dest_len = (unsigned int) (source_len + (0.01 * source_len) + 600);

 	/* Allocate the destination buffer */
-	dest = zend_string_alloc(dest_len, 0);
+	dest = zend_string_falloc(dest_len, 0, /* fallible */ true);
+	if (!dest) {
+		RETURN_OOM();
+	}

 	/* Handle the optional arguments */
 	if (argc > 1) {
@@ -491,7 +494,7 @@ PHP_FUNCTION(bzcompress)
 PHP_FUNCTION(bzdecompress)
 {
 	char *source;
-	zend_string *dest;
+	zend_string *dest = NULL, *tmp;
 	size_t source_len;
 	int error;
 	bool small = 0;
@@ -517,7 +520,10 @@ PHP_FUNCTION(bzdecompress)
 	bzs.avail_in = source_len;

 	/* in most cases bz2 offers at least 2:1 compression, so we use that as our base */
-	dest = zend_string_safe_alloc(source_len, 2, 1, 0);
+	dest = zend_string_safe_falloc(source_len, 2, 1, 0, /* fallible */ true);
+	if (!dest) {
+		goto failure;
+	}
 	bzs.avail_out = source_len * 2;
 	bzs.next_out = ZSTR_VAL(dest);

@@ -531,7 +537,11 @@ PHP_FUNCTION(bzdecompress)
 			break;
 		}
 #endif
-		dest = zend_string_safe_realloc(dest, 1, bzs.avail_out+1, (size_t) size, 0);
+		tmp = zend_string_safe_frealloc(dest, 1, bzs.avail_out+1, (size_t) size, 0, /* fallible */ true);
+		if (!tmp) {
+			goto failure;
+		}
+		dest = tmp;
 		bzs.next_out = ZSTR_VAL(dest) + size;
 	}

@@ -545,7 +555,11 @@ PHP_FUNCTION(bzdecompress)
 		} else
 #endif
 		{
-			dest = zend_string_safe_realloc(dest, 1, (size_t)size, 1, 0);
+			tmp = zend_string_safe_frealloc(dest, 1, (size_t)size, 1, 0, /* fallible */ true);
+			if (!tmp) {
+				goto failure;
+			}
+			dest = tmp;
 			ZSTR_LEN(dest) = (size_t)size;
 			ZSTR_VAL(dest)[(size_t)size] = '\0';
 			RETVAL_STR(dest);
@@ -556,6 +570,13 @@ PHP_FUNCTION(bzdecompress)
 	}

 	BZ2_bzDecompressEnd(&bzs);
+	return;
+
+failure:
+	if (dest) {
+		zend_string_efree(dest);
+	}
+	BZ2_bzDecompressEnd(&bzs);
 }
 /* }}} */

diff --git a/ext/bz2/bz2_filter.c b/ext/bz2/bz2_filter.c
index 9b3480b4a5c..7cef9fa723b 100644
--- a/ext/bz2/bz2_filter.c
+++ b/ext/bz2/bz2_filter.c
@@ -90,6 +90,10 @@ static php_stream_filter_status_t php_bz2_decompress_filter(

 		bucket = php_stream_bucket_make_writeable(buckets_in->head);
 		while (bin < bucket->buflen) {
+			/* Output is unbounded relative to the input. */
+			if (ZEND_HAS_OOM()) {
+				break;
+			}
 			if (data->status == PHP_BZ2_UNINITIALIZED) {
 				status = BZ2_bzDecompressInit(streamp, 0, data->small_footprint);

@@ -155,7 +159,7 @@ static php_stream_filter_status_t php_bz2_decompress_filter(
 	if ((data->status == PHP_BZ2_RUNNING) && (flags & PSFS_FLAG_FLUSH_CLOSE)) {
 		/* Spit it out! */
 		status = BZ_OK;
-		while (status == BZ_OK) {
+		while (status == BZ_OK && !ZEND_HAS_OOM()) {
 			status = BZ2_bzDecompress(&(data->strm));
 			if (data->strm.avail_out < data->outbuf_len) {
 				size_t bucketlen = data->outbuf_len - data->strm.avail_out;
diff --git a/ext/bz2/tests/oom_bzcompress.phpt b/ext/bz2/tests/oom_bzcompress.phpt
new file mode 100644
index 00000000000..a2c9931192d
--- /dev/null
+++ b/ext/bz2/tests/oom_bzcompress.phpt
@@ -0,0 +1,30 @@
+--TEST--
+bzcompress() output buffer pre-allocation doesn't reach hard limit
+--EXTENSIONS--
+bz2
+--INI--
+memory_limit=512M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+$limit = 512 * 1024 * 1024;
+$hard = 2 * $limit;
+
+// Fill the heap to just under the memory limit.
+$subject = str_repeat("a", $limit - memory_get_usage(true) - 64 * 1024);
+
+// bzcompress() pre-allocates source + 1% + 600 bytes for its output, which
+// leaves it about 1% too big for the gap that is left before the hard limit.
+$dest_len = (int) (strlen($subject) + 0.01 * strlen($subject) + 600);
+var_dump($dest_len > $hard - memory_get_usage(true));
+
+bzcompress($subject);
+echo "not reached\n";
+?>
+--EXPECTF--
+bool(true)
+
+Fatal error: Allowed memory size of 536870912 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/bz2/tests/oom_bzdecompress.phpt b/ext/bz2/tests/oom_bzdecompress.phpt
new file mode 100644
index 00000000000..075f0fc6ffe
--- /dev/null
+++ b/ext/bz2/tests/oom_bzdecompress.phpt
@@ -0,0 +1,17 @@
+--TEST--
+bzdecompress() doesn't reach hard limit
+--EXTENSIONS--
+bz2
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv('SKIP_ASAN')) die("skip ASan-instrumented realloc() is slow");
+?>
+--FILE--
+<?php
+// Expands to 256 MB
+bzdecompress(base64_decode('QlpoOTFBWSZTWQ4J4t8BX45AAMAAAAggADCATUZCoCWpCoCXMUFZJlNZDgni3wFfjkAAwAAACCAAMIBNRkKgJakKgJcxQVkmU1kOCeLfAV+OQADAAAAIIAAwgE1GQqAlqQqAlzFBWSZTWQ4J4t8BX45AAMAAAAggADCATUZCoCWpCoCXMUFZJlNZDgni3wFfjkAAwAAACCAAMIBNRkKgJakKgJcxQVkmU1kezuTbASo/wADABAAIIAAwzAUppqqoSRsAIkjxdyRThQkGsXyvAA=='));
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/bz2/tests/oom_bzip2_decompress_filter.phpt b/ext/bz2/tests/oom_bzip2_decompress_filter.phpt
new file mode 100644
index 00000000000..a4ac2ae1175
--- /dev/null
+++ b/ext/bz2/tests/oom_bzip2_decompress_filter.phpt
@@ -0,0 +1,27 @@
+--TEST--
+bzip2.decompress stream filter doesn't reach hard limit
+--EXTENSIONS--
+bz2
+--INI--
+memory_limit=64M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+$file = __DIR__ . "/oom_bzip2_decompress_filter.bz2";
+file_put_contents($file, bzcompress(str_repeat("\0", 12 * 1024 * 1024), 9));
+
+ini_set("memory_limit", "4M");
+
+$fp = fopen($file, "rb");
+stream_filter_append($fp, "bzip2.decompress", STREAM_FILTER_READ);
+var_dump(stream_get_contents($fp));
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/oom_bzip2_decompress_filter.bz2");
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/curl/interface.c b/ext/curl/interface.c
index 1f2fade37db..336b75d1413 100644
--- a/ext/curl/interface.c
+++ b/ext/curl/interface.c
@@ -584,7 +584,13 @@ static size_t curl_write(char *data, size_t size, size_t nmemb, void *ctx)
 			return fwrite(data, size, nmemb, t->fp);
 		case PHP_CURL_RETURN:
 			if (length > 0) {
-				smart_str_appendl(&t->buf, data, (int) length);
+				if (!smart_str_fappendl_ex(&t->buf, data, (int) length, /* persistent */ false, /* fallible */ true)) {
+#ifdef CURL_WRITEFUNC_ERROR
+					return CURL_WRITEFUNC_ERROR;
+#else
+					return 0;
+#endif
+				}
 			}
 			break;
 		case PHP_CURL_USER: {
diff --git a/ext/curl/tests/oom_curl_exec.phpt b/ext/curl/tests/oom_curl_exec.phpt
new file mode 100644
index 00000000000..afbb948fda9
--- /dev/null
+++ b/ext/curl/tests/oom_curl_exec.phpt
@@ -0,0 +1,37 @@
+--TEST--
+curl_exec() doesn't reach hard limit
+--EXTENSIONS--
+curl
+--FILE--
+<?php
+require __DIR__ . '/server.inc';
+$host = curl_cli_server_start();
+
+$extension = ini_get('extension_dir') . '/' . (PHP_OS_FAMILY === 'Windows' ? 'php_' : '') . 'curl.' . PHP_SHLIB_SUFFIX;
+$args = [];
+if (is_file($extension)) {
+    $args[] = '-d';
+    $args[] = "extension=$extension";
+}
+
+$code = <<<'PHP'
+$ch = curl_init($argv[1]);
+curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+curl_exec($ch);
+echo "no OOM\n";
+PHP;
+
+$php = getenv('TEST_PHP_EXECUTABLE') ?: PHP_BINARY;
+$p = proc_open(
+    [$php, '-n', ...$args, '-d', 'memory_limit=8M', '-r', $code, "http://$host/get.inc?test=bigbody&mib=16"],
+    [1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
+    $pipes
+);
+echo stream_get_contents($pipes[1]);
+echo stream_get_contents($pipes[2]);
+fclose($pipes[1]);
+fclose($pipes[2]);
+proc_close($p);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/curl/tests/oom_curl_multi_exec.phpt b/ext/curl/tests/oom_curl_multi_exec.phpt
new file mode 100644
index 00000000000..4a233166e70
--- /dev/null
+++ b/ext/curl/tests/oom_curl_multi_exec.phpt
@@ -0,0 +1,43 @@
+--TEST--
+curl_multi_exec() doesn't reach hard limit
+--EXTENSIONS--
+curl
+--FILE--
+<?php
+require __DIR__ . '/server.inc';
+$host = curl_cli_server_start();
+
+$extension = ini_get('extension_dir') . '/' . (PHP_OS_FAMILY === 'Windows' ? 'php_' : '') . 'curl.' . PHP_SHLIB_SUFFIX;
+$args = [];
+if (is_file($extension)) {
+    $args[] = '-d';
+    $args[] = "extension=$extension";
+}
+
+$code = <<<'PHP'
+$mh = curl_multi_init();
+$ch = curl_init($argv[1]);
+curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+curl_setopt($ch, CURLOPT_BUFFERSIZE, 10 * 1024 * 1024);
+curl_multi_add_handle($mh, $ch);
+do {
+    curl_multi_exec($mh, $running);
+    curl_multi_select($mh, 0.05);
+} while ($running);
+echo "no OOM\n";
+PHP;
+
+$php = getenv('TEST_PHP_EXECUTABLE') ?: PHP_BINARY;
+$p = proc_open(
+    [$php, '-n', ...$args, '-d', 'memory_limit=8M', '-r', $code, "http://$host/get.inc?test=bigbody&mib=16"],
+    [1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
+    $pipes
+);
+echo stream_get_contents($pipes[1]);
+echo stream_get_contents($pipes[2]);
+fclose($pipes[1]);
+fclose($pipes[2]);
+proc_close($p);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/curl/tests/responder/get.inc b/ext/curl/tests/responder/get.inc
index c139c8c7d43..2ae6120c1a4 100644
--- a/ext/curl/tests/responder/get.inc
+++ b/ext/curl/tests/responder/get.inc
@@ -46,6 +46,14 @@
     case 'method':
       echo $_SERVER['REQUEST_METHOD'];
       break;
+    case 'bigbody':
+      $mib = isset($_GET['mib']) ? (int) $_GET['mib'] : 12;
+      header('Content-Type: application/octet-stream');
+      $chunk = str_repeat('x', 1024 * 1024);
+      for ($i = 0; $i < $mib; $i++) {
+        echo $chunk;
+      }
+      break;
     default:
       echo "Hello World!\n";
       echo "Hello World!";
diff --git a/ext/date/lib/parse_date.c b/ext/date/lib/parse_date.c
index 050ca7def28..4d83333532e 100644
--- a/ext/date/lib/parse_date.c
+++ b/ext/date/lib/parse_date.c
@@ -1,4 +1,4 @@
-/* Generated by re2c 1.0.3 on Wed Sep 11 17:29:29 2024 */
+/* Generated by re2c 1.0.3 on Tue Sep 15 16:40:58 2026 */
 #line 1 "ext/date/lib/parse_date.re"
 /*
  * The MIT License (MIT)
@@ -78,6 +78,7 @@
 #define TIMELIB_RELATIVE       310

 #define TIMELIB_ERROR          999
+#define TIMELIB_MAX_ERRORS     1000

 /* Some compilers like AIX, defines uchar in sys/types.h */
 #undef uchar
@@ -365,6 +366,10 @@ static timelib_error_message *alloc_error_message(timelib_error_message **messag

 static void add_warning(Scanner *s, int error_code, const char *error)
 {
+	if (s->errors->warning_count >= TIMELIB_MAX_ERRORS) {
+		return;
+	}
+
 	timelib_error_message *message = alloc_error_message(&s->errors->warning_messages, &s->errors->warning_count);

 	message->error_code = error_code;
@@ -375,6 +380,10 @@ static void add_warning(Scanner *s, int error_code, const char *error)

 static void add_error(Scanner *s, int error_code, const char *error)
 {
+	if (s->errors->error_count >= TIMELIB_MAX_ERRORS) {
+		return;
+	}
+
 	timelib_error_message *message = alloc_error_message(&s->errors->error_messages, &s->errors->error_count);

 	message->error_code = error_code;
@@ -385,6 +394,10 @@ static void add_error(Scanner *s, int error_code, const char *error)

 static void add_pbf_warning(Scanner *s, int error_code, const char *error, const char *sptr, const char *cptr)
 {
+	if (s->errors->warning_count >= TIMELIB_MAX_ERRORS) {
+		return;
+	}
+
 	timelib_error_message *message = alloc_error_message(&s->errors->warning_messages, &s->errors->warning_count);

 	message->error_code = error_code;
@@ -395,6 +408,10 @@ static void add_pbf_warning(Scanner *s, int error_code, const char *error, const

 static void add_pbf_error(Scanner *s, int error_code, const char *error, const char *sptr, const char *cptr)
 {
+	if (s->errors->error_count >= TIMELIB_MAX_ERRORS) {
+		return;
+	}
+
 	timelib_error_message *message = alloc_error_message(&s->errors->error_messages, &s->errors->error_count);

 	message->error_code = error_code;
@@ -1018,11 +1035,11 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 std:
 	s->tok = cursor;
 	s->len = 0;
-#line 1151 "ext/date/lib/parse_date.re"
+#line 1168 "ext/date/lib/parse_date.re"



-#line 1026 "<stdout>"
+#line 1043 "<stdout>"
 {
 	YYCTYPE yych;
 	unsigned int yyaccept = 0;
@@ -1203,23 +1220,23 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	YYDEBUG(2, *YYCURSOR);
 	++YYCURSOR;
 	YYDEBUG(3, *YYCURSOR);
-#line 1984 "ext/date/lib/parse_date.re"
+#line 2001 "ext/date/lib/parse_date.re"
 	{
 		s->pos = cursor; s->line++;
 		goto std;
 	}
-#line 1212 "<stdout>"
+#line 1229 "<stdout>"
 yy4:
 	YYDEBUG(4, *YYCURSOR);
 	++YYCURSOR;
 yy5:
 	YYDEBUG(5, *YYCURSOR);
-#line 1990 "ext/date/lib/parse_date.re"
+#line 2007 "ext/date/lib/parse_date.re"
 	{
 		add_error(s, TIMELIB_ERR_UNEXPECTED_CHARACTER, "Unexpected character");
 		goto std;
 	}
-#line 1223 "<stdout>"
+#line 1240 "<stdout>"
 yy6:
 	YYDEBUG(6, *YYCURSOR);
 	yyaccept = 0;
@@ -1234,11 +1251,11 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	if (yych <= '9') goto yy58;
 yy8:
 	YYDEBUG(8, *YYCURSOR);
-#line 1979 "ext/date/lib/parse_date.re"
+#line 1996 "ext/date/lib/parse_date.re"
 	{
 		goto std;
 	}
-#line 1242 "<stdout>"
+#line 1259 "<stdout>"
 yy9:
 	YYDEBUG(9, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -1272,11 +1289,11 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	YYDEBUG(11, *YYCURSOR);
 	++YYCURSOR;
 	YYDEBUG(12, *YYCURSOR);
-#line 1974 "ext/date/lib/parse_date.re"
+#line 1991 "ext/date/lib/parse_date.re"
 	{
 		goto std;
 	}
-#line 1280 "<stdout>"
+#line 1297 "<stdout>"
 yy13:
 	YYDEBUG(13, *YYCURSOR);
 	yyaccept = 1;
@@ -1777,7 +1794,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy20:
 	YYDEBUG(20, *YYCURSOR);
-#line 1889 "ext/date/lib/parse_date.re"
+#line 1906 "ext/date/lib/parse_date.re"
 	{
 		int tz_not_found;
 		DEBUG_OUTPUT("tzcorrection | tz");
@@ -1791,7 +1808,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_TIMEZONE;
 	}
-#line 1795 "<stdout>"
+#line 1812 "<stdout>"
 yy21:
 	YYDEBUG(21, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -3596,7 +3613,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy81:
 	YYDEBUG(81, *YYCURSOR);
-#line 1636 "ext/date/lib/parse_date.re"
+#line 1653 "ext/date/lib/parse_date.re"
 	{
 		DEBUG_OUTPUT("datenoyearrev");
 		TIMELIB_INIT;
@@ -3607,7 +3624,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_DATE_TEXT;
 	}
-#line 3611 "<stdout>"
+#line 3628 "<stdout>"
 yy82:
 	YYDEBUG(82, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -4122,7 +4139,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 	if (yych == '.') goto yy289;
 	YYDEBUG(114, *YYCURSOR);
-#line 1211 "ext/date/lib/parse_date.re"
+#line 1228 "ext/date/lib/parse_date.re"
 	{
 		timelib_ull i;

@@ -4147,7 +4164,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_RELATIVE;
 	}
-#line 4151 "<stdout>"
+#line 4168 "<stdout>"
 yy115:
 	YYDEBUG(115, *YYCURSOR);
 	++YYCURSOR;
@@ -5873,7 +5890,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy177:
 	YYDEBUG(177, *YYCURSOR);
-#line 1377 "ext/date/lib/parse_date.re"
+#line 1394 "ext/date/lib/parse_date.re"
 	{
 		int tz_not_found;
 		DEBUG_OUTPUT("timetiny24 | timeshort24 | timelong24 | iso8601long");
@@ -5900,7 +5917,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_TIME24_WITH_ZONE;
 	}
-#line 5904 "<stdout>"
+#line 5921 "<stdout>"
 yy178:
 	YYDEBUG(178, *YYCURSOR);
 	yyaccept = 4;
@@ -6929,7 +6946,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy224:
 	YYDEBUG(224, *YYCURSOR);
-#line 1471 "ext/date/lib/parse_date.re"
+#line 1488 "ext/date/lib/parse_date.re"
 	{
 		int length = 0;
 		DEBUG_OUTPUT("americanshort | american");
@@ -6944,7 +6961,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_AMERICAN;
 	}
-#line 6948 "<stdout>"
+#line 6965 "<stdout>"
 yy225:
 	YYDEBUG(225, *YYCURSOR);
 	yyaccept = 5;
@@ -7187,7 +7204,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	if (yych <= '9') goto yy431;
 yy251:
 	YYDEBUG(251, *YYCURSOR);
-#line 1553 "ext/date/lib/parse_date.re"
+#line 1570 "ext/date/lib/parse_date.re"
 	{
 		int length = 0;
 		DEBUG_OUTPUT("datefull");
@@ -7201,7 +7218,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_DATE_FULL;
 	}
-#line 7205 "<stdout>"
+#line 7222 "<stdout>"
 yy252:
 	YYDEBUG(252, *YYCURSOR);
 	yyaccept = 3;
@@ -7315,7 +7332,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	if (yych == 'e') goto yy440;
 yy260:
 	YYDEBUG(260, *YYCURSOR);
-#line 1958 "ext/date/lib/parse_date.re"
+#line 1975 "ext/date/lib/parse_date.re"
 	{
 		timelib_ull i;
 		DEBUG_OUTPUT("relative");
@@ -7330,7 +7347,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_RELATIVE;
 	}
-#line 7334 "<stdout>"
+#line 7351 "<stdout>"
 yy261:
 	YYDEBUG(261, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -7776,7 +7793,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	if (yych <= '9') goto yy471;
 yy290:
 	YYDEBUG(290, *YYCURSOR);
-#line 1237 "ext/date/lib/parse_date.re"
+#line 1254 "ext/date/lib/parse_date.re"
 	{
 		timelib_sll i;
 		timelib_ull us;
@@ -7815,7 +7832,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_RELATIVE;
 	}
-#line 7819 "<stdout>"
+#line 7836 "<stdout>"
 yy291:
 	YYDEBUG(291, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -7840,7 +7857,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy293:
 	YYDEBUG(293, *YYCURSOR);
-#line 1799 "ext/date/lib/parse_date.re"
+#line 1816 "ext/date/lib/parse_date.re"
 	{
 		DEBUG_OUTPUT("ago");
 		TIMELIB_INIT;
@@ -7860,7 +7877,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_AGO;
 	}
-#line 7864 "<stdout>"
+#line 7881 "<stdout>"
 yy294:
 	YYDEBUG(294, *YYCURSOR);
 	yyaccept = 7;
@@ -7899,7 +7916,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy295:
 	YYDEBUG(295, *YYCURSOR);
-#line 1879 "ext/date/lib/parse_date.re"
+#line 1896 "ext/date/lib/parse_date.re"
 	{
 		DEBUG_OUTPUT("monthtext");
 		TIMELIB_INIT;
@@ -7908,7 +7925,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_DATE_TEXT;
 	}
-#line 7912 "<stdout>"
+#line 7929 "<stdout>"
 yy296:
 	YYDEBUG(296, *YYCURSOR);
 	yyaccept = 7;
@@ -8483,7 +8500,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy315:
 	YYDEBUG(315, *YYCURSOR);
-#line 1820 "ext/date/lib/parse_date.re"
+#line 1837 "ext/date/lib/parse_date.re"
 	{
 		const timelib_relunit* relunit;
 		DEBUG_OUTPUT("daytext");
@@ -8500,7 +8517,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_WEEKDAY;
 	}
-#line 8504 "<stdout>"
+#line 8521 "<stdout>"
 yy316:
 	YYDEBUG(316, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -8768,7 +8785,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy325:
 	YYDEBUG(325, *YYCURSOR);
-#line 1622 "ext/date/lib/parse_date.re"
+#line 1639 "ext/date/lib/parse_date.re"
 	{
 		int length = 0;
 		DEBUG_OUTPUT("datetextual | datenoyear");
@@ -8781,7 +8798,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_DATE_TEXT;
 	}
-#line 8785 "<stdout>"
+#line 8802 "<stdout>"
 yy326:
 	YYDEBUG(326, *YYCURSOR);
 	yyaccept = 10;
@@ -9475,7 +9492,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy351:
 	YYDEBUG(351, *YYCURSOR);
-#line 1168 "ext/date/lib/parse_date.re"
+#line 1185 "ext/date/lib/parse_date.re"
 	{
 		DEBUG_OUTPUT("now");
 		TIMELIB_INIT;
@@ -9483,7 +9500,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_RELATIVE;
 	}
-#line 9487 "<stdout>"
+#line 9504 "<stdout>"
 yy352:
 	YYDEBUG(352, *YYCURSOR);
 	yyaccept = 2;
@@ -10986,7 +11003,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy420:
 	YYDEBUG(420, *YYCURSOR);
-#line 1405 "ext/date/lib/parse_date.re"
+#line 1422 "ext/date/lib/parse_date.re"
 	{
 		DEBUG_OUTPUT("gnunocolon");
 		TIMELIB_INIT;
@@ -11008,7 +11025,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_GNU_NOCOLON;
 	}
-#line 11012 "<stdout>"
+#line 11029 "<stdout>"
 yy421:
 	YYDEBUG(421, *YYCURSOR);
 	yyaccept = 13;
@@ -11089,7 +11106,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy422:
 	YYDEBUG(422, *YYCURSOR);
-#line 1790 "ext/date/lib/parse_date.re"
+#line 1807 "ext/date/lib/parse_date.re"
 	{
 		DEBUG_OUTPUT("year4");
 		TIMELIB_INIT;
@@ -11097,7 +11114,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_CLF;
 	}
-#line 11101 "<stdout>"
+#line 11118 "<stdout>"
 yy423:
 	YYDEBUG(423, *YYCURSOR);
 	yyaccept = 3;
@@ -11704,7 +11721,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	YYDEBUG(456, *YYCURSOR);
 	++YYCURSOR;
 	YYDEBUG(457, *YYCURSOR);
-#line 1339 "ext/date/lib/parse_date.re"
+#line 1356 "ext/date/lib/parse_date.re"
 	{
 		DEBUG_OUTPUT("timetiny12 | timeshort12 | timelong12");
 		TIMELIB_INIT;
@@ -11721,7 +11738,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_TIME12;
 	}
-#line 11725 "<stdout>"
+#line 11742 "<stdout>"
 yy458:
 	YYDEBUG(458, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -13048,7 +13065,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy526:
 	YYDEBUG(526, *YYCURSOR);
-#line 1177 "ext/date/lib/parse_date.re"
+#line 1194 "ext/date/lib/parse_date.re"
 	{
 		DEBUG_OUTPUT("noon");
 		TIMELIB_INIT;
@@ -13059,7 +13076,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_RELATIVE;
 	}
-#line 13063 "<stdout>"
+#line 13080 "<stdout>"
 yy527:
 	YYDEBUG(527, *YYCURSOR);
 	yyaccept = 2;
@@ -14105,7 +14122,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy567:
 	YYDEBUG(567, *YYCURSOR);
-#line 1539 "ext/date/lib/parse_date.re"
+#line 1556 "ext/date/lib/parse_date.re"
 	{
 		int length = 0;
 		DEBUG_OUTPUT("gnudateshort");
@@ -14118,7 +14135,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_ISO_DATE;
 	}
-#line 14122 "<stdout>"
+#line 14139 "<stdout>"
 yy568:
 	YYDEBUG(568, *YYCURSOR);
 	yyaccept = 15;
@@ -14569,7 +14586,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy600:
 	YYDEBUG(600, *YYCURSOR);
-#line 1608 "ext/date/lib/parse_date.re"
+#line 1625 "ext/date/lib/parse_date.re"
 	{
 		int length = 0;
 		DEBUG_OUTPUT("datenodayrev");
@@ -14582,7 +14599,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_DATE_NO_DAY;
 	}
-#line 14586 "<stdout>"
+#line 14603 "<stdout>"
 yy601:
 	YYDEBUG(601, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -15957,7 +15974,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	YYDEBUG(696, *YYCURSOR);
 	++YYCURSOR;
 	YYDEBUG(697, *YYCURSOR);
-#line 1594 "ext/date/lib/parse_date.re"
+#line 1611 "ext/date/lib/parse_date.re"
 	{
 		int length = 0;
 		DEBUG_OUTPUT("datenoday");
@@ -15970,7 +15987,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_DATE_NO_DAY;
 	}
-#line 15974 "<stdout>"
+#line 15991 "<stdout>"
 yy698:
 	YYDEBUG(698, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -16531,7 +16548,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy722:
 	YYDEBUG(722, *YYCURSOR);
-#line 1189 "ext/date/lib/parse_date.re"
+#line 1206 "ext/date/lib/parse_date.re"
 	{
 		DEBUG_OUTPUT("midnight | today");
 		TIMELIB_INIT;
@@ -16540,7 +16557,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_RELATIVE;
 	}
-#line 16544 "<stdout>"
+#line 16561 "<stdout>"
 yy723:
 	YYDEBUG(723, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -16850,7 +16867,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	if (yych <= '9') goto yy897;
 yy739:
 	YYDEBUG(739, *YYCURSOR);
-#line 1580 "ext/date/lib/parse_date.re"
+#line 1597 "ext/date/lib/parse_date.re"
 	{
 		int length = 0;
 		DEBUG_OUTPUT("pointed date YY");
@@ -16863,7 +16880,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_DATE_FULL_POINTED;
 	}
-#line 16867 "<stdout>"
+#line 16884 "<stdout>"
 yy740:
 	YYDEBUG(740, *YYCURSOR);
 	yyaccept = 15;
@@ -16975,7 +16992,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy752:
 	YYDEBUG(752, *YYCURSOR);
-#line 1525 "ext/date/lib/parse_date.re"
+#line 1542 "ext/date/lib/parse_date.re"
 	{
 		int length = 0;
 		DEBUG_OUTPUT("gnudateshorter");
@@ -16988,7 +17005,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_ISO_DATE;
 	}
-#line 16992 "<stdout>"
+#line 17009 "<stdout>"
 yy753:
 	YYDEBUG(753, *YYCURSOR);
 	yyaccept = 18;
@@ -17237,7 +17254,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy777:
 	YYDEBUG(777, *YYCURSOR);
-#line 1451 "ext/date/lib/parse_date.re"
+#line 1468 "ext/date/lib/parse_date.re"
 	{
 		int tz_not_found;
 		DEBUG_OUTPUT("iso8601nocolon");
@@ -17256,7 +17273,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_ISO_NOCOLON;
 	}
-#line 17260 "<stdout>"
+#line 17277 "<stdout>"
 yy778:
 	YYDEBUG(778, *YYCURSOR);
 	yyaccept = 19;
@@ -18484,7 +18501,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy849:
 	YYDEBUG(849, *YYCURSOR);
-#line 1928 "ext/date/lib/parse_date.re"
+#line 1945 "ext/date/lib/parse_date.re"
 	{
 		int tz_not_found;
 		DEBUG_OUTPUT("dateshortwithtimeshort | dateshortwithtimelong | dateshortwithtimelongtz");
@@ -18513,7 +18530,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_SHORTDATE_WITH_TIME;
 	}
-#line 18517 "<stdout>"
+#line 18534 "<stdout>"
 yy850:
 	YYDEBUG(850, *YYCURSOR);
 	yyaccept = 20;
@@ -19557,7 +19574,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy926:
 	YYDEBUG(926, *YYCURSOR);
-#line 1686 "ext/date/lib/parse_date.re"
+#line 1703 "ext/date/lib/parse_date.re"
 	{
 		int length = 0;
 		DEBUG_OUTPUT("pgydotd");
@@ -19570,7 +19587,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_PG_YEARDAY;
 	}
-#line 19574 "<stdout>"
+#line 19591 "<stdout>"
 yy927:
 	YYDEBUG(927, *YYCURSOR);
 	yyaccept = 21;
@@ -19824,7 +19841,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	if (yych <= '7') goto yy1059;
 yy942:
 	YYDEBUG(942, *YYCURSOR);
-#line 1719 "ext/date/lib/parse_date.re"
+#line 1736 "ext/date/lib/parse_date.re"
 	{
 		timelib_sll w, d;
 		DEBUG_OUTPUT("isoweek");
@@ -19842,7 +19859,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_ISO_WEEK;
 	}
-#line 19846 "<stdout>"
+#line 19863 "<stdout>"
 yy943:
 	YYDEBUG(943, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -20318,7 +20335,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	if (yych == 'e') goto yy1094;
 yy982:
 	YYDEBUG(982, *YYCURSOR);
-#line 1862 "ext/date/lib/parse_date.re"
+#line 1879 "ext/date/lib/parse_date.re"
 	{
 		timelib_sll i;
 		int         behavior = 0;
@@ -20334,7 +20351,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_RELATIVE;
 	}
-#line 20338 "<stdout>"
+#line 20355 "<stdout>"
 yy983:
 	YYDEBUG(983, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -20681,7 +20698,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	YYDEBUG(1020, *YYCURSOR);
 	++YYCURSOR;
 	YYDEBUG(1021, *YYCURSOR);
-#line 1568 "ext/date/lib/parse_date.re"
+#line 1585 "ext/date/lib/parse_date.re"
 	{
 		DEBUG_OUTPUT("pointed date YYYY");
 		TIMELIB_INIT;
@@ -20692,7 +20709,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_DATE_FULL_POINTED;
 	}
-#line 20696 "<stdout>"
+#line 20713 "<stdout>"
 yy1022:
 	YYDEBUG(1022, *YYCURSOR);
 	++YYCURSOR;
@@ -20721,7 +20738,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy1025:
 	YYDEBUG(1025, *YYCURSOR);
-#line 1499 "ext/date/lib/parse_date.re"
+#line 1516 "ext/date/lib/parse_date.re"
 	{
 		int length = 0;
 		DEBUG_OUTPUT("iso8601date2");
@@ -20734,7 +20751,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_ISO_DATE;
 	}
-#line 20738 "<stdout>"
+#line 20755 "<stdout>"
 yy1026:
 	YYDEBUG(1026, *YYCURSOR);
 	yyaccept = 15;
@@ -20954,7 +20971,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy1043:
 	YYDEBUG(1043, *YYCURSOR);
-#line 1487 "ext/date/lib/parse_date.re"
+#line 1504 "ext/date/lib/parse_date.re"
 	{
 		DEBUG_OUTPUT("iso8601date4 | iso8601date2 | iso8601dateslash | dateslash");
 		TIMELIB_INIT;
@@ -20965,7 +20982,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_ISO_DATE;
 	}
-#line 20969 "<stdout>"
+#line 20986 "<stdout>"
 yy1044:
 	YYDEBUG(1044, *YYCURSOR);
 	yyaccept = 26;
@@ -21080,7 +21097,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy1048:
 	YYDEBUG(1048, *YYCURSOR);
-#line 1648 "ext/date/lib/parse_date.re"
+#line 1665 "ext/date/lib/parse_date.re"
 	{
 		DEBUG_OUTPUT("datenocolon");
 		TIMELIB_INIT;
@@ -21091,7 +21108,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_DATE_NOCOLON;
 	}
-#line 21095 "<stdout>"
+#line 21112 "<stdout>"
 yy1049:
 	YYDEBUG(1049, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -21161,7 +21178,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	YYDEBUG(1059, *YYCURSOR);
 	++YYCURSOR;
 	YYDEBUG(1060, *YYCURSOR);
-#line 1700 "ext/date/lib/parse_date.re"
+#line 1717 "ext/date/lib/parse_date.re"
 	{
 		timelib_sll w, d;
 		DEBUG_OUTPUT("isoweekday");
@@ -21179,7 +21196,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_ISO_WEEK;
 	}
-#line 21183 "<stdout>"
+#line 21200 "<stdout>"
 yy1061:
 	YYDEBUG(1061, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -21242,7 +21259,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	if (yych <= '9') goto yy1143;
 yy1070:
 	YYDEBUG(1070, *YYCURSOR);
-#line 1738 "ext/date/lib/parse_date.re"
+#line 1755 "ext/date/lib/parse_date.re"
 	{
 		int length = 0;
 		DEBUG_OUTPUT("pgtextshort");
@@ -21255,7 +21272,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_PG_TEXT;
 	}
-#line 21259 "<stdout>"
+#line 21276 "<stdout>"
 yy1071:
 	YYDEBUG(1071, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -21728,7 +21745,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	++YYCURSOR;
 yy1107:
 	YYDEBUG(1107, *YYCURSOR);
-#line 1199 "ext/date/lib/parse_date.re"
+#line 1216 "ext/date/lib/parse_date.re"
 	{
 		DEBUG_OUTPUT("tomorrow");
 		TIMELIB_INIT;
@@ -21739,7 +21756,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_RELATIVE;
 	}
-#line 21743 "<stdout>"
+#line 21760 "<stdout>"
 yy1108:
 	YYDEBUG(1108, *YYCURSOR);
 	yyaccept = 28;
@@ -22076,7 +22093,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	YYDEBUG(1140, *YYCURSOR);
 	++YYCURSOR;
 	YYDEBUG(1141, *YYCURSOR);
-#line 1752 "ext/date/lib/parse_date.re"
+#line 1769 "ext/date/lib/parse_date.re"
 	{
 		int length = 0;
 		DEBUG_OUTPUT("pgtextreverse");
@@ -22089,7 +22106,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_PG_TEXT;
 	}
-#line 22093 "<stdout>"
+#line 22110 "<stdout>"
 yy1142:
 	YYDEBUG(1142, *YYCURSOR);
 	++YYCURSOR;
@@ -22133,7 +22150,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy1145:
 	YYDEBUG(1145, *YYCURSOR);
-#line 1294 "ext/date/lib/parse_date.re"
+#line 1311 "ext/date/lib/parse_date.re"
 	{
 		DEBUG_OUTPUT("backof | frontof");
 		TIMELIB_INIT;
@@ -22155,7 +22172,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_LF_DAY_OF_MONTH;
 	}
-#line 22159 "<stdout>"
+#line 22176 "<stdout>"
 yy1146:
 	YYDEBUG(1146, *YYCURSOR);
 	yyaccept = 29;
@@ -22479,7 +22496,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	}
 yy1172:
 	YYDEBUG(1172, *YYCURSOR);
-#line 1838 "ext/date/lib/parse_date.re"
+#line 1855 "ext/date/lib/parse_date.re"
 	{
 		timelib_sll i;
 		int         behavior = 0;
@@ -22502,7 +22519,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_RELATIVE;
 	}
-#line 22506 "<stdout>"
+#line 22523 "<stdout>"
 yy1173:
 	YYDEBUG(1173, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -22514,7 +22531,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	++YYCURSOR;
 yy1175:
 	YYDEBUG(1175, *YYCURSOR);
-#line 1156 "ext/date/lib/parse_date.re"
+#line 1173 "ext/date/lib/parse_date.re"
 	{
 		DEBUG_OUTPUT("yesterday");
 		TIMELIB_INIT;
@@ -22525,7 +22542,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_RELATIVE;
 	}
-#line 22529 "<stdout>"
+#line 22546 "<stdout>"
 yy1176:
 	YYDEBUG(1176, *YYCURSOR);
 	yyaccept = 31;
@@ -23018,7 +23035,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	YYDEBUG(1222, *YYCURSOR);
 	++YYCURSOR;
 	YYDEBUG(1223, *YYCURSOR);
-#line 1904 "ext/date/lib/parse_date.re"
+#line 1921 "ext/date/lib/parse_date.re"
 	{
 		DEBUG_OUTPUT("dateshortwithtimeshort12 | dateshortwithtimelong12");
 		TIMELIB_INIT;
@@ -23041,7 +23058,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_SHORTDATE_WITH_TIME;
 	}
-#line 23045 "<stdout>"
+#line 23062 "<stdout>"
 yy1224:
 	YYDEBUG(1224, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -23543,7 +23560,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	YYDEBUG(1268, *YYCURSOR);
 	++YYCURSOR;
 	YYDEBUG(1269, *YYCURSOR);
-#line 1317 "ext/date/lib/parse_date.re"
+#line 1334 "ext/date/lib/parse_date.re"
 	{
 		timelib_sll i;
 		int         behavior = 0;
@@ -23564,7 +23581,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_WEEK_DAY_OF_MONTH;
 	}
-#line 23568 "<stdout>"
+#line 23585 "<stdout>"
 yy1270:
 	YYDEBUG(1270, *YYCURSOR);
 	yyaccept = 24;
@@ -23611,7 +23628,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	YYDEBUG(1273, *YYCURSOR);
 	++YYCURSOR;
 	YYDEBUG(1274, *YYCURSOR);
-#line 1277 "ext/date/lib/parse_date.re"
+#line 1294 "ext/date/lib/parse_date.re"
 	{
 		DEBUG_OUTPUT("firstdayof | lastdayof");
 		TIMELIB_INIT;
@@ -23627,12 +23644,12 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_LF_DAY_OF_MONTH;
 	}
-#line 23631 "<stdout>"
+#line 23648 "<stdout>"
 yy1275:
 	YYDEBUG(1275, *YYCURSOR);
 	++YYCURSOR;
 	YYDEBUG(1276, *YYCURSOR);
-#line 1513 "ext/date/lib/parse_date.re"
+#line 1530 "ext/date/lib/parse_date.re"
 	{
 		DEBUG_OUTPUT("iso8601datex");
 		TIMELIB_INIT;
@@ -23643,7 +23660,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_ISO_DATE;
 	}
-#line 23647 "<stdout>"
+#line 23664 "<stdout>"
 yy1277:
 	YYDEBUG(1277, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -23746,7 +23763,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	YYDEBUG(1290, *YYCURSOR);
 	++YYCURSOR;
 	YYDEBUG(1291, *YYCURSOR);
-#line 1357 "ext/date/lib/parse_date.re"
+#line 1374 "ext/date/lib/parse_date.re"
 	{
 		DEBUG_OUTPUT("mssqltime");
 		TIMELIB_INIT;
@@ -23765,7 +23782,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_TIME24_WITH_ZONE;
 	}
-#line 23769 "<stdout>"
+#line 23786 "<stdout>"
 yy1292:
 	YYDEBUG(1292, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -24189,7 +24206,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	if (yych <= '9') goto yy1331;
 yy1329:
 	YYDEBUG(1329, *YYCURSOR);
-#line 1660 "ext/date/lib/parse_date.re"
+#line 1677 "ext/date/lib/parse_date.re"
 	{
 		int tz_not_found;
 		DEBUG_OUTPUT("xmlrpc | xmlrpcnocolon | soap | wddx | exif");
@@ -24214,7 +24231,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_XMLRPC_SOAP;
 	}
-#line 24218 "<stdout>"
+#line 24235 "<stdout>"
 yy1330:
 	YYDEBUG(1330, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -24584,7 +24601,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	if (yych <= ':') goto yy1383;
 yy1375:
 	YYDEBUG(1375, *YYCURSOR);
-#line 1766 "ext/date/lib/parse_date.re"
+#line 1783 "ext/date/lib/parse_date.re"
 	{
 		int tz_not_found;
 		DEBUG_OUTPUT("clf");
@@ -24607,7 +24624,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 		TIMELIB_DEINIT;
 		return TIMELIB_CLF;
 	}
-#line 24611 "<stdout>"
+#line 24628 "<stdout>"
 yy1376:
 	YYDEBUG(1376, *YYCURSOR);
 	yyaccept = 33;
@@ -24839,7 +24856,7 @@ static int scan(Scanner *s, timelib_tz_get_wrapper tz_get_wrapper)
 	if (yych == ':') goto yy1286;
 	goto yy1329;
 }
-#line 1994 "ext/date/lib/parse_date.re"
+#line 2011 "ext/date/lib/parse_date.re"

 }

diff --git a/ext/date/lib/parse_date.re b/ext/date/lib/parse_date.re
index 2f405d9381d..6300ebdc7f2 100644
--- a/ext/date/lib/parse_date.re
+++ b/ext/date/lib/parse_date.re
@@ -76,6 +76,7 @@
 #define TIMELIB_RELATIVE       310

 #define TIMELIB_ERROR          999
+#define TIMELIB_MAX_ERRORS     1000

 /* Some compilers like AIX, defines uchar in sys/types.h */
 #undef uchar
@@ -363,6 +364,10 @@ static timelib_error_message *alloc_error_message(timelib_error_message **messag

 static void add_warning(Scanner *s, int error_code, const char *error)
 {
+	if (s->errors->warning_count >= TIMELIB_MAX_ERRORS) {
+		return;
+	}
+
 	timelib_error_message *message = alloc_error_message(&s->errors->warning_messages, &s->errors->warning_count);

 	message->error_code = error_code;
@@ -373,6 +378,10 @@ static void add_warning(Scanner *s, int error_code, const char *error)

 static void add_error(Scanner *s, int error_code, const char *error)
 {
+	if (s->errors->error_count >= TIMELIB_MAX_ERRORS) {
+		return;
+	}
+
 	timelib_error_message *message = alloc_error_message(&s->errors->error_messages, &s->errors->error_count);

 	message->error_code = error_code;
@@ -383,6 +392,10 @@ static void add_error(Scanner *s, int error_code, const char *error)

 static void add_pbf_warning(Scanner *s, int error_code, const char *error, const char *sptr, const char *cptr)
 {
+	if (s->errors->warning_count >= TIMELIB_MAX_ERRORS) {
+		return;
+	}
+
 	timelib_error_message *message = alloc_error_message(&s->errors->warning_messages, &s->errors->warning_count);

 	message->error_code = error_code;
@@ -393,6 +406,10 @@ static void add_pbf_warning(Scanner *s, int error_code, const char *error, const

 static void add_pbf_error(Scanner *s, int error_code, const char *error, const char *sptr, const char *cptr)
 {
+	if (s->errors->error_count >= TIMELIB_MAX_ERRORS) {
+		return;
+	}
+
 	timelib_error_message *message = alloc_error_message(&s->errors->error_messages, &s->errors->error_count);

 	message->error_code = error_code;
diff --git a/ext/date/lib/parse_iso_intervals.c b/ext/date/lib/parse_iso_intervals.c
index d5fa782b8f3..93a5b227afb 100644
--- a/ext/date/lib/parse_iso_intervals.c
+++ b/ext/date/lib/parse_iso_intervals.c
@@ -1,4 +1,4 @@
-/* Generated by re2c 1.0.3 on Wed Sep 11 17:29:40 2024 */
+/* Generated by re2c 1.0.3 on Tue Sep 15 16:40:58 2026 */
 #line 1 "ext/date/lib/parse_iso_intervals.re"
 /*
  * The MIT License (MIT)
@@ -44,6 +44,7 @@
 #define TIMELIB_PERIOD  260
 #define TIMELIB_ISO_DATE 261
 #define TIMELIB_ERROR   999
+#define TIMELIB_MAX_ERRORS 1000

 typedef unsigned char uchar;

@@ -90,6 +91,10 @@ typedef struct _Scanner {

 static void add_error(Scanner *s, const char *error)
 {
+	if (s->errors->error_count >= TIMELIB_MAX_ERRORS) {
+		return;
+	}
+
 	s->errors->error_count++;
 	s->errors->error_messages = timelib_realloc(s->errors->error_messages, s->errors->error_count * sizeof(timelib_error_message));
 	s->errors->error_messages[s->errors->error_count - 1].position = s->tok ? s->tok - s->str : 0;
@@ -176,11 +181,11 @@ static int scan(Scanner *s)
 std:
 	s->tok = cursor;
 	s->len = 0;
-#line 204 "ext/date/lib/parse_iso_intervals.re"
+#line 209 "ext/date/lib/parse_iso_intervals.re"



-#line 184 "<stdout>"
+#line 189 "<stdout>"
 {
 	YYCTYPE yych;
 	unsigned int yyaccept = 0;
@@ -247,32 +252,32 @@ static int scan(Scanner *s)
 	YYDEBUG(2, *YYCURSOR);
 	++YYCURSOR;
 	YYDEBUG(3, *YYCURSOR);
-#line 311 "ext/date/lib/parse_iso_intervals.re"
+#line 316 "ext/date/lib/parse_iso_intervals.re"
 	{
 		s->pos = cursor; s->line++;
 		goto std;
 	}
-#line 256 "<stdout>"
+#line 261 "<stdout>"
 yy4:
 	YYDEBUG(4, *YYCURSOR);
 	++YYCURSOR;
 yy5:
 	YYDEBUG(5, *YYCURSOR);
-#line 317 "ext/date/lib/parse_iso_intervals.re"
+#line 322 "ext/date/lib/parse_iso_intervals.re"
 	{
 		add_error(s, "Unexpected character");
 		goto std;
 	}
-#line 267 "<stdout>"
+#line 272 "<stdout>"
 yy6:
 	YYDEBUG(6, *YYCURSOR);
 	++YYCURSOR;
 	YYDEBUG(7, *YYCURSOR);
-#line 306 "ext/date/lib/parse_iso_intervals.re"
+#line 311 "ext/date/lib/parse_iso_intervals.re"
 	{
 		goto std;
 	}
-#line 276 "<stdout>"
+#line 281 "<stdout>"
 yy8:
 	YYDEBUG(8, *YYCURSOR);
 	yyaccept = 0;
@@ -289,7 +294,7 @@ static int scan(Scanner *s)
 	if (yych == 'T') goto yy15;
 yy10:
 	YYDEBUG(10, *YYCURSOR);
-#line 244 "ext/date/lib/parse_iso_intervals.re"
+#line 249 "ext/date/lib/parse_iso_intervals.re"
 	{
 		timelib_sll nr;
 		int         in_time = 0;
@@ -330,7 +335,7 @@ static int scan(Scanner *s)
 		TIMELIB_DEINIT;
 		return TIMELIB_PERIOD;
 	}
-#line 334 "<stdout>"
+#line 339 "<stdout>"
 yy11:
 	YYDEBUG(11, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -389,7 +394,7 @@ static int scan(Scanner *s)
 		goto yy16;
 	}
 	YYDEBUG(18, *YYCURSOR);
-#line 209 "ext/date/lib/parse_iso_intervals.re"
+#line 214 "ext/date/lib/parse_iso_intervals.re"
 	{
 		DEBUG_OUTPUT("recurrences");
 		TIMELIB_INIT;
@@ -399,7 +404,7 @@ static int scan(Scanner *s)
 		s->have_recurrences = 1;
 		return TIMELIB_PERIOD;
 	}
-#line 403 "<stdout>"
+#line 408 "<stdout>"
 yy19:
 	YYDEBUG(19, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -894,7 +899,7 @@ static int scan(Scanner *s)
 	YYDEBUG(89, *YYCURSOR);
 	++YYCURSOR;
 	YYDEBUG(90, *YYCURSOR);
-#line 220 "ext/date/lib/parse_iso_intervals.re"
+#line 225 "ext/date/lib/parse_iso_intervals.re"
 	{
 		timelib_time *current;

@@ -917,7 +922,7 @@ static int scan(Scanner *s)
 		TIMELIB_DEINIT;
 		return TIMELIB_ISO_DATE;
 	}
-#line 921 "<stdout>"
+#line 926 "<stdout>"
 yy91:
 	YYDEBUG(91, *YYCURSOR);
 	yych = *++YYCURSOR;
@@ -937,7 +942,7 @@ static int scan(Scanner *s)
 	YYDEBUG(95, *YYCURSOR);
 	++YYCURSOR;
 	YYDEBUG(96, *YYCURSOR);
-#line 286 "ext/date/lib/parse_iso_intervals.re"
+#line 291 "ext/date/lib/parse_iso_intervals.re"
 	{
 		DEBUG_OUTPUT("combinedrep");
 		TIMELIB_INIT;
@@ -956,9 +961,9 @@ static int scan(Scanner *s)
 		TIMELIB_DEINIT;
 		return TIMELIB_PERIOD;
 	}
-#line 960 "<stdout>"
+#line 965 "<stdout>"
 }
-#line 321 "ext/date/lib/parse_iso_intervals.re"
+#line 326 "ext/date/lib/parse_iso_intervals.re"

 }
 #ifdef PHP_WIN32
diff --git a/ext/date/lib/parse_iso_intervals.re b/ext/date/lib/parse_iso_intervals.re
index 00942007751..6c3a1c82c0a 100644
--- a/ext/date/lib/parse_iso_intervals.re
+++ b/ext/date/lib/parse_iso_intervals.re
@@ -42,6 +42,7 @@
 #define TIMELIB_PERIOD  260
 #define TIMELIB_ISO_DATE 261
 #define TIMELIB_ERROR   999
+#define TIMELIB_MAX_ERRORS 1000

 typedef unsigned char uchar;

@@ -88,6 +89,10 @@ typedef struct _Scanner {

 static void add_error(Scanner *s, const char *error)
 {
+	if (s->errors->error_count >= TIMELIB_MAX_ERRORS) {
+		return;
+	}
+
 	s->errors->error_count++;
 	s->errors->error_messages = timelib_realloc(s->errors->error_messages, s->errors->error_count * sizeof(timelib_error_message));
 	s->errors->error_messages[s->errors->error_count - 1].position = s->tok ? s->tok - s->str : 0;
diff --git a/ext/date/tests/oom_date_create.phpt b/ext/date/tests/oom_date_create.phpt
new file mode 100644
index 00000000000..c134d424acf
--- /dev/null
+++ b/ext/date/tests/oom_date_create.phpt
@@ -0,0 +1,10 @@
+--TEST--
+date_create() doesn't reach hard limit
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+var_dump(date_create(str_repeat("\x01", 4 * 1024 * 1024)));
+?>
+--EXPECT--
+bool(false)
diff --git a/ext/date/tests/oom_date_create_immutable.phpt b/ext/date/tests/oom_date_create_immutable.phpt
new file mode 100644
index 00000000000..d7ec08275e7
--- /dev/null
+++ b/ext/date/tests/oom_date_create_immutable.phpt
@@ -0,0 +1,10 @@
+--TEST--
+date_create_immutable() doesn't reach hard limit
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+var_dump(date_create_immutable(str_repeat("\x01", 4 * 1024 * 1024)));
+?>
+--EXPECT--
+bool(false)
diff --git a/ext/date/tests/oom_date_interval_create_from_date_string.phpt b/ext/date/tests/oom_date_interval_create_from_date_string.phpt
new file mode 100644
index 00000000000..9e3dc5a2d0f
--- /dev/null
+++ b/ext/date/tests/oom_date_interval_create_from_date_string.phpt
@@ -0,0 +1,11 @@
+--TEST--
+date_interval_create_from_date_string() doesn't reach hard limit
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+var_dump(date_interval_create_from_date_string(str_repeat("\x01", 4 * 1024 * 1024)));
+?>
+--EXPECTF--
+Warning: date_interval_create_from_date_string(): Unknown or bad format (%s) at position 0 (%s): Unexpected character in %s on line %d
+bool(false)
diff --git a/ext/date/tests/oom_date_modify.phpt b/ext/date/tests/oom_date_modify.phpt
new file mode 100644
index 00000000000..93101cd2c2d
--- /dev/null
+++ b/ext/date/tests/oom_date_modify.phpt
@@ -0,0 +1,11 @@
+--TEST--
+date_modify() doesn't reach hard limit
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+var_dump(date_modify(new DateTime(), str_repeat("\x01", 4 * 1024 * 1024)));
+?>
+--EXPECTF--
+Warning: date_modify(): Failed to parse time string (%s) at position 0 (%s): Unexpected character in %s on line %d
+bool(false)
diff --git a/ext/date/tests/oom_date_parse.phpt b/ext/date/tests/oom_date_parse.phpt
new file mode 100644
index 00000000000..c42bcea5b95
--- /dev/null
+++ b/ext/date/tests/oom_date_parse.phpt
@@ -0,0 +1,11 @@
+--TEST--
+date_parse() doesn't reach hard limit
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+$result = date_parse(str_repeat("\x01", 4 * 1024 * 1024));
+var_dump(count($result['errors']));
+?>
+--EXPECT--
+int(1000)
diff --git a/ext/date/tests/oom_dateinterval_construct.phpt b/ext/date/tests/oom_dateinterval_construct.phpt
new file mode 100644
index 00000000000..1ff6b61f8a1
--- /dev/null
+++ b/ext/date/tests/oom_dateinterval_construct.phpt
@@ -0,0 +1,18 @@
+--TEST--
+DateInterval::__construct() doesn't reach hard limit
+--INI--
+memory_limit=64M
+--SKIPIF--
+<?php
+if (getenv('SKIP_ASAN')) die("skip ASan-instrumented realloc() is slow");
+?>
+--FILE--
+<?php
+try {
+    new DateInterval(str_repeat("\x01", 4 * 1024 * 1024));
+} catch (\Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+Exception: Unknown or bad format (%s)
diff --git a/ext/date/tests/oom_dateinterval_createfromdatestring.phpt b/ext/date/tests/oom_dateinterval_createfromdatestring.phpt
new file mode 100644
index 00000000000..58087cfc30c
--- /dev/null
+++ b/ext/date/tests/oom_dateinterval_createfromdatestring.phpt
@@ -0,0 +1,11 @@
+--TEST--
+DateInterval::createFromDateString() doesn't reach hard limit
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+var_dump(DateInterval::createFromDateString(str_repeat("\x01", 4 * 1024 * 1024)));
+?>
+--EXPECTF--
+Warning: DateInterval::createFromDateString(): Unknown or bad format (%s) at position 0 (%s): Unexpected character in %s on line %d
+bool(false)
diff --git a/ext/date/tests/oom_dateperiod_construct.phpt b/ext/date/tests/oom_dateperiod_construct.phpt
new file mode 100644
index 00000000000..77d6ea8a9b8
--- /dev/null
+++ b/ext/date/tests/oom_dateperiod_construct.phpt
@@ -0,0 +1,18 @@
+--TEST--
+DatePeriod::__construct() doesn't reach hard limit
+--INI--
+memory_limit=64M
+--SKIPIF--
+<?php
+if (getenv('SKIP_ASAN')) die("skip ASan-instrumented realloc() is slow");
+?>
+--FILE--
+<?php
+try {
+    new DatePeriod(str_repeat("\x01", 4 * 1024 * 1024));
+} catch (\Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+Exception: Unknown or bad format (%s)
diff --git a/ext/date/tests/oom_datetime_construct.phpt b/ext/date/tests/oom_datetime_construct.phpt
new file mode 100644
index 00000000000..2eec0755dc8
--- /dev/null
+++ b/ext/date/tests/oom_datetime_construct.phpt
@@ -0,0 +1,14 @@
+--TEST--
+DateTime::__construct() doesn't reach hard limit
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+try {
+    new DateTime(str_repeat("\x01", 4 * 1024 * 1024));
+} catch (\Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+Exception: Failed to parse time string (%s) at position 0 (%s): Unexpected character
diff --git a/ext/date/tests/oom_datetime_modify.phpt b/ext/date/tests/oom_datetime_modify.phpt
new file mode 100644
index 00000000000..4e7d9628850
--- /dev/null
+++ b/ext/date/tests/oom_datetime_modify.phpt
@@ -0,0 +1,11 @@
+--TEST--
+DateTime::modify() doesn't reach hard limit
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+var_dump((new DateTime())->modify(str_repeat("\x01", 4 * 1024 * 1024)));
+?>
+--EXPECTF--
+Warning: DateTime::modify(): Failed to parse time string (%s) at position 0 (%s): Unexpected character in %s on line %d
+bool(false)
diff --git a/ext/date/tests/oom_datetimeimmutable_construct.phpt b/ext/date/tests/oom_datetimeimmutable_construct.phpt
new file mode 100644
index 00000000000..e1d8d7dcddb
--- /dev/null
+++ b/ext/date/tests/oom_datetimeimmutable_construct.phpt
@@ -0,0 +1,14 @@
+--TEST--
+DateTimeImmutable::__construct() doesn't reach hard limit
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+try {
+    new DateTimeImmutable(str_repeat("\x01", 4 * 1024 * 1024));
+} catch (\Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+Exception: Failed to parse time string (%s) at position 0 (%s): Unexpected character
diff --git a/ext/date/tests/oom_datetimeimmutable_modify.phpt b/ext/date/tests/oom_datetimeimmutable_modify.phpt
new file mode 100644
index 00000000000..932e5ae7582
--- /dev/null
+++ b/ext/date/tests/oom_datetimeimmutable_modify.phpt
@@ -0,0 +1,11 @@
+--TEST--
+DateTimeImmutable::modify() doesn't reach hard limit
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+var_dump((new DateTimeImmutable())->modify(str_repeat("\x01", 4 * 1024 * 1024)));
+?>
+--EXPECTF--
+Warning: DateTimeImmutable::modify(): Failed to parse time string (%s) at position 0 (%s): Unexpected character in %s on line %d
+bool(false)
diff --git a/ext/date/tests/oom_strtotime.phpt b/ext/date/tests/oom_strtotime.phpt
new file mode 100644
index 00000000000..ac6eb526c79
--- /dev/null
+++ b/ext/date/tests/oom_strtotime.phpt
@@ -0,0 +1,10 @@
+--TEST--
+strtotime() doesn't reach hard limit
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+var_dump(strtotime(str_repeat("\x01", 4 * 1024 * 1024)));
+?>
+--EXPECT--
+bool(false)
diff --git a/ext/dom/tests/oom_xpath.phpt b/ext/dom/tests/oom_xpath.phpt
new file mode 100644
index 00000000000..22466cc87da
--- /dev/null
+++ b/ext/dom/tests/oom_xpath.phpt
@@ -0,0 +1,22 @@
+--TEST--
+DOMXPath::query() with large result set doesn't reach hard memory limit
+--EXTENSIONS--
+dom
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+$path = __DIR__ . '/oom_xpath.xml';
+file_put_contents($path, '<r>' . str_repeat('<a/>', 1000000) . '</r>');
+
+$xml = new DOMDocument;
+$xml->load($path);
+$result = (new DOMXPath($xml))->query("//a");
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/oom_xpath.xml');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 67108864 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+
diff --git a/ext/dom/xpath.c b/ext/dom/xpath.c
index cad08e0a8d8..9ff273cd50e 100644
--- a/ext/dom/xpath.c
+++ b/ext/dom/xpath.c
@@ -424,7 +424,10 @@ static void php_xpath_eval(INTERNAL_FUNCTION_PARAMETERS, int type) /* {{{ */

 			if (xpathobjp->type == XPATH_NODESET && NULL != (nodesetp = xpathobjp->nodesetval) && nodesetp->nodeNr) {

-				array_init(&retval);
+				array_init_size(&retval, nodesetp->nodeNr);
+				if (zend_hash_real_finit_packed(Z_ARRVAL(retval), /* fallible */ true) != SUCCESS) {
+					goto oom_nodeset;
+				}
 				for (i = 0; i < nodesetp->nodeNr; i++) {
 					xmlNodePtr node = nodesetp->nodeTab[i];
 					zval child;
@@ -443,6 +446,9 @@ static void php_xpath_eval(INTERNAL_FUNCTION_PARAMETERS, int type) /* {{{ */
 						php_dom_create_object(node, &child, &intern->dom);
 					}
 					add_next_index_zval(&retval, &child);
+					if (ZEND_HAS_OOM()) {
+						goto oom_nodeset;
+					}
 				}
 			} else {
 				ZVAL_EMPTY_ARRAY(&retval);
@@ -451,6 +457,13 @@ static void php_xpath_eval(INTERNAL_FUNCTION_PARAMETERS, int type) /* {{{ */
 			nodeobj = Z_DOMOBJ_P(return_value);
 			dom_xpath_iter(&retval, nodeobj);
 			break;
+
+oom_nodeset:
+			zval_ptr_dtor(&retval);
+			xmlXPathFreeObject(xpathobjp);
+			zval_ptr_dtor(return_value);
+			ZVAL_NULL(return_value);
+			RETURN_OOM();
 		}

 		case XPATH_BOOLEAN:
diff --git a/ext/exif/exif.c b/ext/exif/exif.c
index 3081ad9a1b2..7944988d097 100644
--- a/ext/exif/exif.c
+++ b/ext/exif/exif.c
@@ -2159,7 +2159,10 @@ static int exif_file_sections_realloc(image_info_type *ImageInfo, int section_in
 		EXIF_ERRLOG_FSREALLOC(ImageInfo)
 		return -1;
 	}
-	tmp = safe_erealloc(ImageInfo->file.list[section_index].data, 1, size, 0);
+	tmp = safe_ferealloc(ImageInfo->file.list[section_index].data, 1, size, 0, /* fallible */ true);
+	if (!tmp) {
+		return -1;
+	}
 	ImageInfo->file.list[section_index].data = tmp;
 	ImageInfo->file.list[section_index].size = size;
 	return 0;
@@ -2278,7 +2281,11 @@ static void exif_iif_add_value(image_info_type *image_info, int section_index, c
 				break;
 			} else
 			if (length>1) {
-				info_value->list = safe_emalloc(length, sizeof(image_info_value), 0);
+				info_value->list = safe_femalloc(length, sizeof(image_info_value), 0, /* fallible */ true);
+				if (!info_value->list) {
+					info_data->length = 0;
+					return;
+				}
 			} else {
 				info_value = &info_data->value;
 			}
@@ -2464,13 +2471,13 @@ static void exif_iif_free(image_info_type *image_info, int section_index) {

 /* {{{ add_assoc_image_info
  * Add image_info to associative array value. */
-static void add_assoc_image_info(zval *value, int sub_array, image_info_type *image_info, int section_index)
+static zend_result add_assoc_image_info(zval *value, int sub_array, image_info_type *image_info, int section_index)
 {
 	char buffer[64], uname[64];
 	int idx = 0, unknown = 0;

 	if (!image_info->info_list[section_index].count) {
-		return;
+		return SUCCESS;
 	}

 	zval tmpi;
@@ -2608,6 +2615,12 @@ static void add_assoc_image_info(zval *value, int sub_array, image_info_type *im
 								}
 								break;
 						}
+						if (ZEND_HAS_OOM()) {
+							if (l > 1) {
+								zval_ptr_dtor(&array);
+							}
+							goto failure;
+						}
 					}
 					if (l > 1) {
 						add_assoc_zval(&tmpi, name, &array);
@@ -2616,10 +2629,20 @@ static void add_assoc_image_info(zval *value, int sub_array, image_info_type *im
 				}
 			}
 		}
+		if (ZEND_HAS_OOM()) {
+			goto failure;
+		}
 	}
 	if (sub_array) {
 		add_assoc_zval(value, exif_get_sectionname(section_index), &tmpi);
 	}
+	return ZEND_HAS_OOM() ? FAILURE : SUCCESS;
+
+failure:
+	if (sub_array) {
+		zval_ptr_dtor(&tmpi);
+	}
+	return FAILURE;
 }
 /* }}} */

@@ -2888,6 +2911,10 @@ static void exif_thumbnail_build(image_info_type *ImageInfo) {
 	if (!ImageInfo->read_thumbnail || !ImageInfo->Thumbnail.offset || !ImageInfo->Thumbnail.size) {
 		return; /* ignore this call */
 	}
+	/* new_size below sums the accumulated tag values, so stop once over the limit. */
+	if (ZEND_HAS_OOM()) {
+		return;
+	}
 #ifdef EXIF_DEBUG
 	exif_error_docref(NULL EXIFERR_CC, ImageInfo, E_NOTICE, "Thumbnail: filetype = %d", ImageInfo->Thumbnail.filetype);
 #endif
@@ -2964,6 +2991,17 @@ static void exif_thumbnail_build(image_info_type *ImageInfo) {
 }
 /* }}} */

+/* Rejects a thumbnail whose data is not actually present in the file. */
+static bool exif_thumbnail_in_file(image_info_type *ImageInfo)
+{
+	if (ImageInfo->Thumbnail.size > ImageInfo->FileSize
+	 || ImageInfo->Thumbnail.offset > ImageInfo->FileSize - ImageInfo->Thumbnail.size) {
+		EXIF_ERRLOG_THUMBEOF(ImageInfo)
+		return false;
+	}
+	return true;
+}
+
 /* {{{ exif_thumbnail_extract
  * Grab the thumbnail, corrected */
 static void exif_thumbnail_extract(image_info_type *ImageInfo, const exif_offset_info *info) {
@@ -3320,7 +3358,10 @@ static bool exif_process_IFD_TAG_impl(image_info_type *ImageInfo, char *dir_entr
 			}
 			if (byte_count>sizeof(cbuf)) {
 				/* mark as outside range and get buffer */
-				value_ptr = safe_emalloc(byte_count, 1, 0);
+				value_ptr = safe_femalloc(byte_count, 1, 0, /* fallible */ true);
+				if (!value_ptr) {
+					return false;
+				}
 				outside = value_ptr;
 			} else {
 				/* In most cases we only access a small range so
@@ -3608,6 +3649,10 @@ static bool exif_process_IFD_TAG(image_info_type *ImageInfo, char *dir_entry, co
 		exif_error_docref("exif_read_data#error_ifd" EXIFERR_CC, ImageInfo, E_WARNING, "corrupt EXIF header: maximum directory nesting level reached");
 		return false;
 	}
+	/* Each tag may retain up to FileSize bytes, so stop once over the limit. */
+	if (ZEND_HAS_OOM()) {
+		return false;
+	}
 	ImageInfo->ifd_nesting_level++;
 	result = exif_process_IFD_TAG_impl(ImageInfo, dir_entry, info, displacement, section_index, ReadNextIFD, tag_table);
 	ImageInfo->ifd_nesting_level--;
@@ -3806,6 +3851,10 @@ static bool exif_scan_JPEG_header(image_info_type *ImageInfo)
 	jpeg_sof_info sof_info;

 	for(section=0;;section++) {
+		/* Every section is retained, so stop once over the limit. */
+		if (ZEND_HAS_OOM()) {
+			return false;
+		}
 #ifdef EXIF_DEBUG
 		fpos = php_stream_tell(ImageInfo->infile);
 		exif_error_docref(NULL EXIFERR_CC, ImageInfo, E_NOTICE, "Needing section %d @ 0x%08X", ImageInfo->file.count, fpos);
@@ -4218,8 +4267,11 @@ static bool exif_process_IFD_in_TIFF_impl(image_info_type *ImageInfo, size_t dir
 #ifdef EXIF_DEBUG
 								exif_error_docref(NULL EXIFERR_CC, ImageInfo, E_NOTICE, "%s THUMBNAIL @0x%04X + 0x%04X", ImageInfo->Thumbnail.data ? "Ignore" : "Read", ImageInfo->Thumbnail.offset, ImageInfo->Thumbnail.size);
 #endif
-								if (!ImageInfo->Thumbnail.data) {
-									ImageInfo->Thumbnail.data = safe_emalloc(ImageInfo->Thumbnail.size, 1, 0);
+								if (!ImageInfo->Thumbnail.data && exif_thumbnail_in_file(ImageInfo)) {
+									ImageInfo->Thumbnail.data = safe_femalloc(ImageInfo->Thumbnail.size, 1, 0, /* fallible */ true);
+									if (!ImageInfo->Thumbnail.data) {
+										return false;
+									}
 									php_stream_seek(ImageInfo->infile, ImageInfo->Thumbnail.offset, SEEK_SET);
 									fgot = exif_read_from_stream_file_looped(ImageInfo->infile, ImageInfo->Thumbnail.data, ImageInfo->Thumbnail.size);
 									if (fgot != ImageInfo->Thumbnail.size) {
@@ -4258,8 +4310,12 @@ static bool exif_process_IFD_in_TIFF_impl(image_info_type *ImageInfo, size_t dir
 #ifdef EXIF_DEBUG
 					exif_error_docref(NULL EXIFERR_CC, ImageInfo, E_NOTICE, "%s THUMBNAIL @0x%04X + 0x%04X", ImageInfo->Thumbnail.data ? "Ignore" : "Read", ImageInfo->Thumbnail.offset, ImageInfo->Thumbnail.size);
 #endif
-					if (!ImageInfo->Thumbnail.data && ImageInfo->Thumbnail.offset && ImageInfo->Thumbnail.size && ImageInfo->read_thumbnail) {
-						ImageInfo->Thumbnail.data = safe_emalloc(ImageInfo->Thumbnail.size, 1, 0);
+					if (!ImageInfo->Thumbnail.data && ImageInfo->Thumbnail.offset && ImageInfo->Thumbnail.size && ImageInfo->read_thumbnail
+					 && exif_thumbnail_in_file(ImageInfo)) {
+						ImageInfo->Thumbnail.data = safe_femalloc(ImageInfo->Thumbnail.size, 1, 0, /* fallible */ true);
+						if (!ImageInfo->Thumbnail.data) {
+							return false;
+						}
 						php_stream_seek(ImageInfo->infile, ImageInfo->Thumbnail.offset, SEEK_SET);
 						fgot = exif_read_from_stream_file_looped(ImageInfo->infile, ImageInfo->Thumbnail.data, ImageInfo->Thumbnail.size);
 						if (fgot != ImageInfo->Thumbnail.size) {
@@ -4299,6 +4355,9 @@ static bool exif_process_IFD_in_TIFF(image_info_type *ImageInfo, size_t dir_offs
 	if (ImageInfo->ifd_nesting_level > MAX_IFD_NESTING_LEVEL) {
 		return false;
 	}
+	if (ZEND_HAS_OOM()) {
+		return false;
+	}
 	ImageInfo->ifd_nesting_level++;
 	result = exif_process_IFD_in_TIFF_impl(ImageInfo, dir_offset, section_index);
 	ImageInfo->ifd_nesting_level--;
@@ -4693,19 +4752,21 @@ PHP_FUNCTION(exif_read_data)
 	exif_error_docref(NULL EXIFERR_CC, &ImageInfo, E_NOTICE, "Adding image infos");
 #endif

-	add_assoc_image_info(return_value, sub_arrays, &ImageInfo, SECTION_FILE      );
-	add_assoc_image_info(return_value, 1,          &ImageInfo, SECTION_COMPUTED  );
-	add_assoc_image_info(return_value, sub_arrays, &ImageInfo, SECTION_ANY_TAG   );
-	add_assoc_image_info(return_value, sub_arrays, &ImageInfo, SECTION_IFD0      );
-	add_assoc_image_info(return_value, 1,          &ImageInfo, SECTION_THUMBNAIL );
-	add_assoc_image_info(return_value, 1,          &ImageInfo, SECTION_COMMENT   );
-	add_assoc_image_info(return_value, sub_arrays, &ImageInfo, SECTION_EXIF      );
-	add_assoc_image_info(return_value, sub_arrays, &ImageInfo, SECTION_GPS       );
-	add_assoc_image_info(return_value, sub_arrays, &ImageInfo, SECTION_INTEROP   );
-	add_assoc_image_info(return_value, sub_arrays, &ImageInfo, SECTION_FPIX      );
-	add_assoc_image_info(return_value, sub_arrays, &ImageInfo, SECTION_APP12     );
-	add_assoc_image_info(return_value, sub_arrays, &ImageInfo, SECTION_WINXP     );
-	add_assoc_image_info(return_value, sub_arrays, &ImageInfo, SECTION_MAKERNOTE );
+	if (UNEXPECTED(add_assoc_image_info(return_value, sub_arrays, &ImageInfo, SECTION_FILE) == FAILURE
+	 || add_assoc_image_info(return_value, 1,          &ImageInfo, SECTION_COMPUTED  ) == FAILURE
+	 || add_assoc_image_info(return_value, sub_arrays, &ImageInfo, SECTION_ANY_TAG   ) == FAILURE
+	 || add_assoc_image_info(return_value, sub_arrays, &ImageInfo, SECTION_IFD0      ) == FAILURE
+	 || add_assoc_image_info(return_value, 1,          &ImageInfo, SECTION_THUMBNAIL ) == FAILURE
+	 || add_assoc_image_info(return_value, 1,          &ImageInfo, SECTION_COMMENT   ) == FAILURE
+	 || add_assoc_image_info(return_value, sub_arrays, &ImageInfo, SECTION_EXIF      ) == FAILURE
+	 || add_assoc_image_info(return_value, sub_arrays, &ImageInfo, SECTION_GPS       ) == FAILURE
+	 || add_assoc_image_info(return_value, sub_arrays, &ImageInfo, SECTION_INTEROP   ) == FAILURE
+	 || add_assoc_image_info(return_value, sub_arrays, &ImageInfo, SECTION_FPIX      ) == FAILURE
+	 || add_assoc_image_info(return_value, sub_arrays, &ImageInfo, SECTION_APP12     ) == FAILURE
+	 || add_assoc_image_info(return_value, sub_arrays, &ImageInfo, SECTION_WINXP     ) == FAILURE
+	 || add_assoc_image_info(return_value, sub_arrays, &ImageInfo, SECTION_MAKERNOTE ) == FAILURE)) {
+		goto oom;
+	}

 #ifdef EXIF_DEBUG
 	exif_error_docref(NULL EXIFERR_CC, &ImageInfo, E_NOTICE, "Discarding info");
@@ -4716,6 +4777,13 @@ PHP_FUNCTION(exif_read_data)
 #ifdef EXIF_DEBUG
 	php_error_docref1(NULL, (Z_TYPE_P(stream) == IS_RESOURCE ? "<stream>" : Z_STRVAL_P(stream)), E_NOTICE, "Done");
 #endif
+	return;
+
+oom:
+	exif_discard_imageinfo(&ImageInfo);
+	zval_ptr_dtor(return_value);
+	ZVAL_NULL(return_value);
+	RETURN_OOM();
 }
 /* }}} */

@@ -4780,7 +4848,7 @@ PHP_FUNCTION(exif_thumbnail)
 	exif_error_docref(NULL EXIFERR_CC, &ImageInfo, E_NOTICE, "Returning thumbnail(%d)", ImageInfo.Thumbnail.size);
 #endif

-	ZVAL_STRINGL(return_value, ImageInfo.Thumbnail.data, ImageInfo.Thumbnail.size);
+	RETVAL_FSTRINGL(ImageInfo.Thumbnail.data, ImageInfo.Thumbnail.size);
 	if (arg_c >= 3) {
 		if (!ImageInfo.Thumbnail.width || !ImageInfo.Thumbnail.height) {
 			if (!exif_scan_thumbnail(&ImageInfo)) {
diff --git a/ext/exif/tests/oom_exif_big_thumbnail.inc b/ext/exif/tests/oom_exif_big_thumbnail.inc
new file mode 100644
index 00000000000..62e7aaa0af5
--- /dev/null
+++ b/ext/exif/tests/oom_exif_big_thumbnail.inc
@@ -0,0 +1,47 @@
+<?php
+
+/* Writes a TIFF whose thumbnail IFD points at $size bytes that really are in the
+ * file. */
+function oom_exif_write_big_thumbnail(string $path, int $size): void
+{
+    $ifd1_off    = 8 + 2 + 2*12 + 4;
+    $payload_off = $ifd1_off + 2 + 2*12 + 4;
+
+    $t  = "II*\x00" . pack('V', 8);
+    $t .= pack('v', 2);
+    $t .= pack('vvVV', 0x0100, 4, 1, 1);                // ImageWidth
+    $t .= pack('vvVV', 0x0101, 4, 1, 1);                // ImageLength
+    $t .= pack('V', $ifd1_off);                         // next IFD = thumbnail IFD
+    $t .= pack('v', 2);
+    $t .= pack('vvVV', 0x0111, 4, 1, $payload_off);     // StripOffsets -> real data
+    $t .= pack('vvVV', 0x0117, 4, 1, $size);            // StripByteCounts
+    $t .= pack('V', 0);
+
+    $fp = fopen($path, 'wb');
+    fwrite($fp, $t);
+    $chunk = str_repeat("\xAB", 1024 * 1024);
+    for ($written = 0; $written < $size; $written += strlen($chunk)) {
+        fwrite($fp, $chunk);
+    }
+    fclose($fp);
+}
+
+/* Writes a TIFF with a single tag of $count elements of $format, whose value
+ * really is present in the file. $bytes_per is the format's element size. */
+function oom_exif_write_big_tag(string $path, int $tag, int $format, int $count, int $bytes_per = 1): void
+{
+    $payload_off = 8 + 2 + 12 + 4;
+    $t  = "II*\x00" . pack('V', 8);
+    $t .= pack('v', 1);
+    $t .= pack('vvVV', $tag, $format, $count, $payload_off);
+    $t .= pack('V', 0);
+
+    $fp = fopen($path, 'wb');
+    fwrite($fp, $t);
+    $size = $count * $bytes_per;
+    $chunk = str_repeat("\x01", 1024 * 1024);
+    for ($written = 0; $written < $size; $written += strlen($chunk)) {
+        fwrite($fp, substr($chunk, 0, min(strlen($chunk), $size - $written)));
+    }
+    fclose($fp);
+}
diff --git a/ext/exif/tests/oom_exif_numeric_metadata.phpt b/ext/exif/tests/oom_exif_numeric_metadata.phpt
new file mode 100644
index 00000000000..93b9027923b
--- /dev/null
+++ b/ext/exif/tests/oom_exif_numeric_metadata.phpt
@@ -0,0 +1,38 @@
+--TEST--
+exif_read_data() with expanded numeric metadata doesn't reach hard limit
+--EXTENSIONS--
+exif
+--INI--
+memory_limit=128M
+--FILE--
+<?php
+$base = file_get_contents(__DIR__ . '/image011.jpg');
+$entryCount = 900;
+$componentCount = 12000;
+$dataOffset = 8 + 2 + $entryCount * 12 + 4;
+
+$entries = '';
+for ($i = 0; $i < $entryCount; $i++) {
+    // Each unique tag retains its own numeric list, while all tags share the
+    // same in-bounds payload in the JPEG's TIFF data.
+    $entries .= pack('vvVV', 0xf000 + $i, 3, $componentCount, $dataOffset);
+}
+
+$tiff = "II*\0" . pack('V', 8)
+    . pack('v', $entryCount) . $entries . pack('V', 0)
+    . str_repeat("\0", $componentCount * 2);
+$app1 = "Exif\0\0" . $tiff;
+$jpeg = "\xff\xd8\xff\xe1" . pack('n', strlen($app1) + 2) . $app1 . substr($base, 2);
+
+$file = __DIR__ . '/oom_exif_numeric_metadata.jpg';
+file_put_contents($file, $jpeg);
+unset($base, $entries, $tiff, $app1, $jpeg);
+
+exif_read_data($file);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/oom_exif_numeric_metadata.jpg');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 134217728 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/exif/tests/oom_exif_read_data_tags.phpt b/ext/exif/tests/oom_exif_read_data_tags.phpt
new file mode 100644
index 00000000000..acb8b3622ff
--- /dev/null
+++ b/ext/exif/tests/oom_exif_read_data_tags.phpt
@@ -0,0 +1,38 @@
+--TEST--
+exif_read_data() doesn't reach hard limit when tag values accumulate
+--EXTENSIONS--
+exif
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+// Each IFD entry is 12 bytes but claims a 256 KB value, so the retained
+// values add up to far more than the file itself.
+$n = 1000;
+$payload_len = 256 * 1024;
+$payload_off = 8 + 2 + $n*12 + 4;
+
+$entries = '';
+for ($i = 0; $i < $n; $i++) {
+    $entries .= pack('vvVV', 0x9000 + ($i & 0xFF), 1, $payload_len, $payload_off);
+}
+$t = "II*\x00" . pack('V', 8)
+   . pack('v', $n) . $entries . pack('V', 0)
+   . str_repeat("A", $payload_len);
+
+$file = __DIR__ . '/oom_exif_read_data_tags.tif';
+file_put_contents($file, $t);
+unset($entries, $t);
+
+var_dump(exif_read_data($file, null, true));
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/oom_exif_read_data_tags.tif');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/exif/tests/oom_exif_scan_jpeg_header.phpt b/ext/exif/tests/oom_exif_scan_jpeg_header.phpt
new file mode 100644
index 00000000000..01f9085bac3
--- /dev/null
+++ b/ext/exif/tests/oom_exif_scan_jpeg_header.phpt
@@ -0,0 +1,29 @@
+--TEST--
+exif_read_data() doesn't reach hard limit when JPEG sections accumulate
+--EXTENSIONS--
+exif
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+// Every APP9 section is retained, and a minimal one is only 4 bytes, so the
+// retained sections add up to many times the file itself.
+$n = (int)((2*1024*1024 - 4) / 4);
+$jpeg = "\xFF\xD8" . str_repeat("\xFF\xE9\x00\x02", $n) . "\xFF\xD9";
+
+$file = __DIR__ . '/oom_exif_scan_jpeg_header.jpg';
+file_put_contents($file, $jpeg);
+unset($jpeg);
+
+var_dump(exif_read_data($file));
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/oom_exif_scan_jpeg_header.jpg');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/exif/tests/oom_exif_thumbnail_large_file_001.phpt b/ext/exif/tests/oom_exif_thumbnail_large_file_001.phpt
new file mode 100644
index 00000000000..e2568375c0f
--- /dev/null
+++ b/ext/exif/tests/oom_exif_thumbnail_large_file_001.phpt
@@ -0,0 +1,21 @@
+--TEST--
+exif_thumbnail() doesn't reach hard limit when the thumbnail is larger than the memory limit
+--EXTENSIONS--
+exif
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+require __DIR__ . '/oom_exif_big_thumbnail.inc';
+
+$file = __DIR__ . '/oom_exif_thumbnail_large_file_001.tif';
+oom_exif_write_big_thumbnail($file, 12 * 1024 * 1024);
+
+var_dump(exif_thumbnail($file));
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/oom_exif_thumbnail_large_file_001.tif');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/exif/tests/oom_exif_thumbnail_large_file_002.phpt b/ext/exif/tests/oom_exif_thumbnail_large_file_002.phpt
new file mode 100644
index 00000000000..0ad9f2d015e
--- /dev/null
+++ b/ext/exif/tests/oom_exif_thumbnail_large_file_002.phpt
@@ -0,0 +1,22 @@
+--TEST--
+exif_read_data() doesn't reach hard limit reading a thumbnail larger than the hard limit
+--EXTENSIONS--
+exif
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+require __DIR__ . '/oom_exif_big_thumbnail.inc';
+
+// Here the thumbnail is larger than the hard limit on its own
+$file = __DIR__ . '/oom_exif_thumbnail_large_file_002.tif';
+oom_exif_write_big_thumbnail($file, 20 * 1024 * 1024);
+
+var_dump(exif_read_data($file, null, false, true));
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/oom_exif_thumbnail_large_file_002.tif');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/exif/tests/oom_exif_thumbnail_tiff.phpt b/ext/exif/tests/oom_exif_thumbnail_tiff.phpt
new file mode 100644
index 00000000000..a074e7c1283
--- /dev/null
+++ b/ext/exif/tests/oom_exif_thumbnail_tiff.phpt
@@ -0,0 +1,32 @@
+--TEST--
+exif_thumbnail() rejects a TIFF thumbnail that is not present in the file
+--EXTENSIONS--
+exif
+--INI--
+memory_limit=128M
+--FILE--
+<?php
+// 68 byte TIFF whose thumbnail IFD claims a 4 GiB StripByteCounts.
+$ifd1_off = 8 + 2 + 2*12 + 4;
+$t  = "II*\x00" . pack("V", 8);
+$t .= pack("v", 2);
+$t .= pack("vvVV", 0x0100, 4, 1, 1);          // ImageWidth
+$t .= pack("vvVV", 0x0101, 4, 1, 1);          // ImageLength
+$t .= pack("V", $ifd1_off);                   // next IFD = thumbnail IFD
+$t .= pack("v", 2);
+$t .= pack("vvVV", 0x0111, 4, 1, 1);          // StripOffsets
+$t .= pack("vvVV", 0x0117, 4, 1, -1);         // StripByteCounts = 4 GiB
+$t .= pack("V", 0);
+
+$file = __DIR__ . '/oom_exif_thumbnail_tiff.tif';
+file_put_contents($file, $t);
+
+var_dump(exif_thumbnail($file));
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/oom_exif_thumbnail_tiff.tif');
+?>
+--EXPECTF--
+Warning: exif_thumbnail(%s): Thumbnail goes IFD boundary or end of file reached in %s on line %d
+bool(false)
diff --git a/ext/fileinfo/libmagic.patch b/ext/fileinfo/libmagic.patch
index ccd5985c6ef..5920bec5e3b 100644
--- a/ext/fileinfo/libmagic.patch
+++ b/ext/fileinfo/libmagic.patch
@@ -1041,7 +1041,7 @@ diff -u libmagic.orig/buffer.c libmagic/buffer.c
  	}
 diff -u libmagic.orig/cdf.c libmagic/cdf.c
 --- libmagic.orig/cdf.c	2021-02-23 01:49:06.000000000 +0100
-+++ libmagic/cdf.c	2023-12-09 11:51:31.704229532 +0100
++++ libmagic/cdf.c	2026-10-02 19:29:25.144332062 +0200
 @@ -43,7 +43,17 @@
  #include <err.h>
  #endif
@@ -1060,51 +1060,57 @@ diff -u libmagic.orig/cdf.c libmagic/cdf.c
  #include <string.h>
  #include <time.h>
  #include <ctype.h>
-@@ -85,40 +95,9 @@
- 			    CDF_TOLE8(CAST(uint64_t, x))))
- #define CDF_GETUINT32(x, y)	cdf_getuint32(x, y)
+@@ -89,35 +99,39 @@
+ #define CDF_REALLOC(p, n) cdf_realloc(__FILE__, __LINE__, (p), (n))
+ #define CDF_CALLOC(n, u) cdf_calloc(__FILE__, __LINE__, (n), (u))

--#define CDF_MALLOC(n) cdf_malloc(__FILE__, __LINE__, (n))
--#define CDF_REALLOC(p, n) cdf_realloc(__FILE__, __LINE__, (p), (n))
--#define CDF_CALLOC(n, u) cdf_calloc(__FILE__, __LINE__, (n), (u))
 -
--
--/*ARGSUSED*/
--static void *
+ /*ARGSUSED*/
+ static void *
 -cdf_malloc(const char *file __attribute__((__unused__)),
 -    size_t line __attribute__((__unused__)), size_t n)
--{
--	DPRINTF(("%s,%" SIZE_T_FORMAT "u: %s %" SIZE_T_FORMAT "u\n",
--	    file, line, __func__, n));
++cdf_malloc(const char *file,
++    size_t line, size_t n)
+ {
+ 	DPRINTF(("%s,%" SIZE_T_FORMAT "u: %s %" SIZE_T_FORMAT "u\n",
+ 	    file, line, __func__, n));
 -	return malloc(n);
--}
--
--/*ARGSUSED*/
--static void *
++	return femalloc(n, /* fallible */ true);
+ }
+
+ /*ARGSUSED*/
+ static void *
 -cdf_realloc(const char *file __attribute__((__unused__)),
 -    size_t line __attribute__((__unused__)), void *p, size_t n)
--{
--	DPRINTF(("%s,%" SIZE_T_FORMAT "u: %s %" SIZE_T_FORMAT "u\n",
--	    file, line, __func__, n));
++cdf_realloc(const char *file,
++    size_t line, void *p, size_t n)
+ {
+ 	DPRINTF(("%s,%" SIZE_T_FORMAT "u: %s %" SIZE_T_FORMAT "u\n",
+ 	    file, line, __func__, n));
 -	return realloc(p, n);
--}
--
--/*ARGSUSED*/
--static void *
++	return safe_ferealloc(p, 1, n, 0, /* fallible */ true);
+ }
+
+ /*ARGSUSED*/
+ static void *
 -cdf_calloc(const char *file __attribute__((__unused__)),
 -    size_t line __attribute__((__unused__)), size_t n, size_t u)
--{
--	DPRINTF(("%s,%" SIZE_T_FORMAT "u: %s %" SIZE_T_FORMAT "u %"
--	    SIZE_T_FORMAT "u\n", file, line, __func__, n, u));
++cdf_calloc(const char *file,
++    size_t line, size_t n, size_t u)
+ {
+ 	DPRINTF(("%s,%" SIZE_T_FORMAT "u: %s %" SIZE_T_FORMAT "u %"
+ 	    SIZE_T_FORMAT "u\n", file, line, __func__, n, u));
 -	return calloc(n, u);
--}
-+#define CDF_MALLOC(n) emalloc(n)
-+#define CDF_REALLOC(p, n) erealloc(p, n)
-+#define CDF_CALLOC(n, u) ecalloc(n, u)
++	void *data = safe_femalloc(n, u, 0, /* fallible */ true);
++	if (!data) {
++		return NULL;
++	}
++	memset(data, 0, n * u);
++	return data;
+ }

  /*
-  * swap a short
-@@ -314,7 +293,7 @@
+@@ -314,7 +328,7 @@
  	scn->sst_len = 0;
  	scn->sst_dirlen = 0;
  	scn->sst_ss = 0;
@@ -1113,7 +1119,7 @@ diff -u libmagic.orig/cdf.c libmagic/cdf.c
  	scn->sst_tab = NULL;
  	return -1;
  }
-@@ -322,9 +301,11 @@
+@@ -322,9 +336,11 @@
  static size_t
  cdf_check_stream(const cdf_stream_t *sst, const cdf_header_t *h)
  {
@@ -1125,7 +1131,7 @@ diff -u libmagic.orig/cdf.c libmagic/cdf.c
  	return sst->sst_ss;
  }

-@@ -347,11 +328,11 @@
+@@ -347,11 +363,11 @@
  }

  static ssize_t
@@ -1139,7 +1145,7 @@ diff -u libmagic.orig/cdf.c libmagic/cdf.c
  		goto out;

  	if (info->i_buf != NULL && info->i_len >= siz) {
-@@ -362,7 +343,10 @@
+@@ -362,7 +378,10 @@
  	if (info->i_fd == -1)
  		goto out;

@@ -1151,7 +1157,7 @@ diff -u libmagic.orig/cdf.c libmagic/cdf.c
  		return -1;

  	return CAST(ssize_t, len);
-@@ -377,7 +361,7 @@
+@@ -377,7 +396,7 @@
  	char buf[512];

  	(void)memcpy(cdf_bo.s, "\01\02\03\04", 4);
@@ -1160,7 +1166,7 @@ diff -u libmagic.orig/cdf.c libmagic/cdf.c
  		return -1;
  	cdf_unpack_header(h, buf);
  	cdf_swap_header(h);
-@@ -524,14 +508,14 @@
+@@ -524,14 +543,14 @@
  	}
  out:
  	sat->sat_len = i;
@@ -1178,7 +1184,7 @@ diff -u libmagic.orig/cdf.c libmagic/cdf.c
  	return -1;
  }

-@@ -699,7 +683,7 @@
+@@ -699,7 +718,7 @@
  		return -1;

  	if ((buf = CAST(char *, CDF_MALLOC(ss))) == NULL) {
@@ -1187,7 +1193,7 @@ diff -u libmagic.orig/cdf.c libmagic/cdf.c
  		return -1;
  	}

-@@ -722,11 +706,11 @@
+@@ -722,11 +741,11 @@
  	if (NEED_SWAP)
  		for (i = 0; i < dir->dir_len; i++)
  			cdf_swap_dir(&dir->dir_tab[i]);
@@ -1202,7 +1208,7 @@ diff -u libmagic.orig/cdf.c libmagic/cdf.c
  	errno = EFTYPE;
  	return -1;
  }
-@@ -771,7 +755,7 @@
+@@ -771,7 +790,7 @@
  out:
  	errno = EFTYPE;
  out1:
@@ -1211,7 +1217,7 @@ diff -u libmagic.orig/cdf.c libmagic/cdf.c
  	return -1;
  }

-@@ -933,7 +917,7 @@
+@@ -933,7 +952,7 @@
  	*maxcount = newcount;
  	return inp;
  out:
@@ -1220,7 +1226,7 @@ diff -u libmagic.orig/cdf.c libmagic/cdf.c
  	*maxcount = 0;
  	*info = NULL;
  	return NULL;
-@@ -1115,7 +1099,7 @@
+@@ -1115,7 +1134,7 @@
  	}
  	return 0;
  out:
@@ -1229,7 +1235,7 @@ diff -u libmagic.orig/cdf.c libmagic/cdf.c
  	*info = NULL;
  	*count = 0;
  	*maxcount = 0;
-@@ -1407,7 +1391,7 @@
+@@ -1407,7 +1426,7 @@
  	cdf_directory_t *d;
  	char name[__arraycount(d->d_name)];
  	cdf_stream_t scn;
@@ -1238,7 +1244,7 @@ diff -u libmagic.orig/cdf.c libmagic/cdf.c

  	static const char *types[] = { "empty", "user storage",
  	    "user stream", "lockbytes", "property", "root storage" };
-@@ -1449,7 +1433,7 @@
+@@ -1449,7 +1468,7 @@
  				break;
  			}
  			cdf_dump_stream(&scn);
@@ -1247,7 +1253,7 @@ diff -u libmagic.orig/cdf.c libmagic/cdf.c
  			break;
  		default:
  			break;
-@@ -1462,7 +1446,7 @@
+@@ -1462,7 +1481,7 @@
  cdf_dump_property_info(const cdf_property_info_t *info, size_t count)
  {
  	cdf_timestamp_t tp;
@@ -1256,7 +1262,7 @@ diff -u libmagic.orig/cdf.c libmagic/cdf.c
  	char buf[64];
  	size_t i, j;

-@@ -1547,7 +1531,7 @@
+@@ -1547,7 +1566,7 @@
  	(void)fprintf(stderr, "Class %s\n", buf);
  	(void)fprintf(stderr, "Count %d\n", ssi.si_count);
  	cdf_dump_property_info(info, count);
@@ -1265,7 +1271,7 @@ diff -u libmagic.orig/cdf.c libmagic/cdf.c
  }


-@@ -1568,7 +1552,7 @@
+@@ -1568,7 +1587,7 @@
  		    cdf_u16tos8(sbuf, ce[i].ce_namlen, ce[i].ce_name),
  		    cdf_ctime(&ts.tv_sec, tbuf));
  	}
diff --git a/ext/fileinfo/libmagic/cdf.c b/ext/fileinfo/libmagic/cdf.c
index 1a08dd7c0d8..cd08c01a398 100644
--- a/ext/fileinfo/libmagic/cdf.c
+++ b/ext/fileinfo/libmagic/cdf.c
@@ -95,9 +95,44 @@ static union {
 			    CDF_TOLE8(CAST(uint64_t, x))))
 #define CDF_GETUINT32(x, y)	cdf_getuint32(x, y)

-#define CDF_MALLOC(n) emalloc(n)
-#define CDF_REALLOC(p, n) erealloc(p, n)
-#define CDF_CALLOC(n, u) ecalloc(n, u)
+#define CDF_MALLOC(n) cdf_malloc(__FILE__, __LINE__, (n))
+#define CDF_REALLOC(p, n) cdf_realloc(__FILE__, __LINE__, (p), (n))
+#define CDF_CALLOC(n, u) cdf_calloc(__FILE__, __LINE__, (n), (u))
+
+/*ARGSUSED*/
+static void *
+cdf_malloc(const char *file,
+    size_t line, size_t n)
+{
+	DPRINTF(("%s,%" SIZE_T_FORMAT "u: %s %" SIZE_T_FORMAT "u\n",
+	    file, line, __func__, n));
+	return femalloc(n, /* fallible */ true);
+}
+
+/*ARGSUSED*/
+static void *
+cdf_realloc(const char *file,
+    size_t line, void *p, size_t n)
+{
+	DPRINTF(("%s,%" SIZE_T_FORMAT "u: %s %" SIZE_T_FORMAT "u\n",
+	    file, line, __func__, n));
+	return safe_ferealloc(p, 1, n, 0, /* fallible */ true);
+}
+
+/*ARGSUSED*/
+static void *
+cdf_calloc(const char *file,
+    size_t line, size_t n, size_t u)
+{
+	DPRINTF(("%s,%" SIZE_T_FORMAT "u: %s %" SIZE_T_FORMAT "u %"
+	    SIZE_T_FORMAT "u\n", file, line, __func__, n, u));
+	void *data = safe_femalloc(n, u, 0, /* fallible */ true);
+	if (!data) {
+		return NULL;
+	}
+	memset(data, 0, n * u);
+	return data;
+}

 /*
  * swap a short
diff --git a/ext/fileinfo/tests/oom_cdf_sector_chain.phpt b/ext/fileinfo/tests/oom_cdf_sector_chain.phpt
new file mode 100644
index 00000000000..18cd575d639
--- /dev/null
+++ b/ext/fileinfo/tests/oom_cdf_sector_chain.phpt
@@ -0,0 +1,76 @@
+--TEST--
+fileinfo on cdf file doesn't reach hard limit
+--EXTENSIONS--
+fileinfo
+--FILE--
+<?php
+/*
+ * The header advertises 32 KiB sectors and two FAT sectors.  The FAT contains
+ * a 10,000-entry mini-FAT chain even though the file has only three sectors.
+ * cdf_read_ssat() consequently attempts to allocate 10,000 * 32 KiB before
+ * validating that the chain data is present in the input.
+ */
+$sectorSize = 32 * 1024;
+$header =
+    "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1" . // CDF signature
+    str_repeat("\0", 16) .                 // CLSID
+    pack('vvvvv', 0x3e, 4, 0xfffe, 15, 6) .
+    str_repeat("\0", 10) .
+    pack('V8',
+        2,          // Number of FAT sectors
+        2,          // First directory sector
+        0,          // Transaction signature
+        4096,       // Mini-stream cutoff
+        2,          // First mini-FAT sector
+        10000,      // Number of mini-FAT sectors
+        -2,         // End of DIFAT chain
+        0,          // Number of DIFAT sectors
+    ) .
+    pack('V2', 0, 1) .
+    str_repeat(pack('V', -1), 107);
+assert(strlen($header) === 512);
+
+$fat = array_fill(0, 16384, -1);
+$fat[0] = -3;
+$fat[1] = -3;
+for ($i = 2; $i < 10001; $i++) {
+    $fat[$i] = $i + 1;
+}
+$fat[10001] = -2;
+
+$path = __DIR__ . '/oom_cdf_sector_chain.cdf';
+$file = fopen($path, 'wb');
+fwrite($file, $header);
+fwrite($file, str_repeat("\0", $sectorSize - strlen($header)));
+foreach ($fat as $sector) {
+    fwrite($file, pack('V', $sector));
+}
+fwrite($file, str_repeat("\0", $sectorSize));
+fclose($file);
+
+function run(string $code): void
+{
+    $command = escapeshellarg(getenv('TEST_PHP_EXECUTABLE')) . ' ' .  getenv('TEST_PHP_EXTRA_ARGS') .  ' -d memory_limit=128M -d html_errors=0 -r ' .  escapeshellarg($code);
+    $process = proc_open($command, [STDIN, STDOUT, STDERR], $pipes);
+    $status = proc_close($process);
+    echo "exit_status=$status\n";
+}
+
+$exportedPath = var_export($path, true);
+run('$f = finfo_open(FILEINFO_MIME_TYPE); finfo_file($f, ' . $exportedPath . ');');
+run('$f = finfo_open(FILEINFO_MIME_TYPE); finfo_buffer($f, file_get_contents(' . $exportedPath . '));');
+run('mime_content_type(' . $exportedPath . ');');
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/oom_cdf_sector_chain.cdf');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 134217728 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+exit_status=255
+
+Fatal error: Allowed memory size of 134217728 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+exit_status=255
+
+Fatal error: Allowed memory size of 134217728 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+exit_status=255
diff --git a/ext/filter/filter.c b/ext/filter/filter.c
index b787ef706c4..605196e33ae 100644
--- a/ext/filter/filter.c
+++ b/ext/filter/filter.c
@@ -332,17 +332,25 @@ static unsigned int php_sapi_filter(int arg, const char *var, char **val, size_t

 	if (array_ptr) {
 		/* Store the RAW variable internally */
-		ZVAL_STRINGL(&raw_var, *val, val_len);
+		if (UNEXPECTED(!ZVAL_FSTRINGL(&raw_var, *val, val_len))) {
+			return 0;
+		}
 		php_register_variable_ex(var, &raw_var, array_ptr);
+		if (ZEND_HAS_OOM()) {
+			return 0;
+		}
 	}

 	if (val_len) {
 		/* Register mangled variable */
+		if (UNEXPECTED(!ZVAL_FSTRINGL(&new_var, *val, val_len))) {
+			return 0;
+		}
 		if (IF_G(default_filter) != FILTER_UNSAFE_RAW) {
-			ZVAL_STRINGL(&new_var, *val, val_len);
 			php_zval_filter(&new_var, IF_G(default_filter), IF_G(default_filter_flags), NULL, NULL, 0);
-		} else {
-			ZVAL_STRINGL(&new_var, *val, val_len);
+			if (ZEND_HAS_OOM()) {
+				return 0;
+			}
 		}
 	} else { /* empty string */
 		ZVAL_EMPTY_STRING(&new_var);
@@ -358,7 +366,18 @@ static unsigned int php_sapi_filter(int arg, const char *var, char **val, size_t
 		}
 		efree(*val);
 		if (Z_STRLEN(new_var)) {
-			*val = estrndup(Z_STRVAL(new_var), Z_STRLEN(new_var));
+			*val = femalloc(Z_STRLEN(new_var) + 1, /* fallible */ true);
+			if (UNEXPECTED(!*val)) {
+				/* The caller frees *val either way, so leave it pointing at
+				 * something. */
+				*val = estrdup("");
+				if (new_val_len) {
+					*new_val_len = 0;
+				}
+				zval_ptr_dtor(&new_var);
+				return 0;
+			}
+			memcpy(*val, Z_STRVAL(new_var), Z_STRLEN(new_var) + 1);
 		} else {
 			*val = estrdup("");
 		}
diff --git a/ext/filter/sanitizing_filters.c b/ext/filter/sanitizing_filters.c
index 585277c2664..141c413499b 100644
--- a/ext/filter/sanitizing_filters.c
+++ b/ext/filter/sanitizing_filters.c
@@ -23,7 +23,7 @@ typedef unsigned long filter_map[256];
 /* }}} */

 /* {{{ HELPER FUNCTIONS */
-static void php_filter_encode_html(zval *value, const unsigned char *chars)
+static bool php_filter_encode_html(zval *value, const unsigned char *chars)
 {
 	smart_str str = {0};
 	size_t len = Z_STRLEN_P(value);
@@ -31,7 +31,7 @@ static void php_filter_encode_html(zval *value, const unsigned char *chars)
 	unsigned char *e = s + len;

 	if (Z_STRLEN_P(value) == 0) {
-		return;
+		return true;
 	}

 	while (s < e) {
@@ -44,10 +44,18 @@ static void php_filter_encode_html(zval *value, const unsigned char *chars)
 			smart_str_appendc(&str, *s);
 		}
 		s++;
+
+		if (ZEND_HAS_OOM()) {
+			smart_str_free(&str);
+			zval_ptr_dtor(value);
+			ZVAL_EMPTY_STRING(value);
+			return false;
+		}
 	}

 	zval_ptr_dtor(value);
 	ZVAL_STR(value, smart_str_extract(&str));
+	return true;
 }

 static const unsigned char hexchars[] = "0123456789ABCDEF";
@@ -72,7 +80,12 @@ static void php_filter_encode_url(zval *value, const unsigned char* chars, const
 		tmp[*s++] = '\0';
 	}

-	str = zend_string_safe_alloc(Z_STRLEN_P(value), 3, 0, 0);
+	str = zend_string_safe_falloc(Z_STRLEN_P(value), 3, 0, /* persistent */ false, /* fallible */ true);
+	if (!str) {
+		zval_ptr_dtor(value);
+		ZVAL_EMPTY_STRING(value);
+		return;
+	}
 	p = (unsigned char *) ZSTR_VAL(str);
 	s = (unsigned char *) Z_STRVAL_P(value);
 	e = s + Z_STRLEN_P(value);
@@ -190,7 +203,9 @@ void php_filter_string(PHP_INPUT_FILTER_PARAM_DECL)
 		memset(enc + 127, 1, sizeof(enc) - 127);
 	}

-	php_filter_encode_html(value, enc);
+	if (!php_filter_encode_html(value, enc)) {
+		return;
+	}

 	/* strip tags, implicitly also removes \0 chars */
 	new_len = php_strip_tags_ex(Z_STRVAL_P(value), Z_STRLEN_P(value), NULL, 0, 1);
diff --git a/ext/filter/tests/oom_filter_default_encoded.phpt b/ext/filter/tests/oom_filter_default_encoded.phpt
new file mode 100644
index 00000000000..4e81326639a
--- /dev/null
+++ b/ext/filter/tests/oom_filter_default_encoded.phpt
@@ -0,0 +1,126 @@
+--TEST--
+filter.default=encoded doesn't reach hard limit while parsing POST data
+--EXTENSIONS--
+filter
+--SKIPIF--
+<?php
+if (!getenv('TEST_PHP_CGI_EXECUTABLE')) {
+    die("skip php-cgi not available");
+}
+?>
+--FILE--
+<?php
+$script = __DIR__ . '/' . basename(__FILE__, '.php') . '.target.php';
+file_put_contents($script, <<<'TARGET'
+<?php
+echo "no OOM\n";
+TARGET);
+
+function oom_post_encoded(string $body, string $script): void
+{
+    $p = proc_open(
+        [
+            getenv('TEST_PHP_CGI_EXECUTABLE'), '-n',
+            '-d', 'memory_limit=8M',
+            '-d', 'post_max_size=1G',
+            '-d', 'html_errors=0',
+            '-d', 'filter.default=encoded',
+            '-d', 'filter.default_flags=0',
+            /* the conversion is what is under test, not the deprecation */
+            '-d', 'error_reporting=' . (E_ALL & ~E_DEPRECATED),
+        ],
+        [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
+        $pipes,
+        null,
+        [
+            'REQUEST_METHOD'  => 'POST',
+            'CONTENT_TYPE'    => 'application/x-www-form-urlencoded',
+            'CONTENT_LENGTH'  => (string) strlen($body),
+            'SCRIPT_FILENAME' => $script,
+            'REDIRECT_STATUS' => '1',
+        ]
+    );
+    fwrite($pipes[0], $body);
+    fclose($pipes[0]);
+    $out = stream_get_contents($pipes[1]) . stream_get_contents($pipes[2]);
+    fclose($pipes[1]);
+    fclose($pipes[2]);
+    proc_close($p);
+
+    echo $out;
+}
+
+for ($kb = 2016; $kb <= 2064; $kb += 4) {
+    oom_post_encoded('a=' . str_repeat('x', $kb * 1024), $script);
+}
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.target.php');
+?>
+--EXPECTF--
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/filter/tests/oom_filter_input.phpt b/ext/filter/tests/oom_filter_input.phpt
new file mode 100644
index 00000000000..aef71dff563
--- /dev/null
+++ b/ext/filter/tests/oom_filter_input.phpt
@@ -0,0 +1,56 @@
+--TEST--
+filter_input() doesn't reach hard limit
+--EXTENSIONS--
+filter
+--SKIPIF--
+<?php
+if (!getenv('TEST_PHP_CGI_EXECUTABLE')) {
+    die("skip php-cgi not available");
+}
+?>
+--FILE--
+<?php
+$script = __DIR__ . '/' . basename(__FILE__, '.php') . '.target.php';
+file_put_contents($script, <<<'TARGET'
+<?php
+filter_input(INPUT_POST, 'k', FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_ENCODE_HIGH);
+echo "no OOM\n";
+TARGET);
+
+$boundary = '----X';
+$body = "--$boundary\r\nContent-Disposition: form-data; name=\"k\"\r\n\r\n"
+      . str_repeat("\xFF", 2 * 1024 * 1024)
+      . "\r\n--$boundary--\r\n";
+
+$p = proc_open(
+    [getenv('TEST_PHP_CGI_EXECUTABLE'), '-n', '-d', 'memory_limit=12M', '-d', 'html_errors=0'],
+    [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
+    $pipes,
+    null,
+    [
+        'REQUEST_METHOD'  => 'POST',
+        'CONTENT_TYPE'    => "multipart/form-data; boundary=$boundary",
+        'CONTENT_LENGTH'  => (string) strlen($body),
+        'SCRIPT_FILENAME' => $script,
+        'REDIRECT_STATUS' => '1',
+    ]
+);
+fwrite($pipes[0], $body);
+fclose($pipes[0]);
+$out = stream_get_contents($pipes[1]) . stream_get_contents($pipes[2]);
+fclose($pipes[1]);
+fclose($pipes[2]);
+proc_close($p);
+
+echo $out;
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.target.php');
+?>
+--EXPECTF--
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 12582912 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/filter/tests/oom_filter_input_array.phpt b/ext/filter/tests/oom_filter_input_array.phpt
new file mode 100644
index 00000000000..a8e2712271c
--- /dev/null
+++ b/ext/filter/tests/oom_filter_input_array.phpt
@@ -0,0 +1,56 @@
+--TEST--
+filter_input_array() doesn't reach hard limit
+--EXTENSIONS--
+filter
+--SKIPIF--
+<?php
+if (!getenv('TEST_PHP_CGI_EXECUTABLE')) {
+    die("skip php-cgi not available");
+}
+?>
+--FILE--
+<?php
+$script = __DIR__ . '/' . basename(__FILE__, '.php') . '.target.php';
+file_put_contents($script, <<<'TARGET'
+<?php
+filter_input_array(INPUT_POST, ['k' => ['filter' => FILTER_SANITIZE_SPECIAL_CHARS, 'flags' => FILTER_FLAG_ENCODE_HIGH]]);
+echo "no OOM\n";
+TARGET);
+
+$boundary = '----X';
+$body = "--$boundary\r\nContent-Disposition: form-data; name=\"k\"\r\n\r\n"
+      . str_repeat("\xFF", 2 * 1024 * 1024)
+      . "\r\n--$boundary--\r\n";
+
+$p = proc_open(
+    [getenv('TEST_PHP_CGI_EXECUTABLE'), '-n', '-d', 'memory_limit=12M', '-d', 'html_errors=0'],
+    [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
+    $pipes,
+    null,
+    [
+        'REQUEST_METHOD'  => 'POST',
+        'CONTENT_TYPE'    => "multipart/form-data; boundary=$boundary",
+        'CONTENT_LENGTH'  => (string) strlen($body),
+        'SCRIPT_FILENAME' => $script,
+        'REDIRECT_STATUS' => '1',
+    ]
+);
+fwrite($pipes[0], $body);
+fclose($pipes[0]);
+$out = stream_get_contents($pipes[1]) . stream_get_contents($pipes[2]);
+fclose($pipes[1]);
+fclose($pipes[2]);
+proc_close($p);
+
+echo $out;
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.target.php');
+?>
+--EXPECTF--
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+
+Fatal error: Allowed memory size of 12582912 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/filter/tests/oom_filter_var.phpt b/ext/filter/tests/oom_filter_var.phpt
new file mode 100644
index 00000000000..10685d7e9af
--- /dev/null
+++ b/ext/filter/tests/oom_filter_var.phpt
@@ -0,0 +1,17 @@
+--TEST--
+filter_var(filter: FILTER_SANITIZE_SPECIAL_CHARS) doesn't reach hard limit
+--EXTENSIONS--
+filter
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv('SKIP_ASAN')) die("skip ASan-instrumented realloc() is slow");
+?>
+--FILE--
+<?php
+$value = str_repeat("\xFF", 2 * 1024 * 1024);
+filter_var($value, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_ENCODE_HIGH);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/filter/tests/oom_filter_var_array.phpt b/ext/filter/tests/oom_filter_var_array.phpt
new file mode 100644
index 00000000000..417bf77ec25
--- /dev/null
+++ b/ext/filter/tests/oom_filter_var_array.phpt
@@ -0,0 +1,20 @@
+--TEST--
+filter_var_array() doesn't reach hard limit
+--EXTENSIONS--
+filter
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv('SKIP_ASAN')) die("skip ASan-instrumented realloc() is slow");
+?>
+--FILE--
+<?php
+$value = str_repeat("\xFF", 2 * 1024 * 1024);
+filter_var_array(
+    ["k" => $value],
+    ["k" => ["filter" => FILTER_SANITIZE_SPECIAL_CHARS, "flags" => FILTER_FLAG_ENCODE_HIGH]]
+);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/filter/tests/oom_filter_var_full_special_chars.phpt b/ext/filter/tests/oom_filter_var_full_special_chars.phpt
new file mode 100644
index 00000000000..fcb81f8ebb6
--- /dev/null
+++ b/ext/filter/tests/oom_filter_var_full_special_chars.phpt
@@ -0,0 +1,17 @@
+--TEST--
+filter_var(filter: FILTER_SANITIZE_FULL_SPECIAL_CHARS) doesn't reach hard limit
+--EXTENSIONS--
+filter
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv('SKIP_ASAN')) die("skip ASan-instrumented realloc() is slow");
+?>
+--FILE--
+<?php
+$value = str_repeat("'", 2 * 1024 * 1024);
+filter_var($value, FILTER_SANITIZE_FULL_SPECIAL_CHARS);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/gd/libgd/gd.c b/ext/gd/libgd/gd.c
index 2664288162a..1fda19a6b21 100644
--- a/ext/gd/libgd/gd.c
+++ b/ext/gd/libgd/gd.c
@@ -186,7 +186,11 @@ gdImagePtr gdImageCreate (int sx, int sy)
 	im = (gdImage *) gdCalloc(1, sizeof(gdImage));

 	/* Row-major ever since gd 1.3 */
-	im->pixels = (unsigned char **) gdMalloc(sizeof(unsigned char *) * sy);
+	im->pixels = (unsigned char **) gdFMalloc(sizeof(unsigned char *) * sy);
+	if (im->pixels == NULL) {
+		gdFree(im);
+		return NULL;
+	}
 	im->polyInts = 0;
 	im->polyAllocated = 0;
 	im->brush = 0;
@@ -194,7 +198,15 @@ gdImagePtr gdImageCreate (int sx, int sy)
 	im->style = 0;
 	for (i = 0; i < sy; i++) {
 		/* Row-major ever since gd 1.3 */
-		im->pixels[i] = (unsigned char *) gdCalloc(sx, sizeof(unsigned char));
+		im->pixels[i] = (unsigned char *) gdFCalloc(sx, sizeof(unsigned char));
+		if (im->pixels[i] == NULL) {
+			while (i > 0) {
+				gdFree(im->pixels[--i]);
+			}
+			gdFree(im->pixels);
+			gdFree(im);
+			return NULL;
+		}
 	}
 	im->sx = sx;
 	im->sy = sy;
@@ -239,14 +251,26 @@ gdImagePtr gdImageCreateTrueColor (int sx, int sy)

 	im = (gdImage *) gdMalloc(sizeof(gdImage));
 	memset(im, 0, sizeof(gdImage));
-	im->tpixels = (int **) gdMalloc(sizeof(int *) * sy);
+	im->tpixels = (int **) gdFMalloc(sizeof(int *) * sy);
+	if (im->tpixels == NULL) {
+		gdFree(im);
+		return NULL;
+	}
 	im->polyInts = 0;
 	im->polyAllocated = 0;
 	im->brush = 0;
 	im->tile = 0;
 	im->style = 0;
 	for (i = 0; i < sy; i++) {
-		im->tpixels[i] = (int *) gdCalloc(sx, sizeof(int));
+		im->tpixels[i] = (int *) gdFCalloc(sx, sizeof(int));
+		if (im->tpixels[i] == NULL) {
+			while (i > 0) {
+				gdFree(im->tpixels[--i]);
+			}
+			gdFree(im->tpixels);
+			gdFree(im);
+			return NULL;
+		}
 	}
 	im->sx = sx;
 	im->sy = sy;
@@ -3106,7 +3130,7 @@ int gdImagePaletteToTrueColor(gdImagePtr src)
 		const unsigned int sy = gdImageSY(src);
 		const unsigned int sx = gdImageSX(src);

-		src->tpixels = (int **) gdMalloc(sizeof(int *) * sy);
+		src->tpixels = (int **) gdFMalloc(sizeof(int *) * sy);
 		if (src->tpixels == NULL) {
 			return 0;
 		}
@@ -3116,7 +3140,7 @@ int gdImagePaletteToTrueColor(gdImagePtr src)
 			int * dst_row;

 			/* no need to calloc it, we overwrite all pxl anyway */
-			src->tpixels[y] = (int *) gdMalloc(sx * sizeof(int));
+			src->tpixels[y] = (int *) gdFMalloc(sx * sizeof(int));
 			if (src->tpixels[y] == NULL) {
 				goto clean_on_error;
 			}
@@ -3156,5 +3180,6 @@ int gdImagePaletteToTrueColor(gdImagePtr src)
 		gdFree(src->tpixels[yy]);
 	}
 	gdFree(src->tpixels);
+	src->tpixels = NULL;
 	return 0;
 }
diff --git a/ext/gd/libgd/gd_gd2.c b/ext/gd/libgd/gd_gd2.c
index c41346a517f..d2f66b7616b 100644
--- a/ext/gd/libgd/gd_gd2.c
+++ b/ext/gd/libgd/gd_gd2.c
@@ -150,7 +150,7 @@ static int _gd2GetHeader(gdIOCtxPtr in, int *sx, int *sy, int *cs, int *vers, in
 		if (sidx <= 0) {
 			goto fail1;
 		}
-		cidx = gdCalloc(sidx, 1);
+		cidx = gdFCalloc(sidx, 1);
 		if (cidx == NULL) {
 			goto fail1;
 		}
@@ -306,8 +306,14 @@ gdImagePtr gdImageCreateFromGd2Ctx (gdIOCtxPtr in)
 		if (chunkMax <= 0) {
 			return 0;
 		}
-		chunkBuf = gdCalloc(chunkMax, 1);
-		compBuf = gdCalloc(compMax, 1);
+		chunkBuf = gdFCalloc(chunkMax, 1);
+		if (chunkBuf == NULL) {
+			goto fail2;
+		}
+		compBuf = gdFCalloc(compMax, 1);
+		if (compBuf == NULL) {
+			goto fail2;
+		}

 		GD2_DBG(gd_error("Largest compressed chunk is %d bytes", compMax));
 	}
@@ -503,8 +509,14 @@ gdImagePtr gdImageCreateFromGd2PartCtx (gdIOCtx * in, int srcx, int srcy, int w,
 			goto fail2;
 		}

-		chunkBuf = gdCalloc(chunkMax, 1);
-		compBuf = gdCalloc(compMax, 1);
+		chunkBuf = gdFCalloc(chunkMax, 1);
+		if (chunkBuf == NULL) {
+			goto fail2;
+		}
+		compBuf = gdFCalloc(compMax, 1);
+		if (compBuf == NULL) {
+			goto fail2;
+		}
 	}

 	/* Work out start/end chunks */
diff --git a/ext/gd/libgd/gd_tga.c b/ext/gd/libgd/gd_tga.c
index 3f9922189aa..07dd1db820f 100644
--- a/ext/gd/libgd/gd_tga.c
+++ b/ext/gd/libgd/gd_tga.c
@@ -236,7 +236,7 @@ int read_image_tga( gdIOCtx *ctx, oTga *tga )
 	/*!	\brief Allocate memmory for image block
 	 *  Allocate a chunk of memory for the image block to be passed into.
 	 */
-	tga->bitmap = (int *) gdMalloc(image_block_size * sizeof(int));
+	tga->bitmap = (int *) gdFMalloc(image_block_size * sizeof(int));
 	if (tga->bitmap == NULL)
 		return -1;

@@ -245,7 +245,7 @@ int read_image_tga( gdIOCtx *ctx, oTga *tga )
 		/*! \brief Read in uncompressed RGB TGA
 		 *  Chunk load the pixel data from an uncompressed RGB type TGA.
 		 */
-		conversion_buffer = (unsigned char *) gdMalloc(image_block_size * sizeof(unsigned char));
+		conversion_buffer = (unsigned char *) gdFMalloc(image_block_size * sizeof(unsigned char));
 		if (conversion_buffer == NULL) {
 			return -1;
 		}
@@ -268,11 +268,11 @@ int read_image_tga( gdIOCtx *ctx, oTga *tga )
 		/*! \brief Read in RLE compressed RGB TGA
 		 *  Chunk load the pixel data from an RLE compressed RGB type TGA.
 		 */
-		decompression_buffer = (int*) gdMalloc(image_block_size * sizeof(int));
+		decompression_buffer = (int*) gdFMalloc(image_block_size * sizeof(int));
 		if (decompression_buffer == NULL) {
 			return -1;
 		}
-		conversion_buffer = (unsigned char *) gdMalloc(image_block_size * sizeof(unsigned char));
+		conversion_buffer = (unsigned char *) gdFMalloc(image_block_size * sizeof(unsigned char));
 		if (conversion_buffer == NULL) {
 			gd_error("gd-tga: premature end of image data\n");
 			gdFree( decompression_buffer );
diff --git a/ext/gd/libgd/gdhelpers.h b/ext/gd/libgd/gdhelpers.h
index afb45f0e67d..ac7f2169202 100644
--- a/ext/gd/libgd/gdhelpers.h
+++ b/ext/gd/libgd/gdhelpers.h
@@ -13,8 +13,10 @@ extern char *gd_strtok_r(char *s, char *sep, char **state);
 	free memory allocated by these functions with the
 	right version of free(). */
 #define gdCalloc(nmemb, size)	ecalloc(nmemb, size)
+#define gdFCalloc(nmemb, size)	fecalloc(nmemb, size, true)
 #define gdMalloc(size)		emalloc(size)
 #define gdRealloc(ptr, size)	erealloc(ptr, size)
+#define gdFMalloc(size)		femalloc(size, true)
 #define gdEstrdup(ptr)		estrdup(ptr)
 #define gdFree(ptr)		efree(ptr)
 #define gdPMalloc(ptr)		pemalloc(ptr, 1)
@@ -48,4 +50,3 @@ int overflow2(int a, int b);
 #define DPI2DPM(dpi)   (unsigned int)((dpi)/0.0254 + 0.5)

 #endif /* GDHELPERS_H */
-
diff --git a/ext/gd/libgd/wbmp.c b/ext/gd/libgd/wbmp.c
index dbca0f0178c..eb84801e46a 100644
--- a/ext/gd/libgd/wbmp.c
+++ b/ext/gd/libgd/wbmp.c
@@ -196,7 +196,7 @@ readwbmp (int (*getin) (void *in), void *in, Wbmp ** return_wbmp)
       return (-1);
     }

-  if ((wbmp->bitmap = (int *) safe_emalloc((size_t)wbmp->width * wbmp->height, sizeof(int), 0)) == NULL)
+  if ((wbmp->bitmap = (int *) safe_femalloc((size_t)wbmp->width * wbmp->height, sizeof(int), 0, /* fallible */ 1)) == NULL)
     {
       gdFree (wbmp);
       return (-1);
diff --git a/ext/gd/tests/oom_imagecreatefrombmp.phpt b/ext/gd/tests/oom_imagecreatefrombmp.phpt
new file mode 100644
index 00000000000..bb352bf6302
--- /dev/null
+++ b/ext/gd/tests/oom_imagecreatefrombmp.phpt
@@ -0,0 +1,28 @@
+--TEST--
+imagecreatefrombmp() doesn't reach hard limit
+--EXTENSIONS--
+gd
+--SKIPIF--
+<?php
+if (!GD_BUNDLED) die("skip requires bundled GD library");
+if (!function_exists("imagecreatefrombmp")) die("skip BMP support unavailable");
+?>
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+$w = $h = 20000;
+$bmp = "BM" . pack('V', 0) . pack('v', 0) . pack('v', 0) . pack('V', 54)
+     . pack('V', 40) . pack('V', $w) . pack('V', $h) . pack('v', 1) . pack('v', 24)
+     . pack('V', 0) . pack('V', 0) . pack('V', 0) . pack('V', 0) . pack('V', 0) . pack('V', 0);
+
+$path = __DIR__ . '/' . basename(__FILE__, '.php') . '.tmp.bmp';
+file_put_contents($path, $bmp);
+imagecreatefrombmp($path);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.tmp.bmp');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/gd/tests/oom_imagecreatefromgd2.phpt b/ext/gd/tests/oom_imagecreatefromgd2.phpt
new file mode 100644
index 00000000000..d20dec1d6d1
--- /dev/null
+++ b/ext/gd/tests/oom_imagecreatefromgd2.phpt
@@ -0,0 +1,31 @@
+--TEST--
+imagecreatefromgd2() doesn't reach hard limit
+--EXTENSIONS--
+gd
+--SKIPIF--
+<?php
+if (!GD_BUNDLED) die("skip requires bundled GD library");
+if (!function_exists("imagecreatefromgd2")) die("skip GD2 support unavailable");
+?>
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+$w = fn($v) => pack('n', $v);   // big-endian 16-bit
+$gd2 = "gd2\x00"                  // magic
+     . $w(2)                      // version
+     . $w(1) . $w(1)             // image dimensions (small)
+     . $w(64)                     // chunk size
+     . $w(2)                      // GD2_FMT_COMPRESSED
+     . $w(65535) . $w(512);      // ncx, ncy -> 33.5M chunk-index entries
+
+$path = __DIR__ . '/' . basename(__FILE__, '.php') . '.tmp.gd2';
+file_put_contents($path, $gd2);
+imagecreatefromgd2($path);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.tmp.gd2');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/gd/tests/oom_imagecreatefromgd2_chunkbuf.phpt b/ext/gd/tests/oom_imagecreatefromgd2_chunkbuf.phpt
new file mode 100644
index 00000000000..255c5173839
--- /dev/null
+++ b/ext/gd/tests/oom_imagecreatefromgd2_chunkbuf.phpt
@@ -0,0 +1,39 @@
+--TEST--
+imagecreatefromgd2() chunk buffer doesn't reach hard limit
+--EXTENSIONS--
+gd
+--SKIPIF--
+<?php
+if (!GD_BUNDLED) die("skip requires bundled GD library");
+if (!function_exists("imagecreatefromgd2")) die("skip GD2 support unavailable");
+?>
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$n = fn($v) => pack('n', $v);   // big-endian 16-bit
+$N = fn($v) => pack('N', $v);   // big-endian 32-bit
+
+// chunkBuf is sized cs * bytesPerPixel * cs, all of it from the header, so a
+// 1x1 image still asks for 4096 * 4 * 4096 = 64 MB. GD2_CHUNKSIZE_MAX caps the
+// chunk size at 4096, hence the small memory_limit.
+$gd2 = "gd2\0"                  // magic
+     . $n(2)                    // version
+     . $n(1) . $n(1)            // image dimensions
+     . $n(4096)                 // chunk size
+     . $n(4)                    // GD2_FMT_TRUECOLOR_COMPRESSED
+     . $n(1) . $n(1)            // ncx, ncy -> one chunk
+     . $N(31) . $N(16)          // chunk index: offset, compressed size
+     . "\x01" . $N(-1);
+// The chunk data itself is never reached: the buffers are allocated first.
+
+$path = __DIR__ . '/oom_imagecreatefromgd2_chunkbuf.tmp.gd2';
+file_put_contents($path, $gd2);
+imagecreatefromgd2($path);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/oom_imagecreatefromgd2_chunkbuf.tmp.gd2');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/gd/tests/oom_imagecreatefromgd2_compbuf.phpt b/ext/gd/tests/oom_imagecreatefromgd2_compbuf.phpt
new file mode 100644
index 00000000000..ec94fa04709
--- /dev/null
+++ b/ext/gd/tests/oom_imagecreatefromgd2_compbuf.phpt
@@ -0,0 +1,38 @@
+--TEST--
+imagecreatefromgd2() compressed chunk buffer doesn't reach hard limit
+--EXTENSIONS--
+gd
+--SKIPIF--
+<?php
+if (!GD_BUNDLED) die("skip requires bundled GD library");
+if (!function_exists("imagecreatefromgd2")) die("skip GD2 support unavailable");
+?>
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+$n = fn($v) => pack('n', $v);   // big-endian 16-bit
+$N = fn($v) => pack('N', $v);   // big-endian 32-bit
+
+// compBuf is sized from the largest "size" in the chunk index, which is only
+// checked for being non-negative, so a 1x1 image can ask for 2 GB.
+$gd2 = "gd2\0"                   // magic
+     . $n(2)                     // version
+     . $n(1) . $n(1)             // image dimensions
+     . $n(64)                    // chunk size
+     . $n(4)                     // GD2_FMT_TRUECOLOR_COMPRESSED
+     . $n(1) . $n(1)             // ncx, ncy -> one chunk
+     . $N(31) . $N(0x7ffffff0)   // chunk index: offset, compressed size
+     . "\x01" . $N(-1);
+// The chunk data itself is never reached: the buffers are allocated first.
+
+$path = __DIR__ . '/oom_imagecreatefromgd2_compbuf.tmp.gd2';
+file_put_contents($path, $gd2);
+imagecreatefromgd2($path);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/oom_imagecreatefromgd2_compbuf.tmp.gd2');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/gd/tests/oom_imagecreatefromgd2part.phpt b/ext/gd/tests/oom_imagecreatefromgd2part.phpt
new file mode 100644
index 00000000000..c79a8676546
--- /dev/null
+++ b/ext/gd/tests/oom_imagecreatefromgd2part.phpt
@@ -0,0 +1,31 @@
+--TEST--
+imagecreatefromgd2part() doesn't reach hard limit
+--EXTENSIONS--
+gd
+--SKIPIF--
+<?php
+if (!GD_BUNDLED) die("skip requires bundled GD library");
+if (!function_exists("imagecreatefromgd2part")) die("skip GD2 support unavailable");
+?>
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+$w = fn($v) => pack('n', $v);   // big-endian 16-bit
+$gd2 = "gd2\x00"                  // magic
+     . $w(2)                      // version
+     . $w(1) . $w(1)             // image dimensions (small)
+     . $w(64)                     // chunk size
+     . $w(2)                      // GD2_FMT_COMPRESSED
+     . $w(65535) . $w(512);      // ncx, ncy -> 33.5M chunk-index entries
+
+$path = __DIR__ . '/' . basename(__FILE__, '.php') . '.tmp.gd2';
+file_put_contents($path, $gd2);
+imagecreatefromgd2part($path, 0, 0, 10, 10);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.tmp.gd2');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/gd/tests/oom_imagecreatefromgd2part_chunkbuf.phpt b/ext/gd/tests/oom_imagecreatefromgd2part_chunkbuf.phpt
new file mode 100644
index 00000000000..697aea42ee5
--- /dev/null
+++ b/ext/gd/tests/oom_imagecreatefromgd2part_chunkbuf.phpt
@@ -0,0 +1,39 @@
+--TEST--
+imagecreatefromgd2part() chunk buffer doesn't reach hard limit
+--EXTENSIONS--
+gd
+--SKIPIF--
+<?php
+if (!GD_BUNDLED) die("skip requires bundled GD library");
+if (!function_exists("imagecreatefromgd2part")) die("skip GD2 support unavailable");
+?>
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$n = fn($v) => pack('n', $v);   // big-endian 16-bit
+$N = fn($v) => pack('N', $v);   // big-endian 32-bit
+
+// chunkBuf is sized cs * bytesPerPixel * cs, all of it from the header, so a
+// 1x1 image still asks for 4096 * 4 * 4096 = 64 MB. GD2_CHUNKSIZE_MAX caps the
+// chunk size at 4096, hence the small memory_limit.
+$gd2 = "gd2\0"                  // magic
+     . $n(2)                    // version
+     . $n(1) . $n(1)            // image dimensions
+     . $n(4096)                 // chunk size
+     . $n(4)                    // GD2_FMT_TRUECOLOR_COMPRESSED
+     . $n(1) . $n(1)            // ncx, ncy -> one chunk
+     . $N(31) . $N(16)          // chunk index: offset, compressed size
+     . "\x01" . $N(-1);
+// The chunk data itself is never reached: the buffers are allocated first.
+
+$path = __DIR__ . '/oom_imagecreatefromgd2part_chunkbuf.tmp.gd2';
+file_put_contents($path, $gd2);
+imagecreatefromgd2part($path, 0, 0, 1, 1);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/oom_imagecreatefromgd2part_chunkbuf.tmp.gd2');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/gd/tests/oom_imagecreatefromgd2part_compbuf.phpt b/ext/gd/tests/oom_imagecreatefromgd2part_compbuf.phpt
new file mode 100644
index 00000000000..884e18f1444
--- /dev/null
+++ b/ext/gd/tests/oom_imagecreatefromgd2part_compbuf.phpt
@@ -0,0 +1,38 @@
+--TEST--
+imagecreatefromgd2part() compressed chunk buffer doesn't reach hard limit
+--EXTENSIONS--
+gd
+--SKIPIF--
+<?php
+if (!GD_BUNDLED) die("skip requires bundled GD library");
+if (!function_exists("imagecreatefromgd2part")) die("skip GD2 support unavailable");
+?>
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+$n = fn($v) => pack('n', $v);   // big-endian 16-bit
+$N = fn($v) => pack('N', $v);   // big-endian 32-bit
+
+// compBuf is sized from the largest "size" in the chunk index, which is only
+// checked for being non-negative, so a 1x1 image can ask for 2 GB.
+$gd2 = "gd2\0"                  // magic
+     . $n(2)                     // version
+     . $n(1) . $n(1)             // image dimensions
+     . $n(64)                    // chunk size
+     . $n(4)                     // GD2_FMT_TRUECOLOR_COMPRESSED
+     . $n(1) . $n(1)             // ncx, ncy -> one chunk
+     . $N(31) . $N(0x7ffffff0)   // chunk index: offset, compressed size
+     . "\x01" . $N(-1);
+// The chunk data itself is never reached: the buffers are allocated first.
+
+$path = __DIR__ . '/oom_imagecreatefromgd2part_compbuf.tmp.gd2';
+file_put_contents($path, $gd2);
+imagecreatefromgd2part($path, 0, 0, 1, 1);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/oom_imagecreatefromgd2part_compbuf.tmp.gd2');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/gd/tests/oom_imagecreatefromgif.phpt b/ext/gd/tests/oom_imagecreatefromgif.phpt
new file mode 100644
index 00000000000..864e17c2f11
--- /dev/null
+++ b/ext/gd/tests/oom_imagecreatefromgif.phpt
@@ -0,0 +1,28 @@
+--TEST--
+imagecreatefromgif() doesn't reach hard limit
+--EXTENSIONS--
+gd
+--SKIPIF--
+<?php
+if (!GD_BUNDLED) die("skip requires bundled GD library");
+if (!function_exists("imagecreatefromgif")) die("skip GIF support unavailable");
+?>
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+$w = $h = 20000;
+$gif = "GIF89a" . pack('v', $w) . pack('v', $h) . "\x00\x00\x00"       // screen descriptor, no GCT
+     . "," . pack('v', 0) . pack('v', 0) . pack('v', $w) . pack('v', $h) . "\x00"  // image descriptor
+     . ";";
+
+$path = __DIR__ . '/' . basename(__FILE__, '.php') . '.tmp.gif';
+file_put_contents($path, $gif);
+imagecreatefromgif($path);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.tmp.gif');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/gd/tests/oom_imagecreatefrompng.phpt b/ext/gd/tests/oom_imagecreatefrompng.phpt
new file mode 100644
index 00000000000..9af064208ff
--- /dev/null
+++ b/ext/gd/tests/oom_imagecreatefrompng.phpt
@@ -0,0 +1,32 @@
+--TEST--
+imagecreatefrompng() doesn't reach hard limit
+--EXTENSIONS--
+gd
+--SKIPIF--
+<?php
+if (!GD_BUNDLED) die("skip requires bundled GD library");
+if (!function_exists("imagecreatefrompng")) die("skip PNG support unavailable");
+?>
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+function png_chunk(string $t, string $d): string {
+    return pack('N', strlen($d)) . $t . $d . pack('N', crc32($t . $d));
+}
+$w = $h = 20000;
+$png = "\x89PNG\r\n\x1a\n"
+     . png_chunk('IHDR', pack('NNCCCCC', $w, $h, 8, 2, 0, 0, 0))  // 8-bit truecolor
+     . png_chunk('IDAT', "\x78\x9c\x63\x00\x00\x00\x01\x00\x01")
+     . png_chunk('IEND', '');
+
+$path = __DIR__ . '/' . basename(__FILE__, '.php') . '.tmp.png';
+file_put_contents($path, $png);
+imagecreatefrompng($path);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.tmp.png');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/gd/tests/oom_imagecreatefromstring.phpt b/ext/gd/tests/oom_imagecreatefromstring.phpt
new file mode 100644
index 00000000000..37b32624732
--- /dev/null
+++ b/ext/gd/tests/oom_imagecreatefromstring.phpt
@@ -0,0 +1,26 @@
+--TEST--
+imagecreatefromstring() doesn't reach hard limit
+--EXTENSIONS--
+gd
+--SKIPIF--
+<?php
+if (!GD_BUNDLED) die("skip requires bundled GD library");
+if (!function_exists("imagecreatefrompng")) die("skip PNG support unavailable");
+?>
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+function png_chunk(string $t, string $d): string {
+    return pack('N', strlen($d)) . $t . $d . pack('N', crc32($t . $d));
+}
+$w = $h = 20000;
+$png = "\x89PNG\r\n\x1a\n"
+     . png_chunk('IHDR', pack('NNCCCCC', $w, $h, 8, 2, 0, 0, 0))
+     . png_chunk('IDAT', "\x78\x9c\x63\x00\x00\x00\x01\x00\x01")
+     . png_chunk('IEND', '');
+
+imagecreatefromstring($png);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/gd/tests/oom_imagecreatefromtga.phpt b/ext/gd/tests/oom_imagecreatefromtga.phpt
new file mode 100644
index 00000000000..f4d8a8a55db
--- /dev/null
+++ b/ext/gd/tests/oom_imagecreatefromtga.phpt
@@ -0,0 +1,36 @@
+--TEST--
+imagecreatefromtga() doesn't reach hard limit
+--EXTENSIONS--
+gd
+--SKIPIF--
+<?php
+if (!GD_BUNDLED) die("skip requires bundled GD library");
+if (!function_exists("imagecreatefromtga")) die("skip TGA support unavailable");
+?>
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+// 8192 x 8192 x 4 bytes per pixel x sizeof(int) = 1 GB.
+$tga = pack('CCCvvCvvvvCC',
+    0,      // id length
+    0,      // colour map type
+    2,      // image type: uncompressed true colour
+    0, 0, 0,// colour map spec
+    0, 0,   // x/y origin
+    8192,   // width
+    8192,   // height
+    32,     // bits per pixel
+    8       // image descriptor
+) . str_repeat("\x00", 64);
+
+$path = __DIR__ . '/oom_imagecreatefromtga.tmp.tga';
+file_put_contents($path, $tga);
+imagecreatefromtga($path);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/oom_imagecreatefromtga.tmp.tga');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/gd/tests/oom_imagecreatefromwbmp.phpt b/ext/gd/tests/oom_imagecreatefromwbmp.phpt
new file mode 100644
index 00000000000..a6a3e67b49f
--- /dev/null
+++ b/ext/gd/tests/oom_imagecreatefromwbmp.phpt
@@ -0,0 +1,33 @@
+--TEST--
+imagecreatefromwbmp() doesn't reach hard limit
+--EXTENSIONS--
+gd
+--SKIPIF--
+<?php
+if (!GD_BUNDLED) die("skip requires bundled GD library");
+if (!function_exists("imagecreatefromwbmp")) die("skip WBMP support unavailable");
+?>
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+$mbi = function (int $v): string {          // WBMP multi-byte integer
+    $out = chr($v & 0x7f);
+    for ($v >>= 7; $v > 0; $v >>= 7) {
+        $out = chr(0x80 | ($v & 0x7f)) . $out;
+    }
+    return $out;
+};
+// 16384 x 16384 x sizeof(int) = 1 GB
+$wbmp = "\x00\x00" . $mbi(16384) . $mbi(16384) . str_repeat("\x00", 64);
+
+$path = __DIR__ . '/oom_imagecreatefromwbmp.tmp.wbmp';
+file_put_contents($path, $wbmp);
+imagecreatefromwbmp($path);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/oom_imagecreatefromwbmp.tmp.wbmp');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/gd/tests/oom_imagecreatefromxbm.phpt b/ext/gd/tests/oom_imagecreatefromxbm.phpt
new file mode 100644
index 00000000000..a4291d52869
--- /dev/null
+++ b/ext/gd/tests/oom_imagecreatefromxbm.phpt
@@ -0,0 +1,27 @@
+--TEST--
+imagecreatefromxbm() doesn't reach hard limit
+--EXTENSIONS--
+gd
+--SKIPIF--
+<?php
+if (!GD_BUNDLED) die("skip requires bundled GD library");
+if (!function_exists("imagecreatefromxbm")) die("skip XBM support unavailable");
+?>
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+$xbm = "#define x_width 20000\n"
+     . "#define x_height 20000\n"
+     . "static char x_bits[] = {\n0x00 };\n";
+
+$path = __DIR__ . '/' . basename(__FILE__, '.php') . '.tmp.xbm';
+file_put_contents($path, $xbm);
+imagecreatefromxbm($path);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.tmp.xbm');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/gd/tests/oom_imagepalettetotruecolor.phpt b/ext/gd/tests/oom_imagepalettetotruecolor.phpt
new file mode 100644
index 00000000000..308c08ec7d6
--- /dev/null
+++ b/ext/gd/tests/oom_imagepalettetotruecolor.phpt
@@ -0,0 +1,17 @@
+--TEST--
+imagepalettetotruecolor() doesn't reach hard limit
+--EXTENSIONS--
+gd
+--SKIPIF--
+<?php
+if (!GD_BUNDLED) die("skip requires bundled GD library");
+?>
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+$im = imagecreate(6000, 6000);      // palette image, one byte per pixel
+imagepalettetotruecolor($im);       // needs four bytes per pixel while the source stays live
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/iconv/iconv.c b/ext/iconv/iconv.c
index 9d7905c9a98..5ef5fff8028 100644
--- a/ext/iconv/iconv.c
+++ b/ext/iconv/iconv.c
@@ -364,7 +364,9 @@ static php_iconv_err_t _php_iconv_appendl(smart_str *d, const char *s, size_t l,
 	if (in_p != NULL) {
 		while (in_left > 0) {
 			out_left = buf_growth;
-			smart_str_alloc(d, out_left, 0);
+			if (UNEXPECTED(smart_str_falloc(d, out_left, 0, /* fallible */ true) == (size_t)-1)) {
+				return PHP_ICONV_ERR_ALLOC;
+			}

 			out_p = ZSTR_VAL((d)->s) + ZSTR_LEN((d)->s);

@@ -389,7 +391,9 @@ static php_iconv_err_t _php_iconv_appendl(smart_str *d, const char *s, size_t l,
 	} else {
 		for (;;) {
 			out_left = buf_growth;
-			smart_str_alloc(d, out_left, 0);
+			if (UNEXPECTED(smart_str_falloc(d, out_left, 0, /* fallible */ true) == (size_t)-1)) {
+				return PHP_ICONV_ERR_ALLOC;
+			}

 			out_p = ZSTR_VAL((d)->s) + ZSTR_LEN((d)->s);

@@ -460,7 +464,11 @@ PHP_ICONV_API php_iconv_err_t php_iconv_string(const char *in_p, size_t in_len,
 	out_left = in_len + 32; /* Avoid realloc() most cases */
 	out_size = 0;
 	bsz = out_left;
-	out_buf = zend_string_alloc(bsz, 0);
+	out_buf = zend_string_falloc(bsz, 0, /* fallible */ true);
+	if (UNEXPECTED(out_buf == NULL)) {
+		iconv_close(cd);
+		return PHP_ICONV_ERR_ALLOC;
+	}
 	out_p = ZSTR_VAL(out_buf);

 	while (in_left > 0) {
@@ -482,7 +490,13 @@ PHP_ICONV_API php_iconv_err_t php_iconv_string(const char *in_p, size_t in_len,
 				/* converted string is longer than out buffer */
 				bsz += in_len;

-				out_buf = zend_string_extend(out_buf, bsz, 0);
+				zend_string *grown = zend_string_fextend(out_buf, bsz, 0, /* fallible */ true);
+				if (UNEXPECTED(grown == NULL)) {
+					zend_string_efree(out_buf);
+					iconv_close(cd);
+					return PHP_ICONV_ERR_ALLOC;
+				}
+				out_buf = grown;
 				out_p = ZSTR_VAL(out_buf);
 				out_p += out_size;
 				out_left = bsz - out_size;
@@ -504,7 +518,13 @@ PHP_ICONV_API php_iconv_err_t php_iconv_string(const char *in_p, size_t in_len,

 			if (errno == E2BIG) {
 				bsz += 16;
-				out_buf = zend_string_extend(out_buf, bsz, 0);
+				zend_string *grown = zend_string_fextend(out_buf, bsz, 0, /* fallible */ true);
+				if (UNEXPECTED(grown == NULL)) {
+					zend_string_efree(out_buf);
+					iconv_close(cd);
+					return PHP_ICONV_ERR_ALLOC;
+				}
+				out_buf = grown;
 				out_p = ZSTR_VAL(out_buf);
 				out_p += out_size;
 				out_left = bsz - out_size;
@@ -963,7 +983,11 @@ static php_iconv_err_t _php_iconv_mime_encode(smart_str *pretval, const char *fn
 		goto out;
 	}

-	buf = safe_emalloc(1, max_line_len, 5);
+	buf = safe_femalloc(1, max_line_len, 5, /* fallible */ true);
+	if (UNEXPECTED(buf == NULL)) {
+		err = PHP_ICONV_ERR_ALLOC;
+		goto out;
+	}

 	char_cnt = max_line_len;

@@ -1169,7 +1193,7 @@ static php_iconv_err_t _php_iconv_mime_encode(smart_str *pretval, const char *fn

 			} break; /* case PHP_ICONV_ENC_SCHEME_QPRINT: */
 		}
-	} while (in_left > 0);
+	} while (in_left > 0 && !ZEND_HAS_OOM());

 	smart_str_0(pretval);

@@ -2124,7 +2148,7 @@ PHP_FUNCTION(iconv_mime_decode_headers)

 	enc_str_tmp = ZSTR_VAL(encoded_str);
 	enc_str_len_tmp = ZSTR_LEN(encoded_str);
-	while (enc_str_len_tmp > 0) {
+	while (enc_str_len_tmp > 0 && !ZEND_HAS_OOM()) {
 		smart_str decoded_header = {0};
 		char *header_name = NULL;
 		size_t header_name_len = 0;
@@ -2533,9 +2557,11 @@ static php_stream_filter_status_t php_iconv_stream_filter_do_filter(

 		php_stream_bucket_unlink(bucket);

-		if (php_iconv_stream_filter_append_bucket(self, stream, filter,
-				buckets_out, bucket->buf, bucket->buflen, &consumed,
-				php_stream_is_persistent(stream)) != SUCCESS) {
+		/* The conversion may grow the input several times over. */
+		if (!ZEND_HAS_OOM()
+				&& php_iconv_stream_filter_append_bucket(self, stream, filter,
+					buckets_out, bucket->buf, bucket->buflen, &consumed,
+					php_stream_is_persistent(stream)) != SUCCESS) {
 			goto out_failure;
 		}

diff --git a/ext/iconv/tests/oom_iconv.phpt b/ext/iconv/tests/oom_iconv.phpt
new file mode 100644
index 00000000000..cbf64c8e663
--- /dev/null
+++ b/ext/iconv/tests/oom_iconv.phpt
@@ -0,0 +1,17 @@
+--TEST--
+iconv() doesn't reach hard limit
+--EXTENSIONS--
+iconv
+--SKIPIF--
+<?php
+if (getenv('SKIP_ASAN')) die("skip ASan-instrumented realloc() is slow");
+?>
+--INI--
+memory_limit=16M
+--FILE--
+<?php
+iconv('ASCII', 'UTF-32', str_repeat('a', 4 * 1024 * 1024));
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/iconv/tests/oom_iconv_mime_decode.phpt b/ext/iconv/tests/oom_iconv_mime_decode.phpt
new file mode 100644
index 00000000000..3f3af020808
--- /dev/null
+++ b/ext/iconv/tests/oom_iconv_mime_decode.phpt
@@ -0,0 +1,17 @@
+--TEST--
+iconv_mime_decode() doesn't reach hard limit
+--EXTENSIONS--
+iconv
+--SKIPIF--
+<?php
+if (getenv('SKIP_ASAN')) die("skip ASan-instrumented realloc() is slow");
+?>
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+iconv_mime_decode('x: ' . str_repeat('a', 2 * 1024 * 1024), 0, 'UTF-32');
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/iconv/tests/oom_iconv_mime_decode_headers.phpt b/ext/iconv/tests/oom_iconv_mime_decode_headers.phpt
new file mode 100644
index 00000000000..8f9002e0b00
--- /dev/null
+++ b/ext/iconv/tests/oom_iconv_mime_decode_headers.phpt
@@ -0,0 +1,13 @@
+--TEST--
+iconv_mime_decode_headers() doesn't reach hard limit
+--EXTENSIONS--
+iconv
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$headers = str_repeat("a:b\n", 512 * 1024); // 2 MB == memory_limit / 4
+iconv_mime_decode_headers($headers, 0, "ISO-8859-1");
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/iconv/tests/oom_iconv_mime_encode.phpt b/ext/iconv/tests/oom_iconv_mime_encode.phpt
new file mode 100644
index 00000000000..b60d44f7e99
--- /dev/null
+++ b/ext/iconv/tests/oom_iconv_mime_encode.phpt
@@ -0,0 +1,21 @@
+--TEST--
+iconv_mime_encode() doesn't reach hard limit
+--EXTENSIONS--
+iconv
+--SKIPIF--
+<?php
+if (getenv('SKIP_ASAN')) die("skip ASan-instrumented realloc() is slow");
+?>
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$value = str_repeat("\xFF", 2 * 1024 * 1024); // 2 MB == memory_limit / 4
+iconv_mime_encode("A", $value, [
+    "scheme" => "Q",
+    "input-charset" => "ISO-8859-1",
+    "output-charset" => "ISO-8859-1",
+]);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/iconv/tests/oom_iconv_mime_encode_line_length.phpt b/ext/iconv/tests/oom_iconv_mime_encode_line_length.phpt
new file mode 100644
index 00000000000..53143f2f175
--- /dev/null
+++ b/ext/iconv/tests/oom_iconv_mime_encode_line_length.phpt
@@ -0,0 +1,13 @@
+--TEST--
+iconv_mime_encode() line-length doesn't reach hard limit
+--EXTENSIONS--
+iconv
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+iconv_mime_encode('x', 'y', ['line-length' => PHP_INT_MAX]);
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/iconv/tests/oom_iconv_stream_filter.phpt b/ext/iconv/tests/oom_iconv_stream_filter.phpt
new file mode 100644
index 00000000000..31fa9409964
--- /dev/null
+++ b/ext/iconv/tests/oom_iconv_stream_filter.phpt
@@ -0,0 +1,28 @@
+--TEST--
+convert.iconv stream filter doesn't reach hard limit
+--EXTENSIONS--
+iconv
+--INI--
+memory_limit=64M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+$file = __DIR__ . "/oom_iconv_stream_filter.txt";
+file_put_contents($file, str_repeat("a", 4 * 1024 * 1024));
+
+ini_set("memory_limit", "4M");
+
+// UTF-8 to UTF-32 quadruples the input.
+$fp = fopen($file, "rb");
+stream_filter_append($fp, "convert.iconv.UTF-8/UTF-32", STREAM_FILTER_READ);
+var_dump(stream_get_contents($fp));
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/oom_iconv_stream_filter.txt");
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/imap/php_imap.c b/ext/imap/php_imap.c
index 6676d24800d..9a8b00aadf3 100644
--- a/ext/imap/php_imap.c
+++ b/ext/imap/php_imap.c
@@ -3728,7 +3728,7 @@ PHP_FUNCTION(imap_mime_header_decode)

 	charset = (char *) safe_emalloc((end + 1), 2, 0);
 	text = &charset[end + 1];
-	while (offset < end) {	/* Reached end of the string? */
+	while ((offset < end) && EXPECTED(!ZEND_HAS_OOM())) {	/* Reached end of the string? */
 		if ((charset_token = (zend_long)php_memnstr(&string[offset], "=?", 2, string + end))) {	/* Is there anything encoded in the string? */
 			charset_token -= (zend_long)string;
 			if (offset != charset_token) {	/* Is there anything before the encoded data? */
diff --git a/ext/imap/tests/oom_imap_mime_header_decode.phpt b/ext/imap/tests/oom_imap_mime_header_decode.phpt
new file mode 100644
index 00000000000..b2919b449cd
--- /dev/null
+++ b/ext/imap/tests/oom_imap_mime_header_decode.phpt
@@ -0,0 +1,13 @@
+--TEST--
+imap_mime_header_decode() doesn't reach hard limit
+--EXTENSIONS--
+imap
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$header = str_repeat("=?a?q?b?=", 233016);
+imap_mime_header_decode($header);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/intl/converter/converter.c b/ext/intl/converter/converter.c
index 04a34e207cc..822afcfb88c 100644
--- a/ext/intl/converter/converter.c
+++ b/ext/intl/converter/converter.c
@@ -638,7 +638,10 @@ static zend_string* php_converter_do_convert(UConverter *dest_cnv,
 		THROW_UFAILURE(objval, "ucnv_toUChars", error);
 		return NULL;
 	}
-	temp = safe_emalloc(sizeof(UChar), temp_len, sizeof(UChar));
+	temp = safe_femalloc(sizeof(UChar), temp_len, sizeof(UChar), /* fallible */ true);
+	if (!temp) {
+		return NULL;
+	}

 	/* Convert to intermediate UChar* array */
 	error = U_ZERO_ERROR;
@@ -658,7 +661,11 @@ static zend_string* php_converter_do_convert(UConverter *dest_cnv,
 		return NULL;
 	}

-	ret = zend_string_alloc(ret_len, 0);
+	ret = zend_string_falloc(ret_len, /* persistent */ false, /* fallible */ true);
+	if (!ret) {
+		efree(temp);
+		return NULL;
+	}

 	/* Convert to final encoding */
 	error = U_ZERO_ERROR;
diff --git a/ext/intl/normalizer/normalizer_normalize.c b/ext/intl/normalizer/normalizer_normalize.c
index ee7b787b9ee..9f24ffb0915 100644
--- a/ext/intl/normalizer/normalizer_normalize.c
+++ b/ext/intl/normalizer/normalizer_normalize.c
@@ -148,7 +148,10 @@ PHP_FUNCTION( normalizer_normalize )

 	/* Allocate memory for the destination buffer for normalization */
 	uret_len = uinput_len * expansion_factor;
-	uret_buf = eumalloc( uret_len + 1 );
+	uret_buf = safe_femalloc(uret_len + 1, sizeof(UChar), 0, /* fallible */ true);
+	if (!uret_buf) {
+		RETURN_OOM();
+	}

 	/* normalize */
 #if U_ICU_VERSION_MAJOR_NUM < 56
@@ -173,7 +176,11 @@ PHP_FUNCTION( normalizer_normalize )
 		 * uret_buf =  eurealloc(uret_buf, size_needed + 1);
 		 */
 		efree( uret_buf );
-		uret_buf = eumalloc( size_needed + 1 );
+		uret_buf = safe_femalloc(size_needed + 1, sizeof(UChar), 0, /* fallible */ true);
+		if (!uret_buf) {
+			efree(uinput);
+			RETURN_OOM();
+		}
 		uret_len = size_needed;

 		status = U_ZERO_ERROR;
diff --git a/ext/intl/tests/oom_normalizer_normalize.phpt b/ext/intl/tests/oom_normalizer_normalize.phpt
new file mode 100644
index 00000000000..673e33ac965
--- /dev/null
+++ b/ext/intl/tests/oom_normalizer_normalize.phpt
@@ -0,0 +1,14 @@
+--TEST--
+Normalizer::normalize() doesn't reach hard limit
+--EXTENSIONS--
+intl
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$text = str_repeat("A", 2 * 1024 * 1024);
+Normalizer::normalize($text, Normalizer::FORM_D);
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/intl/tests/oom_normalizer_normalize_func.phpt b/ext/intl/tests/oom_normalizer_normalize_func.phpt
new file mode 100644
index 00000000000..427642f14a8
--- /dev/null
+++ b/ext/intl/tests/oom_normalizer_normalize_func.phpt
@@ -0,0 +1,14 @@
+--TEST--
+normalizer_normalize() doesn't reach hard limit
+--EXTENSIONS--
+intl
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$text = str_repeat("A", 2 * 1024 * 1024);
+normalizer_normalize($text, Normalizer::FORM_D);
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/intl/tests/oom_uconverter_convert.phpt b/ext/intl/tests/oom_uconverter_convert.phpt
new file mode 100644
index 00000000000..df98ee802ef
--- /dev/null
+++ b/ext/intl/tests/oom_uconverter_convert.phpt
@@ -0,0 +1,15 @@
+--TEST--
+UConverter::convert() doesn't reach hard limit
+--EXTENSIONS--
+intl
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$converter = new UConverter("UTF-32BE", "ISO-8859-1");
+$source = str_repeat("A", 2 * 1024 * 1024);
+$converter->convert($source);
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/intl/tests/oom_uconverter_transcode.phpt b/ext/intl/tests/oom_uconverter_transcode.phpt
new file mode 100644
index 00000000000..28b20d75a48
--- /dev/null
+++ b/ext/intl/tests/oom_uconverter_transcode.phpt
@@ -0,0 +1,14 @@
+--TEST--
+UConverter::transcode() doesn't reach hard limit
+--EXTENSIONS--
+intl
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$source = str_repeat("A", 2 * 1024 * 1024);
+UConverter::transcode($source, "UTF-32BE", "ISO-8859-1");
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/json/json_encoder.c b/ext/json/json_encoder.c
index 14fd86d7342..12b897e0b43 100644
--- a/ext/json/json_encoder.c
+++ b/ext/json/json_encoder.c
@@ -165,11 +165,16 @@ static zend_result php_json_encode_array(smart_str *buf, zval *val, int options,
 			php_json_pretty_print_indent(buf, options, encoder);

 			if (php_json_escape_string(buf, ZSTR_VAL(prop_info->name), ZSTR_LEN(prop_info->name),
-					options & ~PHP_JSON_NUMERIC_CHECK, encoder) == FAILURE &&
-					(options & PHP_JSON_PARTIAL_OUTPUT_ON_ERROR) &&
-					buf->s) {
-				ZSTR_LEN(buf->s) -= 4;
-				smart_str_appendl(buf, "\"\"", 2);
+					options & ~PHP_JSON_NUMERIC_CHECK, encoder) == FAILURE) {
+				if (ZEND_HAS_OOM()) {
+					PHP_JSON_HASH_UNPROTECT_RECURSION(obj);
+					return FAILURE;
+				}
+				if ((options & PHP_JSON_PARTIAL_OUTPUT_ON_ERROR) && buf->s) {
+					ZEND_ASSERT(ZSTR_LEN(buf->s) >= 4);
+					ZSTR_LEN(buf->s) -= 4;
+					smart_str_appendl(buf, "\"\"", 2);
+				}
 			}

 			smart_str_appendc(buf, ':');
@@ -253,11 +258,17 @@ static zend_result php_json_encode_array(smart_str *buf, zval *val, int options,
 					php_json_pretty_print_indent(buf, options, encoder);

 					if (php_json_escape_string(buf, ZSTR_VAL(key), ZSTR_LEN(key),
-								options & ~PHP_JSON_NUMERIC_CHECK, encoder) == FAILURE &&
-							(options & PHP_JSON_PARTIAL_OUTPUT_ON_ERROR) &&
-							buf->s) {
-						ZSTR_LEN(buf->s) -= 4;
-						smart_str_appendl(buf, "\"\"", 2);
+								options & ~PHP_JSON_NUMERIC_CHECK, encoder) == FAILURE) {
+						if (ZEND_HAS_OOM()) {
+							PHP_JSON_HASH_UNPROTECT_RECURSION(myht);
+							zend_release_properties(prop_ht);
+							return FAILURE;
+						}
+						if ((options & PHP_JSON_PARTIAL_OUTPUT_ON_ERROR) && buf->s) {
+							ZEND_ASSERT(ZSTR_LEN(buf->s) >= 4);
+							ZSTR_LEN(buf->s) -= 4;
+							smart_str_appendl(buf, "\"\"", 2);
+						}
 					}
 				} else {
 					if (need_comma) {
@@ -347,7 +358,9 @@ zend_result php_json_escape_string(
 	checkpoint = buf->s ? ZSTR_LEN(buf->s) : 0;

 	/* pre-allocate for string length plus 2 quotes */
-	smart_str_alloc(buf, len+2, 0);
+	if (UNEXPECTED(smart_str_falloc(buf, len+2, 0, /* fallible */ true) == (size_t)-1)) {
+		return FAILURE;
+	}
 	smart_str_appendc(buf, '"');

 	pos = 0;
@@ -366,6 +379,11 @@ zend_result php_json_escape_string(
 				break;
 			}
 		} else {
+			/* An escaped byte can append six. */
+			if (ZEND_HAS_OOM()) {
+				ZSTR_LEN(buf->s) = checkpoint;
+				return FAILURE;
+			}
 			if (pos) {
 				smart_str_appendl(buf, s, pos);
 				s += pos;
@@ -605,6 +623,9 @@ static zend_result php_json_encode_serializable_enum(smart_str *buf, zval *val,

 zend_result php_json_encode_zval(smart_str *buf, zval *val, int options, php_json_encoder *encoder) /* {{{ */
 {
+	if (ZEND_HAS_OOM()) {
+		return FAILURE;
+	}
 again:
 	switch (Z_TYPE_P(val))
 	{
diff --git a/ext/json/json_parser.y b/ext/json/json_parser.y
index 90ff0a7c758..a2080fa1f6c 100644
--- a/ext/json/json_parser.y
+++ b/ext/json/json_parser.y
@@ -118,7 +118,9 @@ members:
 				if ((parser->scanner.options & PHP_JSON_OBJECT_AS_ARRAY) && parser->methods.object_create == php_json_parser_object_create) {
 					ZVAL_EMPTY_ARRAY(&$$);
 				} else {
-					parser->methods.object_create(parser, &$$);
+					if (UNEXPECTED(parser->methods.object_create(parser, &$$) == FAILURE)) {
+						YYERROR;
+					}
 				}
 			}
 	|	member
@@ -127,7 +129,9 @@ members:
 member:
 		key ':' value
 			{
-				parser->methods.object_create(parser, &$$);
+				if (UNEXPECTED(parser->methods.object_create(parser, &$$) == FAILURE)) {
+					YYERROR;
+				}
 				if (parser->methods.object_update(parser, &$$, Z_STR($1), &$3) == FAILURE) {
 					YYERROR;
 				}
@@ -174,7 +178,9 @@ elements:
 				if (parser->methods.array_create == php_json_parser_array_create) {
 					ZVAL_EMPTY_ARRAY(&$$);
 				} else {
-					parser->methods.array_create(parser, &$$);
+					if (UNEXPECTED(parser->methods.array_create(parser, &$$) == FAILURE)) {
+						YYERROR;
+					}
 				}
 			}
 	|	element
@@ -183,12 +189,18 @@ elements:
 element:
 		value
 			{
-				parser->methods.array_create(parser, &$$);
-				parser->methods.array_append(parser, &$$, &$1);
+				if (UNEXPECTED(parser->methods.array_create(parser, &$$) == FAILURE)) {
+					YYERROR;
+				}
+				if (UNEXPECTED(parser->methods.array_append(parser, &$$, &$1) == FAILURE)) {
+					YYERROR;
+				}
 			}
 	|	element ',' value
 			{
-				parser->methods.array_append(parser, &$1, &$3);
+				if (UNEXPECTED(parser->methods.array_append(parser, &$1, &$3) == FAILURE)) {
+					YYERROR;
+				}
 				ZVAL_COPY_VALUE(&$$, &$1);
 			}
 ;
@@ -214,22 +226,34 @@ value:

 static int php_json_parser_array_create(php_json_parser *parser, zval *array)
 {
-	array_init(array);
+	if (UNEXPECTED(!array_finit(array))) {
+		ZEND_ASSERT(ZEND_HAS_OOM());
+		return FAILURE;
+	}
 	return SUCCESS;
 }

 static int php_json_parser_array_append(php_json_parser *parser, zval *array, zval *zvalue)
 {
-	zend_hash_next_index_insert(Z_ARRVAL_P(array), zvalue);
+	if (UNEXPECTED(zend_hash_next_index_finsert(Z_ARRVAL_P(array), zvalue) == HASH_NOMEM)) {
+		ZEND_ASSERT(ZEND_HAS_OOM());
+		return FAILURE;
+	}
 	return SUCCESS;
 }

 static int php_json_parser_object_create(php_json_parser *parser, zval *object)
 {
+	bool created;
+
 	if (parser->scanner.options & PHP_JSON_OBJECT_AS_ARRAY) {
-		array_init(object);
+		created = array_finit(object);
 	} else {
-		object_init(object);
+		created = object_finit(object);
+	}
+	if (UNEXPECTED(!created)) {
+		ZEND_ASSERT(ZEND_HAS_OOM());
+		return FAILURE;
 	}
 	return SUCCESS;
 }
@@ -238,7 +262,9 @@ static int php_json_parser_object_update(php_json_parser *parser, zval *object,
 {
 	/* if JSON_OBJECT_AS_ARRAY is set */
 	if (Z_TYPE_P(object) == IS_ARRAY) {
-		zend_symtable_update(Z_ARRVAL_P(object), key, zvalue);
+		if (UNEXPECTED(zend_symtable_fupdate(Z_ARRVAL_P(object), key, zvalue) == HASH_NOMEM)) {
+			return FAILURE;
+		}
 	} else {
 		if (ZSTR_LEN(key) > 0 && ZSTR_VAL(key)[0] == '\0') {
 			parser->scanner.errcode = PHP_JSON_ERROR_INVALID_PROPERTY_NAME;
@@ -249,6 +275,9 @@ static int php_json_parser_object_update(php_json_parser *parser, zval *object,
 		}
 		zend_std_write_property(Z_OBJ_P(object), key, zvalue, NULL);
 		Z_TRY_DELREF_P(zvalue);
+		if (ZEND_HAS_OOM()) {
+			return FAILURE;
+		}
 	}
 	zend_string_release_ex(key, 0);

diff --git a/ext/json/json_scanner.re b/ext/json/json_scanner.re
index 1db43dd081a..a22b396621e 100644
--- a/ext/json/json_scanner.re
+++ b/ext/json/json_scanner.re
@@ -264,7 +264,11 @@ std:
 			ZVAL_EMPTY_STRING(&s->value);
 			return PHP_JSON_T_ESTRING;
 		}
-		str = zend_string_alloc(len, 0);
+		str = zend_string_falloc(len, /* persistent */ false, /* fallible */ true);
+		if (UNEXPECTED(!str)) {
+			ZEND_ASSERT(ZEND_HAS_OOM());
+			return PHP_JSON_T_ERROR;
+		}
 		ZSTR_VAL(str)[len] = '\0';
 		ZVAL_STR(&s->value, str);
 		if (s->str_esc || s->utf8_invalid) {
diff --git a/ext/json/tests/json_decode_oom_utf8_substitute.phpt b/ext/json/tests/json_decode_oom_utf8_substitute.phpt
new file mode 100644
index 00000000000..147de741eb0
--- /dev/null
+++ b/ext/json/tests/json_decode_oom_utf8_substitute.phpt
@@ -0,0 +1,14 @@
+--TEST--
+json_decode() with JSON_INVALID_UTF8_SUBSTITUTE doesn't reach hard limit
+--INI--
+memory_limit=16M
+--FILE--
+<?php
+$n = 10 * 1024 * 1024;
+$json = str_repeat("\x80", $n);
+$json[0] = '"';
+$json[$n - 1] = '"';
+json_decode($json, false, 512, JSON_INVALID_UTF8_SUBSTITUTE);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 16777216 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/json/tests/json_encode_oom_escape.phpt b/ext/json/tests/json_encode_oom_escape.phpt
new file mode 100644
index 00000000000..fdffc833d6b
--- /dev/null
+++ b/ext/json/tests/json_encode_oom_escape.phpt
@@ -0,0 +1,11 @@
+--TEST--
+json_encode() escaping doesn't reach hard limit
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$subject = str_repeat("\x01", 2 * 1024 * 1024);
+json_encode($subject);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/json/tests/json_encode_oom_partial_output.phpt b/ext/json/tests/json_encode_oom_partial_output.phpt
new file mode 100644
index 00000000000..122a6aad0a7
--- /dev/null
+++ b/ext/json/tests/json_encode_oom_partial_output.phpt
@@ -0,0 +1,11 @@
+--TEST--
+json_encode() with JSON_PARTIAL_OUTPUT_ON_ERROR doesn't reach hard limit
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$subject = array_fill(0, 200, str_repeat("\x01", 20000));
+json_encode($subject, JSON_PARTIAL_OUTPUT_ON_ERROR);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/json/tests/json_encode_oom_pretty_print.phpt b/ext/json/tests/json_encode_oom_pretty_print.phpt
new file mode 100644
index 00000000000..a462963c454
--- /dev/null
+++ b/ext/json/tests/json_encode_oom_pretty_print.phpt
@@ -0,0 +1,11 @@
+--TEST--
+json_encode() with JSON_PRETTY_PRINT doesn't reach hard limit
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$subject = array_fill(0, 200000, [1, 2, 3]);
+json_encode($subject, JSON_PRETTY_PRINT);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/json/tests/json_encode_oom_string_prealloc.phpt b/ext/json/tests/json_encode_oom_string_prealloc.phpt
new file mode 100644
index 00000000000..09126bf7a6a
--- /dev/null
+++ b/ext/json/tests/json_encode_oom_string_prealloc.phpt
@@ -0,0 +1,13 @@
+--TEST--
+json_encode() string pre-allocation doesn't reach hard limit
+--INI--
+memory_limit=32M
+memory_hard_limit_percent=110
+--FILE--
+<?php
+// The hard limit is lowered to 110% to keep the test cheap
+$subject = str_repeat("a", 20 * 1024 * 1024);
+json_encode($subject);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 33554432 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/libxml/libxml.c b/ext/libxml/libxml.c
index 735a58e0d31..8852e491aaf 100644
--- a/ext/libxml/libxml.c
+++ b/ext/libxml/libxml.c
@@ -554,6 +554,10 @@ static void php_libxml_internal_error_handler(int error_type, void *ctx, const c
 	char *buf;
 	int len, len_iter, output = 0;

+	if (ZEND_HAS_OOM()) {
+		return;
+	}
+
 	len = vspprintf(&buf, 0, *msg, ap);
 	len_iter = len;

@@ -1019,10 +1023,10 @@ PHP_FUNCTION(libxml_get_errors)

 	if (LIBXML(error_list)) {

-		array_init(return_value);
+		array_init_size(return_value, zend_llist_count(LIBXML(error_list)));
 		error = zend_llist_get_first(LIBXML(error_list));

-		while (error != NULL) {
+		while (error != NULL && !ZEND_HAS_OOM()) {
 			zval z_error;

 			object_init_ex(&z_error, libxmlerror_class_entry);
diff --git a/ext/mbstring/libmbfl/filters/mbfilter_qprint.c b/ext/mbstring/libmbfl/filters/mbfilter_qprint.c
index 2bcddedede3..e59bbeb301a 100644
--- a/ext/mbstring/libmbfl/filters/mbfilter_qprint.c
+++ b/ext/mbstring/libmbfl/filters/mbfilter_qprint.c
@@ -151,7 +151,7 @@ int mbfl_filt_conv_qprintenc(int c, mbfl_convert_filter *filter)
 int mbfl_filt_conv_qprintenc_flush(mbfl_convert_filter *filter)
 {
 	/* flush filter cache */
-	(*filter->filter_function)('\0', filter);
+	CK((*filter->filter_function)('\0', filter));
 	filter->status &= ~0xffff;
 	filter->cache = 0;

diff --git a/ext/mbstring/libmbfl/mbfl/mbfilter.c b/ext/mbstring/libmbfl/mbfl/mbfilter.c
index a517c12c72e..b7be9556761 100644
--- a/ext/mbstring/libmbfl/mbfl/mbfilter.c
+++ b/ext/mbstring/libmbfl/mbfl/mbfilter.c
@@ -95,6 +95,8 @@

 #include "rare_cp_bitvec.h"

+#define CK(statement)	do { if ((statement) < 0) return (-1); } while (0)
+
 /*
  *  buffering converter
  */
@@ -327,7 +329,8 @@ int mbfl_encoding_detector_feed(mbfl_encoding_detector *identd, mbfl_string *str
 			mbfl_convert_filter *filter = identd->filter_list[i];
 			mbfl_encoding_detector_data *data = &identd->filter_data[i];
 			if (!data->num_illegalchars) {
-				(*filter->filter_function)(*p, filter);
+				int ret = (*filter->filter_function)(*p, filter);
+				ZEND_ASSERT(ret >= 0);
 				if (data->num_illegalchars) {
 					bad++;
 				}
@@ -757,7 +760,7 @@ collector_substr(int c, void* data)
 	}

 	if (pc->output >= pc->start) {
-		(*pc->next_filter->filter_function)(c, pc->next_filter);
+		CK((*pc->next_filter->filter_function)(c, pc->next_filter));
 	}

 	pc->output++;
@@ -858,11 +861,17 @@ mbfl_substr(
 		mbfl_convert_filter *decoder;
 		mbfl_convert_filter *encoder;

+		size_t init_size;
 		if (length == MBFL_SUBSTR_UNTIL_END) {
 			length = mbfl_strlen(string) - from;
+			init_size = length;
+		} else {
+			/* clamp init size to avoid over allocation */
+			init_size = MIN(string->len, length);
 		}

-		mbfl_memory_device_init(&device, length + 1, 0);
+		init_size += 1;
+		mbfl_memory_device_init(&device, init_size, 0);
 		mbfl_string_init(result);
 		result->encoding = string->encoding;
 		/* output code filter */
@@ -1030,13 +1039,17 @@ mbfl_strcut(
 			return NULL;
 		}

-		mbfl_memory_device_init(&device, length + 8, 0);
+		/* clamp init size to avoid over allocation */
+		size_t init_size = MIN(string->len, length) + 8;
+		mbfl_memory_device_init(&device, init_size, 0);

 		p = string->val;

 		/* search start position */
 		for (q = string->val + from; p < q; p++) {
-			(*encoder->filter_function)(*p, encoder);
+			if ((*encoder->filter_function)(*p, encoder) < 0) {
+				goto err;
+			}
 		}

 		/* switch the drain direction */
@@ -1060,7 +1073,9 @@ mbfl_strcut(
 			/* output a little shorter than "length" */
 			/* XXX: the constant "20" was determined purely on the heuristics. */
 			for (r = p + length - 20; p < r; p++) {
-				(*encoder->filter_function)(*p, encoder);
+				if ((*encoder->filter_function)(*p, encoder) < 0) {
+					goto err_prev_bk;
+				}
 			}

 			/* if the offset of the resulting string exceeds the length,
@@ -1124,7 +1139,9 @@ mbfl_strcut(

 		/* detect end position */
 		while (p < q) {
-			(*encoder->filter_function)(*p, encoder);
+			if ((*encoder->filter_function)(*p, encoder) < 0) {
+				goto err_bk;
+			}

 			if (device.pos > length) {
 				/* restore filter */
@@ -1196,6 +1213,31 @@ mbfl_strcut(

 		mbfl_convert_filter_delete(encoder);
 		mbfl_convert_filter_delete(decoder);
+
+		return result;
+
+		/* Conversion failed, which for these filters means the output device
+		 * could not be grown. Release the saved filter state which the normal
+		 * exit path would have released, then everything else. */
+err_bk:
+		if (bk.decoder.filter_dtor)
+			bk.decoder.filter_dtor(&bk.decoder);
+		if (bk.encoder.filter_dtor)
+			bk.encoder.filter_dtor(&bk.encoder);
+		goto err;
+
+err_prev_bk:
+		if (_bk.decoder.filter_dtor)
+			_bk.decoder.filter_dtor(&_bk.decoder);
+		if (_bk.encoder.filter_dtor)
+			_bk.encoder.filter_dtor(&_bk.encoder);
+
+err:
+		mbfl_convert_filter_delete(encoder);
+		mbfl_convert_filter_delete(decoder);
+		mbfl_memory_device_clear(&device);
+
+		return NULL;
 	}

 	return result;
@@ -1236,7 +1278,7 @@ mime_header_encoder_block_collector(int c, void *data)
 		pe->prevpos = pe->outdev.pos;
 		mbfl_convert_filter_copy(pe->conv2_filter, pe->conv2_filter_backup);
 		mbfl_convert_filter_copy(pe->encod_filter, pe->encod_filter_backup);
-		(*pe->conv2_filter->filter_function)(c, pe->conv2_filter);
+		CK((*pe->conv2_filter->filter_function)(c, pe->conv2_filter));
 		(*pe->conv2_filter->filter_flush)(pe->conv2_filter);
 		(*pe->encod_filter->filter_flush)(pe->encod_filter);
 		n = pe->outdev.pos - pe->linehead + pe->firstindent;
@@ -1264,6 +1306,8 @@ mime_header_encoder_block_collector(int c, void *data)
 		break;
 	}

+	CK(c);
+
 	return 0;
 }

@@ -1294,7 +1338,7 @@ mime_header_encoder_collector(int c, void *data)

 	switch (pe->status1) {
 	case 11:	/* encoded word */
-		(*pe->block_filter->filter_function)(c, pe->block_filter);
+		CK((*pe->block_filter->filter_function)(c, pe->block_filter));
 		break;

 	default:	/* ASCII */
@@ -1327,7 +1371,7 @@ mime_header_encoder_collector(int c, void *data)
 				}
 				mbfl_convert_filter_devcat(pe->block_filter, &pe->tmpdev);
 				mbfl_memory_device_reset(&pe->tmpdev);
-				(*pe->block_filter->filter_function)(c, pe->block_filter);
+				CK((*pe->block_filter->filter_function)(c, pe->block_filter));
 				pe->status1 = 11;
 			}
 		}
@@ -1500,7 +1544,10 @@ mbfl_mime_header_encode(
 	n = string->len;
 	p = string->val;
 	while (n > 0) {
-		(*pe->conv1_filter->filter_function)(*p++, pe->conv1_filter);
+		if (UNEXPECTED((*pe->conv1_filter->filter_function)(*p++, pe->conv1_filter) < 0)) {
+			mime_header_encoder_delete(pe);
+			return NULL;
+		}
 		n--;
 	}

@@ -1547,7 +1594,7 @@ mime_header_decoder_collector(int c, void* data)
 			} else if (c == 0x0d || c == 0x0a) {	/* CR or LF */
 				pd->status = 9;
 			} else {
-				(*pd->conv1_filter->filter_function)(c, pd->conv1_filter);
+				CK((*pd->conv1_filter->filter_function)(c, pd->conv1_filter));
 				pd->status = 0;
 			}
 		}
@@ -1619,7 +1666,7 @@ mime_header_decoder_collector(int c, void* data)
 		if (c == 0x3f) {		/* ? */
 			pd->status = 6;
 		} else {
-			(*pd->deco_filter->filter_function)(c, pd->deco_filter);
+			CK((*pd->deco_filter->filter_function)(c, pd->deco_filter));
 		}
 		break;
 	case 6:		/* check end position */
@@ -1630,9 +1677,9 @@ mime_header_decoder_collector(int c, void* data)
 			mbfl_convert_filter_reset(pd->conv1_filter, &mbfl_encoding_ascii, &mbfl_encoding_wchar);
 			pd->status = 7;
 		} else {
-			(*pd->deco_filter->filter_function)(0x3f, pd->deco_filter);
+			CK((*pd->deco_filter->filter_function)(0x3f, pd->deco_filter));
 			if (c != 0x3f) {		/* ? */
-				(*pd->deco_filter->filter_function)(c, pd->deco_filter);
+				CK((*pd->deco_filter->filter_function)(c, pd->deco_filter));
 				pd->status = 5;
 			}
 		}
@@ -1658,7 +1705,7 @@ mime_header_decoder_collector(int c, void* data)
 				if (pd->status == 8) {
 					mbfl_memory_device_output(0x20, &pd->tmpdev);	/* SPACE */
 				} else {
-					(*pd->conv1_filter->filter_function)(0x20, pd->conv1_filter);
+					CK((*pd->conv1_filter->filter_function)(0x20, pd->conv1_filter));
 				}
 				mbfl_memory_device_output(c, &pd->tmpdev);
 				pd->status = 1;
@@ -1678,7 +1725,7 @@ mime_header_decoder_collector(int c, void* data)
 			mbfl_memory_device_output(c, &pd->tmpdev);
 			pd->status = 1;
 		} else {
-			(*pd->conv1_filter->filter_function)(c, pd->conv1_filter);
+			CK((*pd->conv1_filter->filter_function)(c, pd->conv1_filter));
 		}
 		break;
 	}
@@ -1773,7 +1820,10 @@ mbfl_mime_header_decode(
 	n = string->len;
 	p = string->val;
 	while (n > 0) {
-		mime_header_decoder_collector(*p++, pd);
+		if (UNEXPECTED(mime_header_decoder_collector(*p++, pd) < 0)) {
+			mime_header_decoder_delete(pd);
+			return NULL;
+		}
 		n--;
 	}

diff --git a/ext/mbstring/libmbfl/mbfl/mbfl_encoding.h b/ext/mbstring/libmbfl/mbfl/mbfl_encoding.h
index f66e85acd8a..e7f3129359e 100644
--- a/ext/mbstring/libmbfl/mbfl/mbfl_encoding.h
+++ b/ext/mbstring/libmbfl/mbfl/mbfl_encoding.h
@@ -159,15 +159,29 @@ static inline void mb_convert_buf_init(mb_convert_buf *buf, size_t initsize, uin
 	buf->error_mode = err_mode;
 }

+static inline zend_result mb_convert_buf_ensure(mb_convert_buf *buf, unsigned char **out, unsigned char **limit, size_t needed)
+{
+	ZEND_ASSERT(*out <= *limit);
+	if ((*limit - *out) < needed) {
+		size_t oldsize = *limit - (unsigned char*)ZSTR_VAL(buf->str);
+		size_t newsize = oldsize + MAX(oldsize >> 1, needed);
+		zend_string *newstr = safe_ferealloc(buf->str, 1, newsize, _ZSTR_STRUCT_SIZE(0), /* fallible */ true);
+		if (!newstr) {
+			/* An earlier growth may have moved the buffer. */
+			buf->out = *out;
+			buf->limit = *limit;
+			return FAILURE;
+		}
+		*out = (unsigned char*)ZSTR_VAL(newstr) + (*out - (unsigned char*)ZSTR_VAL(buf->str));
+		*limit = (unsigned char*)ZSTR_VAL(newstr) + newsize;
+		buf->str = newstr;
+	}
+	return SUCCESS;
+}
+
 #define MB_CONVERT_BUF_ENSURE(buf, out, limit, needed) \
-	ZEND_ASSERT(out <= limit); \
-	if ((limit - out) < (needed)) { \
-		size_t oldsize = limit - (unsigned char*)ZSTR_VAL(buf->str); \
-		size_t newsize = oldsize + MAX(oldsize >> 1, needed); \
-		zend_string *newstr = erealloc(buf->str, _ZSTR_STRUCT_SIZE(newsize)); \
-		out = (unsigned char*)ZSTR_VAL(newstr) + (out - (unsigned char*)ZSTR_VAL(buf->str)); \
-		limit = (unsigned char*)ZSTR_VAL(newstr) + newsize; \
-		buf->str = newstr; \
+	if (UNEXPECTED(mb_convert_buf_ensure((buf), &(out), &(limit), (needed)) == FAILURE)) { \
+		return; \
 	}

 #define MB_CONVERT_BUF_STORE(buf, _out, _limit) buf->out = _out; buf->limit = _limit
diff --git a/ext/mbstring/libmbfl/mbfl/mbfl_memory_device.c b/ext/mbstring/libmbfl/mbfl/mbfl_memory_device.c
index 7fbe4e703bb..6622f844c53 100644
--- a/ext/mbstring/libmbfl/mbfl/mbfl_memory_device.c
+++ b/ext/mbstring/libmbfl/mbfl/mbfl_memory_device.c
@@ -98,7 +98,11 @@ int mbfl_memory_device_output(int c, void *data)
 		}

 		size_t newlen = device->length + device->allocsz;
-		device->buffer = erealloc(device->buffer, newlen);
+		unsigned char *newbuffer = safe_ferealloc(device->buffer, 1, newlen, 0, /* fallible */ true);
+		if (!newbuffer) {
+			return -1;
+		}
+		device->buffer = newbuffer;
 		device->length = newlen;
 	}

@@ -123,7 +127,11 @@ int mbfl_memory_device_strncat(mbfl_memory_device *device, const char *psrc, siz
 		}

 		size_t newlen = device->length + len + MBFL_MEMORY_DEVICE_ALLOC_SIZE;
-		device->buffer = erealloc(device->buffer, newlen);
+		unsigned char *newbuffer = safe_ferealloc(device->buffer, 1, newlen, 0, /* fallible */ true);
+		if (!newbuffer) {
+			return -1;
+		}
+		device->buffer = newbuffer;
 		device->length = newlen;
 	}

@@ -175,7 +183,12 @@ int mbfl_wchar_device_output(int c, void *data)
 			return -1;
 		}

-		device->buffer = erealloc(device->buffer, newlen * sizeof(int));
+		unsigned int * newbuffer = safe_ferealloc(device->buffer, newlen, sizeof(int), 0, /* fallible */ true);
+		if (!newbuffer) {
+			return -1;
+		}
+
+		device->buffer = newbuffer;
 		device->length = newlen;
 	}

diff --git a/ext/mbstring/mb_gpc.c b/ext/mbstring/mb_gpc.c
index 99a8ff903a1..5d749059293 100644
--- a/ext/mbstring/mb_gpc.c
+++ b/ext/mbstring/mb_gpc.c
@@ -194,8 +194,15 @@ const mbfl_encoding *_php_mb_encoding_handler_ex(const php_mb_encoding_handler_i
 	}
 	num *= 2; /* need space for variable name and value */

-	val_list = (char **)ecalloc(num, sizeof(char *));
-	len_list = (size_t *)ecalloc(num, sizeof(size_t));
+	val_list = (char **)fecalloc(num, sizeof(char *), /* fallible */ true);
+	if (!val_list) {
+		goto out;
+	}
+
+	len_list = (size_t *)fecalloc(num, sizeof(size_t), /* fallible */ true);
+	if (!len_list) {
+		goto out;
+	}

 	/* split and decode the query */
 	n = 0;
@@ -291,6 +298,11 @@ const mbfl_encoding *_php_mb_encoding_handler_ex(const php_mb_encoding_handler_i
 			n++;
 		}
 		efree(val);
+
+		if (ZEND_HAS_OOM()) {
+			from_encoding = NULL;
+			goto out;
+		}
 	}

 out:
diff --git a/ext/mbstring/mbstring.c b/ext/mbstring/mbstring.c
index 18231eccf94..b5c1d482e9a 100644
--- a/ext/mbstring/mbstring.c
+++ b/ext/mbstring/mbstring.c
@@ -1670,7 +1670,9 @@ static int mbfl_split_output(int c, void *data)
 {
 	struct mbfl_split_params *params = (struct mbfl_split_params *)data; /* cast passed data */

-	(*params->next_filter->filter_function)(c, params->next_filter); /* decoder filter */
+	if (UNEXPECTED((*params->next_filter->filter_function)(c, params->next_filter) < 0)) { /* decoder filter */
+		return -1;
+	}

 	if (params->split_length == ++params->mb_chunk_length) { /* if current chunk size reached defined chunk size or last char reached */
 		mbfl_convert_filter_flush(params->next_filter);/* concatenate separate decoded chars to the solid string */
@@ -1680,6 +1682,9 @@ static int mbfl_split_output(int c, void *data)
 		add_next_index_stringl(params->return_value, (const char *)chunk->val, chunk->len); /* add chunk to the array */
 		efree(chunk->val);
 		params->mb_chunk_length = 0; /* reset mb_chunk size */
+		if (ZEND_HAS_OOM()) {
+			return -1;
+		}
 	}

 	return 0;
@@ -1738,7 +1743,9 @@ PHP_FUNCTION(mb_str_split)
 		char unsigned const *mbtab = mbfl_encoding->mblen_table;

 		/* assume that we have 1-bytes characters */
-		array_init_size(return_value, (string.len + split_length) / split_length); /* round up */
+		if (UNEXPECTED(!array_finit_size_packed(return_value, (string.len + split_length) / split_length /* round up */))) {
+			return;
+		}

 		while (p < last) { /* split cycle work until the cursor has reached the last byte */
 			char const *chunk_p = p; /* chunk first byte pointer */
@@ -1752,6 +1759,9 @@ PHP_FUNCTION(mb_str_split)
 			}
 			if (p >= last) chunk_len -= p - last; /* check if chunk is in bounds */
 			add_next_index_stringl(return_value, chunk_p, chunk_len);
+			if (ZEND_HAS_OOM()) {
+				break;
+			}
 		}
 		return;
 	} else {
@@ -1759,11 +1769,15 @@ PHP_FUNCTION(mb_str_split)
 		mbfl_convert_filter *filter, *decoder;

 		/* assume that we have 1-bytes characters */
-		array_init_size(return_value, (string.len + split_length) / split_length); /* round up */
+		if (UNEXPECTED(!array_finit_size_packed(return_value, (string.len + split_length) / split_length /* round up */))) {
+			return;
+		}

 		/* decoder filter to decode wchar to encoding */
 		mbfl_memory_device device;
-		mbfl_memory_device_init(&device, split_length + 1, 0);
+		/* clamp init size to avoid over allocation */
+		size_t init_size = MIN(ZSTR_LEN(str), split_length) + 1;
+		mbfl_memory_device_init(&device, init_size, 0);

 		decoder = mbfl_convert_filter_new(
 				&mbfl_encoding_wchar,
@@ -1794,10 +1808,15 @@ PHP_FUNCTION(mb_str_split)
 		ZEND_ASSERT(filter != NULL);

 		while (p < last - 1) { /* cycle each byte except last with callback function */
-			(*filter->filter_function)(*p++, filter);
+			if (UNEXPECTED((*filter->filter_function)(*p++, filter) < 0)) {
+				return;
+			}
 		}
 		params.mb_chunk_length = split_length - 1; /* force to finish current chunk */
-		(*filter->filter_function)(*p++, filter); /* process last char */
+		/* process last char */
+		if (UNEXPECTED((*filter->filter_function)(*p++, filter) < 0)) {
+			return;
+		}

 		mbfl_convert_filter_delete(decoder);
 		mbfl_convert_filter_delete(filter);
@@ -1807,12 +1826,17 @@ PHP_FUNCTION(mb_str_split)

 	/* first scenario: 1,2,4-bytes fixed width encodings (tail part) */
 	chunks = (mb_len + split_length - 1) / split_length; /* (round up idiom) */
-	array_init_size(return_value, chunks);
+	if (UNEXPECTED(!array_finit_size_packed(return_value, chunks))) {
+		return;
+	}
 	if (chunks != 0) {
 		zend_long i;

 		for (i = 0; i < chunks - 1; p += chunk_len, ++i) {
 			add_next_index_stringl(return_value, p, chunk_len);
+			if (ZEND_HAS_OOM()) {
+				return;
+			}
 		}
 		add_next_index_stringl(return_value, p, last - p);
 	}
@@ -2068,13 +2092,13 @@ static void php_mb_strstr_variants(INTERNAL_FUNCTION_PARAMETERS, unsigned int va
 			ret = mbfl_substr(&haystack, &result, 0, n);
 			ZEND_ASSERT(ret != NULL);
 			// TODO: avoid reallocation ???
-			RETVAL_STRINGL((char *)ret->val, ret->len);
+			RETVAL_FSTRINGL((char *)ret->val, ret->len);
 			efree(ret->val);
 		} else {
 			ret = mbfl_substr(&haystack, &result, n, MBFL_SUBSTR_UNTIL_END);
 			ZEND_ASSERT(ret != NULL);
 			// TODO: avoid reallocation ???
-			RETVAL_STRINGL((char *)ret->val, ret->len);
+			RETVAL_FSTRINGL((char *)ret->val, ret->len);
 			efree(ret->val);
 		}
 	} else {
@@ -2145,10 +2169,13 @@ PHP_FUNCTION(mb_substr_count)

 	size_t n = mbfl_substr_count(&haystack, &needle);
 	/* An error can only occur if needle is empty,
-	 * an encoding error happens (which should not happen at this stage and is a bug)
-	 * or the haystack is more than sizeof(size_t) bytes
-	 * If one of these things occur this is a bug and should be flagged as such */
-	ZEND_ASSERT(!mbfl_is_error(n));
+	 * an encoding error happens (which should not happen at this stage and is a bug),
+	 * the haystack is more than sizeof(size_t) bytes,
+	 * or on OOM.
+	 * If one of these things other than OOM occur this is a bug and should be flagged as such */
+	if (mbfl_is_error(n)) {
+		RETURN_OOM();
+	}
 	RETVAL_LONG(n);
 }
 /* }}} */
@@ -2224,7 +2251,7 @@ PHP_FUNCTION(mb_substr)
 	ZEND_ASSERT(ret != NULL);

 	// TODO: avoid reallocation ???
-	RETVAL_STRINGL((char *)ret->val, ret->len); /* the string is already strdup()'ed */
+	RETVAL_FSTRINGL((char *)ret->val, ret->len); /* the string is already strdup()'ed */
 	efree(ret->val);
 }
 /* }}} */
@@ -2281,10 +2308,12 @@ PHP_FUNCTION(mb_strcut)
 	}

 	ret = mbfl_strcut(&string, &result, from, len);
-	ZEND_ASSERT(ret != NULL);
+	if (UNEXPECTED(ret == NULL)) {
+		RETURN_OOM();
+	}

 	// TODO: avoid reallocation ???
-	RETVAL_STRINGL((char *)ret->val, ret->len); /* the string is already strdup()'ed */
+	RETVAL_FSTRINGL((char *)ret->val, ret->len); /* the string is already strdup()'ed */
 	efree(ret->val);
 }
 /* }}} */
@@ -2499,7 +2528,9 @@ static zend_string* mb_trim_string(zend_string *input, zend_string *marker, cons

 append_trim_marker:
 	if (ZSTR_LEN(marker) > 0) {
-		MB_CONVERT_BUF_ENSURE((&buf), buf.out, buf.limit, ZSTR_LEN(marker));
+		if (UNEXPECTED(mb_convert_buf_ensure(&buf, &buf.out, &buf.limit, ZSTR_LEN(marker)) == FAILURE)) {
+			return mb_convert_buf_result(&buf);
+		}
 		memcpy(buf.out, ZSTR_VAL(marker), ZSTR_LEN(marker));
 		buf.out += ZSTR_LEN(marker);
 	}
@@ -3044,7 +3075,9 @@ PHP_FUNCTION(mb_encode_mimeheader)

 	mbfl_string_init(&result);
 	ret = mbfl_mime_header_encode(&string, &result, charset, transenc, linefeed, indent);
-	ZEND_ASSERT(ret != NULL);
+	if (UNEXPECTED(ret == NULL)) {
+		RETURN_OOM();
+	}
 	// TODO: avoid reallocation ???
 	RETVAL_STRINGL((char *)ret->val, ret->len);	/* the string is already strdup()'ed */
 	efree(ret->val);
@@ -3066,7 +3099,10 @@ PHP_FUNCTION(mb_decode_mimeheader)
 	string.val = (unsigned char*)string_val;
 	mbfl_string_init(&result);
 	ret = mbfl_mime_header_decode(&string, &result, MBSTRG(current_internal_encoding));
-	ZEND_ASSERT(ret != NULL);
+	if (UNEXPECTED(ret == NULL)) {
+		RETURN_OOM();
+	}
+
 	// TODO: avoid reallocation ???
 	RETVAL_STRINGL((char *)ret->val, ret->len);	/* the string is already strdup()'ed */
 	efree(ret->val);
@@ -3302,6 +3338,9 @@ static int mb_recursive_convert_variable(mbfl_buffer_converter *convd, zval *var
 			ZVAL_STRINGL(orig_var, (const char *) ret->val, ret->len);
 			efree(ret->val);
 		}
+		if (ZEND_HAS_OOM()) {
+			return 1;
+		}
 	} else if (Z_TYPE_P(var) == IS_ARRAY || Z_TYPE_P(var) == IS_OBJECT) {
 		if (Z_TYPE_P(var) == IS_ARRAY) {
 			SEPARATE_ARRAY(var);
diff --git a/ext/mbstring/php_mbregex.c b/ext/mbstring/php_mbregex.c
index d37d3e96f23..82479ecdfab 100644
--- a/ext/mbstring/php_mbregex.c
+++ b/ext/mbstring/php_mbregex.c
@@ -726,6 +726,10 @@ static inline void mb_regex_substitute(
 	eos = replace + replace_len;

 	while (p < eos) {
+		if (ZEND_HAS_OOM()) {
+			return;
+		}
+
 		clen = (int) php_mb_mbchar_bytes(p, enc);
 		if (clen != 1 || p == eos || p[0] != '\\') {
 			/* skip anything that's not an ascii backslash */
@@ -1236,6 +1240,10 @@ PHP_FUNCTION(mb_split)
 			pos++;
 		}
 		onig_region_free(regs, 0);
+		if (ZEND_HAS_OOM()) {
+			err = -2;
+			break;
+		}
 	}

 	onig_region_free(regs, 1);
diff --git a/ext/mbstring/php_unicode.c b/ext/mbstring/php_unicode.c
index 0da63fe1e6a..05f02e6a478 100644
--- a/ext/mbstring/php_unicode.c
+++ b/ext/mbstring/php_unicode.c
@@ -229,60 +229,84 @@ static inline unsigned php_unicode_tofold_simple(unsigned code, enum mbfl_no_enc
 	return code;
 }

-static inline void php_unicode_tolower_full(unsigned code, enum mbfl_no_encoding enc,
+static inline int php_unicode_tolower_full(unsigned code, enum mbfl_no_encoding enc,
 	mbfl_convert_filter* next_filter) {
 	code = php_unicode_tolower_raw(code, enc);
 	if (UNEXPECTED(code > 0xffffff)) {
 		unsigned len = code >> 24;
 		const unsigned *p = &_uccase_extra_table[code & 0xffffff];
 		while (len--) {
-			(next_filter->filter_function)(*++p, next_filter);
+			if ((next_filter->filter_function)(*++p, next_filter) < 0) {
+				return -1;
+			}
 		}
 	} else {
-		(next_filter->filter_function)(code, next_filter);
+		if ((next_filter->filter_function)(code, next_filter) < 0) {
+			return -1;
+		}
 	}
+
+	return 0;
 }

-static inline void php_unicode_toupper_full(unsigned code, enum mbfl_no_encoding enc,
+static inline int php_unicode_toupper_full(unsigned code, enum mbfl_no_encoding enc,
 	mbfl_convert_filter* next_filter) {
 	code = php_unicode_toupper_raw(code, enc);
 	if (UNEXPECTED(code > 0xffffff)) {
 		unsigned len = code >> 24;
 		const unsigned *p = &_uccase_extra_table[code & 0xffffff];
 		while (len--) {
-			(next_filter->filter_function)(*++p, next_filter);
+			if ((next_filter->filter_function)(*++p, next_filter) < 0) {
+				return -1;
+			}
 		}
 	} else {
-		(next_filter->filter_function)(code, next_filter);
+		if ((next_filter->filter_function)(code, next_filter) < 0) {
+			return -1;
+		}
 	}
+
+	return 0;
 }

-static inline void php_unicode_totitle_full(unsigned code, enum mbfl_no_encoding enc,
+static inline int php_unicode_totitle_full(unsigned code, enum mbfl_no_encoding enc,
 	mbfl_convert_filter* next_filter) {
 	code = php_unicode_totitle_raw(code, enc);
 	if (UNEXPECTED(code > 0xffffff)) {
 		unsigned len = code >> 24;
 		const unsigned *p = &_uccase_extra_table[code & 0xffffff];
 		while (len--) {
-			(next_filter->filter_function)(*++p, next_filter);
+			if ((next_filter->filter_function)(*++p, next_filter) < 0) {
+				return -1;
+			}
 		}
 	} else {
-		(next_filter->filter_function)(code, next_filter);
+		if ((next_filter->filter_function)(code, next_filter) < 0) {
+			return -1;
+		}
 	}
+
+	return 0;
 }

-static inline void php_unicode_tofold_full(unsigned code, enum mbfl_no_encoding enc,
+static inline int php_unicode_tofold_full(unsigned code, enum mbfl_no_encoding enc,
 	mbfl_convert_filter* next_filter) {
 	code = php_unicode_tofold_raw(code, enc);
 	if (UNEXPECTED(code > 0xffffff)) {
 		unsigned len = code >> 24;
 		const unsigned *p = &_uccase_extra_table[code & 0xffffff];
 		while (len--) {
-			(next_filter->filter_function)(*++p, next_filter);
+			if ((next_filter->filter_function)(*++p, next_filter) < 0) {
+				return -1;
+			}
 		}
 	} else {
-		(next_filter->filter_function)(code, next_filter);
+		if ((next_filter->filter_function)(code, next_filter) < 0) {
+			return -1;
+		}
 	}
+
+	return 0;
 }

 struct convert_case_data {
@@ -300,36 +324,36 @@ static int convert_case_filter(int c, void *void_data)
 	/* Handle invalid characters early, as we assign special meaning to
 	 * codepoints above 0xffffff. */
 	if (UNEXPECTED((unsigned) c > 0xffffff)) {
-		(*data->next_filter->filter_function)(c, data->next_filter);
-		return 0;
+		return (*data->next_filter->filter_function)(c, data->next_filter);
 	}

+	int res;
 	switch (data->case_mode) {
 		case PHP_UNICODE_CASE_UPPER_SIMPLE:
 			code = php_unicode_toupper_simple(c, data->no_encoding);
-			(data->next_filter->filter_function)(code, data->next_filter);
+			res = (data->next_filter->filter_function)(code, data->next_filter);
 			break;

 		case PHP_UNICODE_CASE_UPPER:
-			php_unicode_toupper_full(c, data->no_encoding, data->next_filter);
+			res = php_unicode_toupper_full(c, data->no_encoding, data->next_filter);
 			break;

 		case PHP_UNICODE_CASE_LOWER_SIMPLE:
 			code = php_unicode_tolower_simple(c, data->no_encoding);
-			(data->next_filter->filter_function)(code, data->next_filter);
+			res = (data->next_filter->filter_function)(code, data->next_filter);
 			break;

 		case PHP_UNICODE_CASE_LOWER:
-			php_unicode_tolower_full(c, data->no_encoding, data->next_filter);
+			res = php_unicode_tolower_full(c, data->no_encoding, data->next_filter);
 			break;

 		case PHP_UNICODE_CASE_FOLD:
-			php_unicode_tofold_full(c, data->no_encoding, data->next_filter);
+			res = php_unicode_tofold_full(c, data->no_encoding, data->next_filter);
 			break;

 		case PHP_UNICODE_CASE_FOLD_SIMPLE:
 			code = php_unicode_tofold_simple(c, data->no_encoding);
-			(data->next_filter->filter_function)(code, data->next_filter);
+			res = (data->next_filter->filter_function)(code, data->next_filter);
 			break;

 		case PHP_UNICODE_CASE_TITLE_SIMPLE:
@@ -338,16 +362,16 @@ static int convert_case_filter(int c, void *void_data)
 			if (data->title_mode) {
 				if (data->case_mode == PHP_UNICODE_CASE_TITLE_SIMPLE) {
 					code = php_unicode_tolower_simple(c, data->no_encoding);
-					(data->next_filter->filter_function)(code, data->next_filter);
+					res = (data->next_filter->filter_function)(code, data->next_filter);
 				} else {
-					php_unicode_tolower_full(c, data->no_encoding, data->next_filter);
+					res = php_unicode_tolower_full(c, data->no_encoding, data->next_filter);
 				}
 			} else {
 				if (data->case_mode == PHP_UNICODE_CASE_TITLE_SIMPLE) {
 					code = php_unicode_totitle_simple(c, data->no_encoding);
-					(data->next_filter->filter_function)(code, data->next_filter);
+					res = (data->next_filter->filter_function)(code, data->next_filter);
 				} else {
-					php_unicode_totitle_full(c, data->no_encoding, data->next_filter);
+					res = php_unicode_totitle_full(c, data->no_encoding, data->next_filter);
 				}
 			}
 			if (!php_unicode_is_case_ignorable(c)) {
@@ -358,7 +382,7 @@ static int convert_case_filter(int c, void *void_data)
 		EMPTY_SWITCH_DEFAULT_CASE()
 	}

-	return 0;
+	return res;
 }

 MBSTRING_API char *php_unicode_convert_case(
diff --git a/ext/mbstring/tests/oom_buf_growth.phpt b/ext/mbstring/tests/oom_buf_growth.phpt
new file mode 100644
index 00000000000..04f563a6d3a
--- /dev/null
+++ b/ext/mbstring/tests/oom_buf_growth.phpt
@@ -0,0 +1,17 @@
+--TEST--
+mbstring stores conversion-buffer pointers when growth fails
+--EXTENSIONS--
+mbstring
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+// A single batch expands from 128 to 384 bytes, requiring two buffer growths.
+$input = str_repeat("\x80", 128);
+// The tracked allocator leaves enough space for the first growth, but not the second.
+$padding = str_repeat('a', 8 * 1024 * 1024 - memory_get_usage() - 384);
+mb_substr_count($input, 'Z', 'CP1252');
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/mbstring/tests/oom_mb_convert_case.phpt b/ext/mbstring/tests/oom_mb_convert_case.phpt
new file mode 100644
index 00000000000..8a320c6c154
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_convert_case.phpt
@@ -0,0 +1,14 @@
+--TEST--
+mb_convert_case() doesn't reach hard limit
+--EXTENSIONS--
+mbstring
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+// 2 MB == memory_limit / 4
+mb_convert_case(str_repeat("\u{0390}", 1024 * 1024), MB_CASE_UPPER, "UTF-8");
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/mbstring/tests/oom_mb_convert_encoding.phpt b/ext/mbstring/tests/oom_mb_convert_encoding.phpt
new file mode 100644
index 00000000000..9ac547e1338
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_convert_encoding.phpt
@@ -0,0 +1,14 @@
+--TEST--
+mb_convert_encoding() doesn't reach hard limit
+--EXTENSIONS--
+mbstring
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+// 2 MB == memory_limit / 4
+mb_convert_encoding(str_repeat("a", 2 * 1024 * 1024), "UTF-32", "ISO-8859-1");
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/mbstring/tests/oom_mb_convert_variables.phpt b/ext/mbstring/tests/oom_mb_convert_variables.phpt
new file mode 100644
index 00000000000..b4df748d1d4
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_convert_variables.phpt
@@ -0,0 +1,14 @@
+--TEST--
+mb_convert_variables() doesn't reach hard limit
+--EXTENSIONS--
+mbstring
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$value = str_repeat("a", 2 * 1024 * 1024); // 2 MB == memory_limit / 4
+mb_convert_variables("UTF-32", "ISO-8859-1", $value);
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/mbstring/tests/oom_mb_convert_variables_many.phpt b/ext/mbstring/tests/oom_mb_convert_variables_many.phpt
new file mode 100644
index 00000000000..dd071b57b8c
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_convert_variables_many.phpt
@@ -0,0 +1,15 @@
+--TEST--
+mb_convert_variables() doesn't reach hard limit - many strings
+--EXTENSIONS--
+mbstring
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$values = array_fill(0, 200000, "abc");
+mb_convert_variables("UTF-8", "UTF-8", $values);
+
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/mbstring/tests/oom_mb_decode_mimeheader.phpt b/ext/mbstring/tests/oom_mb_decode_mimeheader.phpt
new file mode 100644
index 00000000000..85bbddd7297
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_decode_mimeheader.phpt
@@ -0,0 +1,15 @@
+--TEST--
+mb_decode_mimeheader() doesn't reach hard limit
+--EXTENSIONS--
+mbstring
+--INI--
+memory_limit=8M
+memory_hard_limit_percent=125
+--FILE--
+<?php
+$encoded = str_repeat("=?UTF-8?B?w6nDqcOp?= ", (int) (5500 * 1024 / 21));
+mb_decode_mimeheader($encoded);
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line 3
diff --git a/ext/mbstring/tests/oom_mb_encode_mimeheader.phpt b/ext/mbstring/tests/oom_mb_encode_mimeheader.phpt
new file mode 100644
index 00000000000..db8c328f269
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_encode_mimeheader.phpt
@@ -0,0 +1,14 @@
+--TEST--
+mb_encode_mimeheader() doesn't reach hard limit
+--EXTENSIONS--
+mbstring
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+// 2 MB == memory_limit / 4
+mb_encode_mimeheader(str_repeat("\xFF", 2 * 1024 * 1024), "ISO-8859-1", "Q");
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/mbstring/tests/oom_mb_encode_numericentity.phpt b/ext/mbstring/tests/oom_mb_encode_numericentity.phpt
new file mode 100644
index 00000000000..dbf63c580c3
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_encode_numericentity.phpt
@@ -0,0 +1,16 @@
+--TEST--
+mb_encode_numericentity() doesn't reach hard limit
+--EXTENSIONS--
+mbstring
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+// 2 MB == memory_limit / 4
+$map = [0x0, 0x10FFFF, 0, 0xFFFFFF];
+$string = str_repeat("a", 2 * 1024 * 1024);
+mb_encode_numericentity($string, $map, "UTF-8", true);
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/mbstring/tests/oom_mb_ereg_replace.phpt b/ext/mbstring/tests/oom_mb_ereg_replace.phpt
new file mode 100644
index 00000000000..0041a4c1985
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_ereg_replace.phpt
@@ -0,0 +1,18 @@
+--TEST--
+mb_ereg_replace() doesn't reach hard limit
+--EXTENSIONS--
+mbstring
+--SKIPIF--
+<?php
+if (!function_exists("mb_ereg_replace")) die("skip mbregex support unavailable");
+?>
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+// 2 MB == memory_limit / 4
+mb_ereg_replace("a", "bbbbbbbbbb", str_repeat("a", 2 * 1024 * 1024));
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/mbstring/tests/oom_mb_eregi_replace.phpt b/ext/mbstring/tests/oom_mb_eregi_replace.phpt
new file mode 100644
index 00000000000..9d72892941d
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_eregi_replace.phpt
@@ -0,0 +1,18 @@
+--TEST--
+mb_eregi_replace() doesn't reach hard limit
+--EXTENSIONS--
+mbstring
+--SKIPIF--
+<?php
+if (!function_exists("mb_eregi_replace")) die("skip mbregex support unavailable");
+?>
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+// 2 MB == memory_limit / 4
+mb_eregi_replace("a", "bbbbbbbbbb", str_repeat("a", 2 * 1024 * 1024));
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/mbstring/tests/oom_mb_parse_str.phpt b/ext/mbstring/tests/oom_mb_parse_str.phpt
new file mode 100644
index 00000000000..ad3414063e5
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_parse_str.phpt
@@ -0,0 +1,14 @@
+--TEST--
+mb_parse_str() doesn't reach hard limit
+--EXTENSIONS--
+mbstring
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+// 2 MB == memory_limit / 4
+mb_parse_str(str_repeat("a&", 1024 * 1024), $result);
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/mbstring/tests/oom_mb_split.phpt b/ext/mbstring/tests/oom_mb_split.phpt
new file mode 100644
index 00000000000..4d375a0a313
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_split.phpt
@@ -0,0 +1,17 @@
+--TEST--
+mb_split() doesn't reach hard limit
+--EXTENSIONS--
+mbstring
+--SKIPIF--
+<?php
+if (!function_exists("mb_split")) die("skip mbregex support unavailable");
+?>
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+mb_split("a", str_repeat("a", 2 * 1024 * 1024)); // 2 MB == memory_limit / 4
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/mbstring/tests/oom_mb_str_split.phpt b/ext/mbstring/tests/oom_mb_str_split.phpt
new file mode 100644
index 00000000000..6c3fe7ab208
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_str_split.phpt
@@ -0,0 +1,13 @@
+--TEST--
+mb_str_split() doesn't reach hard limit
+--EXTENSIONS--
+mbstring
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+mb_str_split(str_repeat("a", 2 * 1024 * 1024)); // 2 MB == memory_limit / 4
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/mbstring/tests/oom_mb_str_split_filter.phpt b/ext/mbstring/tests/oom_mb_str_split_filter.phpt
new file mode 100644
index 00000000000..b427572cc37
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_str_split_filter.phpt
@@ -0,0 +1,14 @@
+--TEST--
+mb_str_split() doesn't reach hard limit - encodings without a length table
+--EXTENSIONS--
+mbstring
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+// 2 MB == memory_limit / 4
+mb_str_split(str_repeat("a", 2 * 1024 * 1024), 1, "UTF-7");
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/mbstring/tests/oom_mb_str_split_fixed_width.phpt b/ext/mbstring/tests/oom_mb_str_split_fixed_width.phpt
new file mode 100644
index 00000000000..c22de7588b4
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_str_split_fixed_width.phpt
@@ -0,0 +1,14 @@
+--TEST--
+mb_str_split() doesn't reach hard limit - fixed width encodings
+--EXTENSIONS--
+mbstring
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+// 2 MB == memory_limit / 4
+mb_str_split(str_repeat("a", 2 * 1024 * 1024), 1, "ASCII");
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/mbstring/tests/oom_mb_str_split_length.phpt b/ext/mbstring/tests/oom_mb_str_split_length.phpt
new file mode 100644
index 00000000000..46e8f8fddf8
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_str_split_length.phpt
@@ -0,0 +1,15 @@
+--TEST--
+mb_str_split() doesn't over allocate - split length argument
+--EXTENSIONS--
+mbstring
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+var_dump(mb_str_split("abc", 100 * 1024 * 1024, "UTF-7"));
+?>
+--EXPECT--
+array(1) {
+  [0]=>
+  string(3) "abc"
+}
diff --git a/ext/mbstring/tests/oom_mb_strcut_filter.phpt b/ext/mbstring/tests/oom_mb_strcut_filter.phpt
new file mode 100644
index 00000000000..01102259286
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_strcut_filter.phpt
@@ -0,0 +1,19 @@
+--TEST--
+mb_strcut() doesn't reach hard limit - encodings without a length table
+--EXTENSIONS--
+mbstring
+--INI--
+memory_limit=8M
+memory_hard_limit_percent=125
+--FILE--
+<?php
+// UTF-7 has no length table, so mb_strcut() converts through a filter chain
+// into a memory device sized for the input. UTF-7 re-encodes "_" as "+AF8-",
+// so the device has to grow while the limit is already exhausted.
+// 2 MB == memory_limit / 4
+$string = str_repeat("_", 2 * 1024 * 1024);
+mb_strcut($string, 0, strlen($string), "UTF-7");
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/mbstring/tests/oom_mb_strcut_length.phpt b/ext/mbstring/tests/oom_mb_strcut_length.phpt
new file mode 100644
index 00000000000..8c02abfed6a
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_strcut_length.phpt
@@ -0,0 +1,12 @@
+--TEST--
+mb_strcut() doesn't over allocate - length argument
+--EXTENSIONS--
+mbstring
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+var_dump(mb_strcut("abc", 0, 100 * 1024 * 1024, "UTF-7"));
+?>
+--EXPECT--
+string(3) "abc"
diff --git a/ext/mbstring/tests/oom_mb_strimwidth.phpt b/ext/mbstring/tests/oom_mb_strimwidth.phpt
new file mode 100644
index 00000000000..5fc942585c4
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_strimwidth.phpt
@@ -0,0 +1,17 @@
+--TEST--
+mb_strimwidth() doesn't reach hard limit - trim marker
+--EXTENSIONS--
+mbstring
+--INI--
+memory_limit=16M
+--FILE--
+<?php
+
+$string = str_repeat("a", 6 * 1024 * 1024);
+$marker = str_repeat("b", 5 * 1024 * 1024);
+
+mb_strimwidth($string, 0, 5 * 1024 * 1024 + 512 * 1024, $marker);
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 16777216 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/mbstring/tests/oom_mb_strtoupper.phpt b/ext/mbstring/tests/oom_mb_strtoupper.phpt
new file mode 100644
index 00000000000..814f34a1788
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_strtoupper.phpt
@@ -0,0 +1,18 @@
+--TEST--
+mb_strtoupper() doesn't reach hard limit
+--EXTENSIONS--
+mbstring
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+// 2 MB == memory_limit / 4
+mb_strtoupper(str_repeat("\u{0390}", 1024 * 1024), "UTF-8");
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/mbstring/tests/oom_mb_substr_count.phpt b/ext/mbstring/tests/oom_mb_substr_count.phpt
new file mode 100644
index 00000000000..9e346ed86f3
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_substr_count.phpt
@@ -0,0 +1,14 @@
+--TEST--
+mb_substr_count() doesn't reach hard limit
+--EXTENSIONS--
+mbstring
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+// 3 MB needle -> 12 MB wchar buffer, memory_limit is 8 MB
+mb_substr_count("abc", str_repeat("a", 3 * 1024 * 1024));
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/mbstring/tests/oom_mb_substr_length.phpt b/ext/mbstring/tests/oom_mb_substr_length.phpt
new file mode 100644
index 00000000000..dd373eca627
--- /dev/null
+++ b/ext/mbstring/tests/oom_mb_substr_length.phpt
@@ -0,0 +1,12 @@
+--TEST--
+mb_substr() doesn't over allocate - length argument
+--EXTENSIONS--
+mbstring
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+var_dump(mb_substr("abc", 0, 100 * 1024 * 1024, "UTF-7"));
+?>
+--EXPECT--
+string(3) "abc"
diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c
index 1afcff2d1c2..586e3aaa47d 100644
--- a/ext/opcache/ZendAccelerator.c
+++ b/ext/opcache/ZendAccelerator.c
@@ -612,14 +612,17 @@ static zend_string* ZEND_FASTCALL accel_init_interned_string_for_php(const char
 		zend_string *ret = accel_find_interned_string_ex(h, str, size);

 		if (!ret) {
-			ret = zend_string_init(str, size, permanent);
+			ret = zend_string_finit(str, size, permanent, CG(interned_strings_fallible));
+			if (UNEXPECTED(!ret)) {
+				return NULL;
+			}
 			ZSTR_H(ret) = h;
 		}

 		return ret;
 	}

-	return zend_string_init(str, size, permanent);
+	return zend_string_finit(str, size, permanent, CG(interned_strings_fallible));
 }

 static inline void accel_copy_permanent_list_types(
@@ -2409,6 +2412,7 @@ static zend_class_entry* zend_accel_inheritance_cache_add(zend_class_entry *ce,
 	}

 	zend_map_ptr_extend(ZCSG(map_ptr_last));
+	zend_accel_pending_ce_cache_begin();

 #if ZEND_MM_NEED_EIGHT_BYTE_REALIGNMENT
 	/* Align to 8-byte boundary */
@@ -2455,6 +2459,7 @@ static zend_class_entry* zend_accel_inheritance_cache_add(zend_class_entry *ce,
 	EG(errors) = NULL;

 	ZCSG(map_ptr_last) = CG(map_ptr_last);
+	zend_accel_pending_ce_cache_commit();

 	zend_shared_alloc_destroy_xlat_table();

@@ -2779,6 +2784,9 @@ zend_result accel_post_deactivate(void)
 		ZCG(cwd) = NULL;
 	}

+	/* A persist window left through a bailout never published its slots */
+	zend_accel_pending_ce_cache_discard();
+
 	if (!ZCG(enabled) || !accel_startup_ok) {
 		return SUCCESS;
 	}
@@ -4721,6 +4729,7 @@ static int accel_finish_startup(void)
 		EG(error_reporting) = 0;

 		rc = php_request_startup();
+		zend_mm_set_max_allocs(0);

 		EG(error_reporting) = orig_error_reporting;

diff --git a/ext/opcache/ZendAccelerator.h b/ext/opcache/ZendAccelerator.h
index bc92854c355..d29c30bc06f 100644
--- a/ext/opcache/ZendAccelerator.h
+++ b/ext/opcache/ZendAccelerator.h
@@ -230,6 +230,12 @@ typedef struct _zend_accel_globals {
 	/* preallocated buffer for keys */
 	zend_string             key;
 	char                    _key[MAXPATHLEN * 8];
+
+	/* CE cache slots allocated during the current persist window, keyed by the
+	 * address of the shared class name string. They are written into the
+	 * strings by zend_accel_pending_ce_cache_commit() once ZCSG(map_ptr_last)
+	 * covers them. NULL outside a persist window. */
+	HashTable              *pending_ce_cache;
 } zend_accel_globals;

 typedef struct _zend_string_table {
@@ -329,7 +335,10 @@ zend_op_array *persistent_compile_file(zend_file_handle *file_handle, int type);

 zend_string* ZEND_FASTCALL accel_new_interned_string(zend_string *str);

-uint32_t zend_accel_get_class_name_map_ptr(zend_string *type_name);
+void zend_accel_get_class_name_map_ptr(zend_string *type_name);
+void zend_accel_pending_ce_cache_begin(void);
+void zend_accel_pending_ce_cache_commit(void);
+void zend_accel_pending_ce_cache_discard(void);

 END_EXTERN_C()

diff --git a/ext/opcache/jit/zend_jit_arm64.dasc b/ext/opcache/jit/zend_jit_arm64.dasc
index 1e31c075d6b..69f5f4ff287 100644
--- a/ext/opcache/jit/zend_jit_arm64.dasc
+++ b/ext/opcache/jit/zend_jit_arm64.dasc
@@ -1794,6 +1794,18 @@ static int zend_jit_interrupt_handler_stub(dasm_State **Dst)
 	|	//zend_timeout();
 	|	EXT_CALL zend_timeout, TMP1
 	|1:
+	|	//} else if (ZEND_HAS_OOM()) {
+	ZEND_ASSERT(XtOffsetOf(zend_oom_info, state) == 0);
+	|	MEM_LOAD_8_ZTS ldrb, TMP1w, executor_globals, delayed_oom, TMP1
+	|	cbz TMP1w, >1
+	|	//zend_handle_delayed_oom();
+	|	EXT_CALL zend_handle_delayed_oom, TMP1
+	|	EXT_CALL zend_jit_exception_in_interrupt_handler_helper, TMP1
+	|	//ZEND_VM_ENTER();
+	|	//execute_data = EG(current_execute_data);
+	|	MEM_LOAD_64_ZTS ldr, FP, executor_globals, current_execute_data, TMP1
+	|	LOAD_IP
+	|1:
 	|	//} else if (zend_interrupt_function) {
 	if (zend_interrupt_function) {
 		|	//zend_interrupt_function(execute_data);
diff --git a/ext/opcache/jit/zend_jit_disasm.c b/ext/opcache/jit/zend_jit_disasm.c
index c49a0df863a..7aec3e62319 100644
--- a/ext/opcache/jit/zend_jit_disasm.c
+++ b/ext/opcache/jit/zend_jit_disasm.c
@@ -581,6 +581,7 @@ static int zend_jit_disasm_init(void)
 	REGISTER_EG(vm_interrupt);
 	REGISTER_EG(exception_op);
 	REGISTER_EG(timed_out);
+	REGISTER_EG(delayed_oom);
 	REGISTER_EG(current_execute_data);
 	REGISTER_EG(vm_stack_top);
 	REGISTER_EG(vm_stack_end);
diff --git a/ext/opcache/jit/zend_jit_x86.dasc b/ext/opcache/jit/zend_jit_x86.dasc
index aff19300cd7..baed662fae1 100644
--- a/ext/opcache/jit/zend_jit_x86.dasc
+++ b/ext/opcache/jit/zend_jit_x86.dasc
@@ -1723,6 +1723,18 @@ static int zend_jit_interrupt_handler_stub(dasm_State **Dst)
 	|	//zend_timeout();
 	|	EXT_CALL zend_timeout, r0
 	|1:
+	|	//} else if (ZEND_HAS_OOM()) {
+	ZEND_ASSERT(XtOffsetOf(zend_oom_info, state) == 0);
+	|	MEM_CMP_ZTS byte, executor_globals, delayed_oom, 0, r0
+	|	je >1
+	|	//zend_handle_delayed_oom();
+	|	EXT_CALL zend_handle_delayed_oom, r0
+	|	EXT_CALL zend_jit_exception_in_interrupt_handler_helper, r0
+	|	//ZEND_VM_ENTER();
+	|	//execute_data = EG(current_execute_data);
+	|	MEM_LOAD_ZTS FP, aword, executor_globals, current_execute_data, r0
+	|	LOAD_IP
+	|1:
 	|	//} else if (zend_interrupt_function) {
 	if (zend_interrupt_function) {
 		|	//zend_interrupt_function(execute_data);
diff --git a/ext/opcache/zend_file_cache.c b/ext/opcache/zend_file_cache.c
index a42e5370914..b3161b51fd7 100644
--- a/ext/opcache/zend_file_cache.c
+++ b/ext/opcache/zend_file_cache.c
@@ -1770,6 +1770,7 @@ zend_persistent_script *zend_file_cache_script_load(zend_file_handle *file_handl
 	bool cache_it = true;
 	unsigned int actual_checksum;
 	bool ok;
+	size_t orig_map_ptr_last = 0;

 	if (!full_path) {
 		return NULL;
@@ -1899,6 +1900,8 @@ zend_persistent_script *zend_file_cache_script_load(zend_file_handle *file_handl
 		}
 		memcpy(buf, mem, info.mem_size);
 		zend_map_ptr_extend(ZCSG(map_ptr_last));
+		orig_map_ptr_last = CG(map_ptr_last);
+		zend_accel_pending_ce_cache_begin();
 	} else {
 use_process_mem:
 		buf = mem;
@@ -1917,6 +1920,10 @@ zend_persistent_script *zend_file_cache_script_load(zend_file_handle *file_handl
 	} zend_end_try();
 	if (!ok) {
 		if (cache_it) {
+			/* The slots allocated by the failed unserialization are referenced
+			 * only by the abandoned buffer, so they can be reclaimed. */
+			zend_accel_pending_ce_cache_discard();
+			CG(map_ptr_last) = orig_map_ptr_last;
 			zend_shared_alloc_unlock();
 			goto use_process_mem;
 		} else {
@@ -1930,6 +1937,7 @@ zend_persistent_script *zend_file_cache_script_load(zend_file_handle *file_handl

 	if (cache_it) {
 		ZCSG(map_ptr_last) = CG(map_ptr_last);
+		zend_accel_pending_ce_cache_commit();
 		script->dynamic_members.checksum = zend_accel_script_checksum(script);
 		script->dynamic_members.last_used = ZCG(request_time);

diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c
index 8272e10be0a..f1de96cbaa0 100644
--- a/ext/opcache/zend_persist.c
+++ b/ext/opcache/zend_persist.c
@@ -311,31 +311,78 @@ static HashTable *zend_persist_attributes(HashTable *attributes)
 	return ptr;
 }

-uint32_t zend_accel_get_class_name_map_ptr(zend_string *type_name)
+/* We delay the publishing of CE cache slots until after ZCSG(map_ptr_last) is
+ * published.
+ * CE cache slots stored in interned strings are immediately visible to other
+ * processes, so if we fail to update ZCSG(map_ptr_last) after publishing a
+ * slot (due to a bailout/exit), that slot may be re-allocated for something
+ * else by other processes. */
+void zend_accel_pending_ce_cache_begin(void)
+{
+	ZEND_ASSERT(ZCG(locked));
+
+	/* A persist window left through a bailout caught by the caller (e.g.
+	 * opcache_compile_file()) never published its slots. Drop them. */
+	zend_accel_pending_ce_cache_discard();
+
+	ZCG(pending_ce_cache) = emalloc(sizeof(HashTable));
+	zend_hash_init(ZCG(pending_ce_cache), 8, NULL, NULL, 0);
+}
+
+void zend_accel_pending_ce_cache_commit(void)
+{
+	ZEND_ASSERT(ZCG(pending_ce_cache));
+	/* Every pending slot must be covered by ZCSG(map_ptr_last) */
+	ZEND_ASSERT(ZCSG(map_ptr_last) == CG(map_ptr_last));
+
+	ZEND_HASH_FOREACH_NUM_KEY_VAL(ZCG(pending_ce_cache), zend_ulong h, zval *zv) {
+		zend_string *type_name = (zend_string*)(uintptr_t)h;
+
+		ZEND_ASSERT(!ZSTR_HAS_CE_CACHE(type_name));
+		GC_SET_REFCOUNT(type_name, (uint32_t)Z_LVAL_P(zv));
+		GC_ADD_FLAGS(type_name, IS_STR_CLASS_NAME_MAP_PTR);
+	} ZEND_HASH_FOREACH_END();
+
+	zend_accel_pending_ce_cache_discard();
+}
+
+void zend_accel_pending_ce_cache_discard(void)
+{
+	if (ZCG(pending_ce_cache)) {
+		zend_hash_destroy(ZCG(pending_ce_cache));
+		efree(ZCG(pending_ce_cache));
+		ZCG(pending_ce_cache) = NULL;
+	}
+}
+
+void zend_accel_get_class_name_map_ptr(zend_string *type_name)
 {
 	uint32_t ret;
+	zval *slot;

 	if (zend_string_equals_literal_ci(type_name, "self") ||
 			zend_string_equals_literal_ci(type_name, "parent")) {
-		return 0;
+		return;
 	}

 	/* We use type.name.gc.refcount to keep map_ptr of corresponding type */
 	if (ZSTR_HAS_CE_CACHE(type_name)) {
-		return GC_REFCOUNT(type_name);
+		return;
 	}

 	if ((GC_FLAGS(type_name) & GC_IMMUTABLE)
 	 && (GC_FLAGS(type_name) & IS_STR_PERMANENT)) {
+		ZEND_ASSERT(ZCG(pending_ce_cache) && "Must be called inside a persist window");
+		slot = zend_hash_index_lookup(ZCG(pending_ce_cache), (zend_ulong)(uintptr_t)type_name);
+		if (Z_TYPE_P(slot) != IS_NULL) {
+			/* Already allocated earlier in this window */
+			return;
+		}
 		do {
 			ret = ZEND_MAP_PTR_NEW_OFFSET();
 		} while (ret <= 2);
-		GC_SET_REFCOUNT(type_name, ret);
-		GC_ADD_FLAGS(type_name, IS_STR_CLASS_NAME_MAP_PTR);
-		return ret;
+		ZVAL_LONG(slot, ret);
 	}
-
-	return 0;
 }

 static void zend_persist_type(zend_type *type) {
@@ -1343,6 +1390,9 @@ zend_persistent_script *zend_accel_script_persist(zend_persistent_script *script
 #endif

 	zend_map_ptr_extend(ZCSG(map_ptr_last));
+	if (for_shm) {
+		zend_accel_pending_ce_cache_begin();
+	}

 	zend_accel_persist_class_table(&script->script.class_table);
 	zend_hash_persist(&script->script.function_table);
@@ -1369,6 +1419,7 @@ zend_persistent_script *zend_accel_script_persist(zend_persistent_script *script

 	if (for_shm) {
 		ZCSG(map_ptr_last) = CG(map_ptr_last);
+		zend_accel_pending_ce_cache_commit();
 	}

 #ifdef HAVE_JIT
diff --git a/ext/pcre/php_pcre.c b/ext/pcre/php_pcre.c
index ce7eb1259fa..93d8269f58b 100644
--- a/ext/pcre/php_pcre.c
+++ b/ext/pcre/php_pcre.c
@@ -194,7 +194,7 @@ static void php_pcre_free(void *block, void *data)

 static void *php_pcre_emalloc(PCRE2_SIZE size, void *data)
 {
-	return emalloc(size);
+	return femalloc(size, /* fallible */ true);
 }

 static void php_pcre_efree(void *block, void *data)
@@ -1043,7 +1043,7 @@ static inline void add_offset_pair(
 }
 /* }}} */

-static void populate_subpat_array(
+static zend_result populate_subpat_array(
 		zval *subpats, const char *subject, PCRE2_SIZE *offsets, zend_string **subpat_names,
 		uint32_t num_subpats, int count, const PCRE2_SPTR mark, zend_long flags) {
 	bool offset_capture = (flags & PREG_OFFSET_CAPTURE) != 0;
@@ -1056,10 +1056,16 @@ static void populate_subpat_array(
 				add_offset_pair(
 					subpats, subject, offsets[2*i], offsets[2*i+1],
 					subpat_names[i], unmatched_as_null);
+				if (ZEND_HAS_OOM()) {
+					return FAILURE;
+				}
 			}
 			if (unmatched_as_null) {
 				for (i = count; i < num_subpats; i++) {
 					add_offset_pair(subpats, NULL, PCRE2_UNSET, PCRE2_UNSET, subpat_names[i], 1);
+					if (ZEND_HAS_OOM()) {
+						return FAILURE;
+					}
 				}
 			}
 		} else {
@@ -1070,6 +1076,9 @@ static void populate_subpat_array(
 					add_named(subpats, subpat_names[i], &val, offsets[2*i] == PCRE2_UNSET);
 				}
 				zend_hash_next_index_insert(Z_ARRVAL_P(subpats), &val);
+				if (ZEND_HAS_OOM()) {
+					return FAILURE;
+				}
 			}
 			if (unmatched_as_null) {
 				for (i = count; i < num_subpats; i++) {
@@ -1078,6 +1087,9 @@ static void populate_subpat_array(
 						zend_hash_add(Z_ARRVAL_P(subpats), subpat_names[i], &val);
 					}
 					zend_hash_next_index_insert(Z_ARRVAL_P(subpats), &val);
+					if (ZEND_HAS_OOM()) {
+						return FAILURE;
+					}
 				}
 			}
 		}
@@ -1086,10 +1098,16 @@ static void populate_subpat_array(
 			for (i = 0; i < count; i++) {
 				add_offset_pair(
 					subpats, subject, offsets[2*i], offsets[2*i+1], NULL, unmatched_as_null);
+				if (ZEND_HAS_OOM()) {
+					return FAILURE;
+				}
 			}
 			if (unmatched_as_null) {
 				for (i = count; i < num_subpats; i++) {
 					add_offset_pair(subpats, NULL, PCRE2_UNSET, PCRE2_UNSET, NULL, 1);
+					if (ZEND_HAS_OOM()) {
+						return FAILURE;
+					}
 				}
 			}
 		} else {
@@ -1097,10 +1115,16 @@ static void populate_subpat_array(
 				populate_match_value(
 					&val, subject, offsets[2*i], offsets[2*i+1], unmatched_as_null);
 				zend_hash_next_index_insert(Z_ARRVAL_P(subpats), &val);
+				if (ZEND_HAS_OOM()) {
+					return FAILURE;
+				}
 			}
 			if (unmatched_as_null) {
 				for (i = count; i < num_subpats; i++) {
 					add_next_index_null(subpats);
+					if (ZEND_HAS_OOM()) {
+						return FAILURE;
+					}
 				}
 			}
 		}
@@ -1109,6 +1133,7 @@ static void populate_subpat_array(
 	if (mark) {
 		add_assoc_string_ex(subpats, "MARK", sizeof("MARK") - 1, (char *)mark);
 	}
+	return ZEND_HAS_OOM() ? FAILURE : SUCCESS;
 }

 static void php_do_pcre_match(INTERNAL_FUNCTION_PARAMETERS, int global) /* {{{ */
@@ -1296,6 +1321,10 @@ PHPAPI void php_pcre_match_impl(pcre_cache_entry *pce, zend_string *subject_str,
 			options, match_data, mctx);

 	while (1) {
+		if (ZEND_HAS_OOM()) {
+			goto oom;
+		}
+
 		/* If something has matched */
 		if (count >= 0) {
 			/* Check for too many substrings condition. */
@@ -1329,6 +1358,9 @@ PHPAPI void php_pcre_match_impl(pcre_cache_entry *pce, zend_string *subject_str,
 								add_offset_pair(
 									&match_sets[i], subject, offsets[2*i], offsets[2*i+1],
 									NULL, unmatched_as_null);
+								if (ZEND_HAS_OOM()) {
+									goto oom;
+								}
 							}
 						} else {
 							for (i = 0; i < count; i++) {
@@ -1336,6 +1368,9 @@ PHPAPI void php_pcre_match_impl(pcre_cache_entry *pce, zend_string *subject_str,
 								populate_match_value(
 									&val, subject, offsets[2*i], offsets[2*i+1], unmatched_as_null);
 								zend_hash_next_index_insert_new(Z_ARRVAL(match_sets[i]), &val);
+								if (ZEND_HAS_OOM()) {
+									goto oom;
+								}
 							}
 						}
 						mark = pcre2_get_mark(match_data);
@@ -1362,23 +1397,33 @@ PHPAPI void php_pcre_match_impl(pcre_cache_entry *pce, zend_string *subject_str,
 								} else {
 									add_next_index_str(&match_sets[i], ZSTR_EMPTY_ALLOC());
 								}
+								if (ZEND_HAS_OOM()) {
+									goto oom;
+								}
 							}
 						}
 					} else {
 						/* Allocate and populate the result set array */
 						array_init_size(&result_set, count + (mark ? 1 : 0));
 						mark = pcre2_get_mark(match_data);
-						populate_subpat_array(
+						zend_result result = populate_subpat_array(
 							&result_set, subject, offsets, subpat_names,
 							num_subpats, count, mark, flags);
+						if (UNEXPECTED(result == FAILURE)) {
+							zval_ptr_dtor(&result_set);
+							goto oom;
+						}
 						/* And add it to the output array */
 						zend_hash_next_index_insert(Z_ARRVAL_P(subpats), &result_set);
 					}
 				} else {			/* single pattern matching */
 					/* For each subpattern, insert it into the subpatterns array. */
 					mark = pcre2_get_mark(match_data);
-					populate_subpat_array(
+					zend_result result = populate_subpat_array(
 						subpats, subject, offsets, subpat_names, num_subpats, count, mark, flags);
+					if (UNEXPECTED(result == FAILURE)) {
+						goto oom;
+					}
 					break;
 				}
 			}
@@ -1482,6 +1527,28 @@ PHPAPI void php_pcre_match_impl(pcre_cache_entry *pce, zend_string *subject_str,
 	} else {
 		RETVAL_FALSE;
 	}
+	return;
+
+oom:
+	if (match_data != mdata) {
+		pcre2_match_data_free(match_data);
+	}
+	if (match_sets) {
+		for (i = 0; i < num_subpats; i++) {
+			zval_ptr_dtor(&match_sets[i]);
+		}
+		efree(match_sets);
+	}
+	if (Z_TYPE(marks) != IS_UNDEF) {
+		zval_ptr_dtor(&marks);
+	}
+	if (subpat_names) {
+		free_subpats_table(subpat_names, num_subpats);
+	}
+	if (subpats) {
+		zend_hash_clean(Z_ARRVAL_P(subpats));
+	}
+	RETURN_OOM();
 }
 /* }}} */

@@ -1545,7 +1612,11 @@ static zend_string *preg_do_repl_func(zend_fcall_info *fci, zend_fcall_info_cach
 	zval	     arg;				/* Argument to pass to function */

 	array_init_size(&arg, count + (mark ? 1 : 0));
-	populate_subpat_array(&arg, subject, offsets, subpat_names, num_subpats, count, mark, flags);
+	zend_result result = populate_subpat_array(&arg, subject, offsets, subpat_names, num_subpats, count, mark, flags);
+	if (UNEXPECTED(result == FAILURE)) {
+		zval_ptr_dtor(&arg);
+		return NULL;
+	}

 	fci->retval = &retval;
 	fci->param_count = 1;
@@ -1716,9 +1787,18 @@ PHPAPI zend_string *php_pcre_replace_impl(pcre_cache_entry *pce, zend_string *su
 			if (new_len >= alloc_len) {
 				alloc_len = zend_safe_address_guarded(2, new_len, ZSTR_MAX_OVERHEAD) - ZSTR_MAX_OVERHEAD;
 				if (result == NULL) {
-					result = zend_string_alloc(alloc_len, 0);
+					result = zend_string_falloc(alloc_len, /* persistent */ false, /* fallible */ true);
+					if (!result) {
+						goto error;
+					}
 				} else {
-					result = zend_string_extend(result, alloc_len, 0);
+					zend_string *extended_result = zend_string_fextend(result, alloc_len, /* persistent */ false, /* fallible */ true);
+					if (!extended_result) {
+						zend_string_release(result);
+						result = NULL;
+						goto error;
+					}
+					result = extended_result;
 				}
 			}

@@ -1951,14 +2031,27 @@ static zend_string *php_pcre_replace_func_impl(pcre_cache_entry *pce, zend_strin
 				fci, fcc, subject, offsets, subpat_names, num_subpats, count,
 				pcre2_get_mark(match_data), flags);

-			ZEND_ASSERT(eval_result);
+			if (UNEXPECTED(!eval_result)) {
+				goto error;
+			}
 			new_len = zend_safe_address_guarded(1, ZSTR_LEN(eval_result) + ZSTR_MAX_OVERHEAD, new_len) -ZSTR_MAX_OVERHEAD;
 			if (new_len >= alloc_len) {
 				alloc_len = zend_safe_address_guarded(2, new_len, ZSTR_MAX_OVERHEAD) - ZSTR_MAX_OVERHEAD;
 				if (result == NULL) {
-					result = zend_string_alloc(alloc_len, 0);
+					result = zend_string_falloc(alloc_len, /* persistent */ false, /* fallible */ true);
+					if (!result) {
+						zend_string_release_ex(eval_result, 0);
+						goto error;
+					}
 				} else {
-					result = zend_string_extend(result, alloc_len, 0);
+					zend_string *extended_result = zend_string_fextend(result, alloc_len, /* persistent */ false, /* fallible */ true);
+					if (!extended_result) {
+						zend_string_release_ex(eval_result, 0);
+						zend_string_release(result);
+						result = NULL;
+						goto error;
+					}
+					result = extended_result;
 				}
 			}

@@ -2586,6 +2679,11 @@ PHPAPI void php_pcre_split_impl(pcre_cache_entry *pce, zend_string *subject_str,
 			options, match_data, mctx);

 	while (1) {
+		if (ZEND_HAS_OOM()) {
+			PCRE_G(error_code) = PHP_PCRE_INTERNAL_ERROR;
+			break;
+		}
+
 		/* If something matched */
 		if (count >= 0) {
 			/* Check for too many substrings condition. */
diff --git a/ext/pcre/tests/oom_preg_filter.phpt b/ext/pcre/tests/oom_preg_filter.phpt
new file mode 100644
index 00000000000..c404d363748
--- /dev/null
+++ b/ext/pcre/tests/oom_preg_filter.phpt
@@ -0,0 +1,11 @@
+--TEST--
+preg_filter() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$subject = str_repeat("a", 2 * 1024 * 1024);
+preg_filter('/a/', 'bbbbbbbbbb', $subject);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/pcre/tests/oom_preg_match_all.phpt b/ext/pcre/tests/oom_preg_match_all.phpt
new file mode 100644
index 00000000000..9c4efdee518
--- /dev/null
+++ b/ext/pcre/tests/oom_preg_match_all.phpt
@@ -0,0 +1,11 @@
+--TEST--
+preg_match_all() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$subject = str_repeat("a", 2 * 1024 * 1024);
+preg_match_all('/a/', $subject, $matches);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/pcre/tests/oom_preg_match_captures.phpt b/ext/pcre/tests/oom_preg_match_captures.phpt
new file mode 100644
index 00000000000..87f02dc22a0
--- /dev/null
+++ b/ext/pcre/tests/oom_preg_match_captures.phpt
@@ -0,0 +1,12 @@
+--TEST--
+preg_match() with overlapping captures doesn't reach hard limit
+--INI--
+memory_limit=128M
+--FILE--
+<?php
+$subject = str_repeat('a', 6 * 1024 * 1024);
+$pattern = '/^' . str_repeat('(', 48) . '.*' . str_repeat(')', 48) . '$/s';
+preg_match($pattern, $subject, $matches);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 134217728 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/pcre/tests/oom_preg_replace.phpt b/ext/pcre/tests/oom_preg_replace.phpt
new file mode 100644
index 00000000000..f83dfc61840
--- /dev/null
+++ b/ext/pcre/tests/oom_preg_replace.phpt
@@ -0,0 +1,11 @@
+--TEST--
+preg_replace() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$subject = str_repeat("a", 2 * 1024 * 1024);
+preg_replace('/a/', 'bbbbbbbbbb', $subject);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/pcre/tests/oom_preg_replace_callback.phpt b/ext/pcre/tests/oom_preg_replace_callback.phpt
new file mode 100644
index 00000000000..ec3da50b085
--- /dev/null
+++ b/ext/pcre/tests/oom_preg_replace_callback.phpt
@@ -0,0 +1,11 @@
+--TEST--
+preg_replace_callback() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$subject = str_repeat("a", 5 * 1024 * 1024);
+preg_replace_callback('/^/', function () use ($subject) { return $subject; }, $subject);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/pcre/tests/oom_preg_replace_callback_array.phpt b/ext/pcre/tests/oom_preg_replace_callback_array.phpt
new file mode 100644
index 00000000000..72272d7197b
--- /dev/null
+++ b/ext/pcre/tests/oom_preg_replace_callback_array.phpt
@@ -0,0 +1,11 @@
+--TEST--
+preg_replace_callback_array() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$subject = str_repeat("a", 5 * 1024 * 1024);
+preg_replace_callback_array(['/^/' => function () use ($subject) { return $subject; }], $subject);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/pcre/tests/oom_preg_split.phpt b/ext/pcre/tests/oom_preg_split.phpt
new file mode 100644
index 00000000000..b26f4464aa1
--- /dev/null
+++ b/ext/pcre/tests/oom_preg_split.phpt
@@ -0,0 +1,11 @@
+--TEST--
+preg_split() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$subject = str_repeat("ab", 1024 * 1024);
+preg_split('/a/', $subject);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/session/php_session.h b/ext/session/php_session.h
index 31b96340e82..e2cc4dc5487 100644
--- a/ext/session/php_session.h
+++ b/ext/session/php_session.h
@@ -202,6 +202,8 @@ typedef struct _php_ps_globals {
 	bool in_save_handler; /* state if session is in save handler or not */
 	bool set_handler;     /* state if session module i setting handler or not */
 	zend_string *session_vars; /* serialized original session data */
+
+	bool in_rfc1867_update;    /* php_session_rfc1867_update() is running */
 } php_ps_globals;

 typedef php_ps_globals zend_ps_globals;
diff --git a/ext/session/session.c b/ext/session/session.c
index dd780f4afd4..c19d85b9c18 100644
--- a/ext/session/session.c
+++ b/ext/session/session.c
@@ -108,7 +108,6 @@ static int my_module_number = 0;
 static inline void php_rinit_session_globals(void) /* {{{ */
 {
 	/* Do NOT init PS(mod_user_names) here! */
-	/* TODO: These could be moved to MINIT and removed. These should be initialized by php_rshutdown_session_globals() always when execution is finished. */
 	PS(id) = NULL;
 	PS(session_status) = php_session_none;
 	PS(in_save_handler) = 0;
@@ -118,6 +117,7 @@ static inline void php_rinit_session_globals(void) /* {{{ */
 	PS(define_sid) = 1;
 	PS(session_vars) = NULL;
 	PS(module_number) = my_module_number;
+	PS(in_rfc1867_update) = false;
 	ZVAL_UNDEF(&PS(http_session_vars));
 }
 /* }}} */
@@ -845,13 +845,35 @@ PHP_INI_END()
 /* ***************
    * Serializers *
    *************** */
+
+/* The upload progress handler (php_session_rfc1867_update()) decodes and
+ * re-encodes the session during request startup, before RINIT, when no user
+ * code may run. Objects are then kept as __PHP_Incomplete_Class: no
+ * autoloading, no unserialize_callback_func, no __wakeup() or __unserialize(),
+ * and they are written back unchanged. */
+static void php_session_unserialize_init(php_unserialize_data_t *var_hash)
+{
+	PHP_VAR_UNSERIALIZE_INIT(*var_hash);
+	if (PS(in_rfc1867_update)) {
+		php_var_unserialize_set_incomplete_classes_only(*var_hash, true);
+	}
+}
+
+static void php_session_serialize_init(php_serialize_data_t *var_hash)
+{
+	PHP_VAR_SERIALIZE_INIT(*var_hash);
+	if (PS(in_rfc1867_update)) {
+		php_var_serialize_set_incomplete_classes_only(*var_hash, true);
+	}
+}
+
 PS_SERIALIZER_ENCODE_FUNC(php_serialize) /* {{{ */
 {
 	smart_str buf = {0};
 	php_serialize_data_t var_hash;

 	IF_SESSION_VARS() {
-		PHP_VAR_SERIALIZE_INIT(var_hash);
+		php_session_serialize_init(&var_hash);
 		php_var_serialize(&buf, Z_REFVAL(PS(http_session_vars)), &var_hash);
 		PHP_VAR_SERIALIZE_DESTROY(var_hash);
 	}
@@ -868,7 +890,7 @@ PS_SERIALIZER_DECODE_FUNC(php_serialize) /* {{{ */
 	zend_string *var_name = zend_string_init("_SESSION", sizeof("_SESSION") - 1, 0);

 	ZVAL_NULL(&session_vars);
-	PHP_VAR_UNSERIALIZE_INIT(var_hash);
+	php_session_unserialize_init(&var_hash);
 	result = php_var_unserialize(
 		&session_vars, (const unsigned char **)&val, (const unsigned char *)endptr, &var_hash);
 	PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
@@ -901,7 +923,7 @@ PS_SERIALIZER_ENCODE_FUNC(php_binary) /* {{{ */
 	php_serialize_data_t var_hash;
 	PS_ENCODE_VARS;

-	PHP_VAR_SERIALIZE_INIT(var_hash);
+	php_session_serialize_init(&var_hash);

 	PS_ENCODE_LOOP(
 			if (ZSTR_LEN(key) > PS_BIN_MAX) continue;
@@ -925,7 +947,7 @@ PS_SERIALIZER_DECODE_FUNC(php_binary) /* {{{ */
 	php_unserialize_data_t var_hash;
 	zval *current, rv;

-	PHP_VAR_UNSERIALIZE_INIT(var_hash);
+	php_session_unserialize_init(&var_hash);

 	for (p = val; p < endptr; ) {
 		// Can this be changed to size_t?
@@ -967,7 +989,7 @@ PS_SERIALIZER_ENCODE_FUNC(php) /* {{{ */
 	php_serialize_data_t var_hash;
 	PS_ENCODE_VARS;

-	PHP_VAR_SERIALIZE_INIT(var_hash);
+	php_session_serialize_init(&var_hash);

 	PS_ENCODE_LOOP(
 		smart_str_appendl(&buf, ZSTR_VAL(key), ZSTR_LEN(key));
@@ -997,7 +1019,7 @@ PS_SERIALIZER_DECODE_FUNC(php) /* {{{ */
 	php_unserialize_data_t var_hash;
 	zval *current, rv;

-	PHP_VAR_UNSERIALIZE_INIT(var_hash);
+	php_session_unserialize_init(&var_hash);

 	p = val;

@@ -2779,6 +2801,7 @@ static PHP_GINIT_FUNCTION(ps) /* {{{ */
 	ps_globals->mod = NULL;
 	ps_globals->serializer = NULL;
 	ps_globals->mod_data = NULL;
+	ps_globals->in_rfc1867_update = false;
 	ps_globals->session_status = php_session_none;
 	ps_globals->default_mod = NULL;
 	ps_globals->mod_user_implemented = 0;
@@ -2978,8 +3001,12 @@ static void php_session_rfc1867_update(php_session_rfc1867_progress *progress, i
 		progress->next_update = Z_LVAL_P(progress->post_bytes_processed) + progress->update_step;
 	}

-	php_session_initialize();
-	PS(session_status) = php_session_active;
+	/* Runs before RINIT: no user code may be invoked, see php_session_unserialize_init() */
+	PS(in_rfc1867_update) = true;
+	if (php_session_initialize() == FAILURE || PS(session_status) != php_session_active) {
+		PS(in_rfc1867_update) = false;
+		return;
+	}
 	IF_SESSION_VARS() {
 		zval *sess_var = Z_REFVAL(PS(http_session_vars));
 		SEPARATE_ARRAY(sess_var);
@@ -2989,18 +3016,23 @@ static void php_session_rfc1867_update(php_session_rfc1867_progress *progress, i
 		zend_hash_update(Z_ARRVAL_P(sess_var), progress->key.s, &progress->data);
 	}
 	php_session_flush(1);
+	PS(in_rfc1867_update) = false;
 } /* }}} */

 static void php_session_rfc1867_cleanup(php_session_rfc1867_progress *progress) /* {{{ */
 {
-	php_session_initialize();
-	PS(session_status) = php_session_active;
+	PS(in_rfc1867_update) = true;
+	if (php_session_initialize() == FAILURE || PS(session_status) != php_session_active) {
+		PS(in_rfc1867_update) = false;
+		return;
+	}
 	IF_SESSION_VARS() {
 		zval *sess_var = Z_REFVAL(PS(http_session_vars));
 		SEPARATE_ARRAY(sess_var);
 		zend_hash_del(Z_ARRVAL_P(sess_var), progress->key.s);
 	}
 	php_session_flush(1);
+	PS(in_rfc1867_update) = false;
 } /* }}} */

 static zend_result php_session_rfc1867_callback(unsigned int event, void *event_data, void **extra) /* {{{ */
@@ -3014,12 +3046,20 @@ static zend_result php_session_rfc1867_callback(unsigned int event, void *event_
 	if (!PS(rfc1867_enabled)) {
 		return retval;
 	}
+	if (PG(modules_activated)) {
+		/* request_parse_body(). Do not run here as it corrupts the session. */
+		return retval;
+	}

 	progress = PS(rfc1867_progress);

 	switch(event) {
 		case MULTIPART_EVENT_START: {
 			multipart_event_start *data = (multipart_event_start *) event_data;
+
+			/* This runs before RINIT, so we need to clear these early */
+			php_rinit_session_globals();
+
 			progress = ecalloc(1, sizeof(php_session_rfc1867_progress));
 			progress->content_length = data->content_length;
 			progress->sname_len  = strlen(PS(session_name));
@@ -3177,6 +3217,11 @@ static zend_result php_session_rfc1867_callback(unsigned int event, void *event_
 			efree(progress);
 			progress = NULL;
 			PS(rfc1867_progress) = NULL;
+
+			/* $_SESSION was populated in incomplete_classes_only mode */
+			zend_string *session_var_name = ZSTR_INIT_LITERAL("_SESSION", 0);
+			zend_delete_global_variable(session_var_name);
+			zend_string_release_ex(session_var_name, 0);
 		}
 		break;
 	}
diff --git a/ext/session/tests/rfc1867_incomplete_class_reserved_props.phpt b/ext/session/tests/rfc1867_incomplete_class_reserved_props.phpt
new file mode 100644
index 00000000000..954e46b7116
--- /dev/null
+++ b/ext/session/tests/rfc1867_incomplete_class_reserved_props.phpt
@@ -0,0 +1,139 @@
+--TEST--
+session rfc1867: the upload progress update rejects session data whose objects carry the incomplete class bookkeeping property names
+--EXTENSIONS--
+session
+--SKIPIF--
+<?php
+include('skipif.inc');
+if (!getenv('TEST_PHP_CGI_EXECUTABLE')) {
+    die('skip CGI not available');
+}
+?>
+--FILE--
+<?php
+
+$dir = __DIR__ . '/rfc1867_incomplete_class_reserved_props';
+@mkdir($dir);
+$sid = 'rfc1867reservedprops';
+$file = "$dir/sess_$sid";
+$worker = "$dir/worker.php";
+file_put_contents($worker, '<?php');
+
+/* The handler decodes the record with every object kept as
+ * __PHP_Incomplete_Class, which stores the class name of each object and the
+ * payload of C:/E: entries under these three property names. Data that already
+ * carries one of them cannot be told apart from that bookkeeping, so the decode
+ * has to fail rather than let the two meanings be confused. */
+$cases = [
+    'control'          => 'x|O:8:"stdClass":1:{s:1:"a";i:1;}',
+    'Class_Name'       => 'x|O:8:"stdClass":1:{s:27:"__PHP_Incomplete_Class_Name";s:3:"foo";}',
+    'Class_Serialized' => 'x|O:8:"stdClass":1:{s:33:"__PHP_Incomplete_Class_Serialized";s:3:"foo";}',
+    'Enum_Case'        => 'x|O:8:"stdClass":1:{s:26:"__PHP_Incomplete_Enum_Case";s:3:"foo";}',
+    'nested'           => 'x|a:1:{i:0;O:8:"stdClass":1:{s:27:"__PHP_Incomplete_Class_Name";s:3:"foo";}}',
+    'mangled'          => 'x|O:8:"stdClass":1:{s:30:"' . "\0" . 'A' . "\0" . '__PHP_Incomplete_Class_Name";s:3:"foo";}',
+    'corrupt'          => 'x|not-serialized-at-all',
+];
+
+$boundary = '---------------------------20896060251896012921717172737';
+$body = "--$boundary\r\n"
+      . "Content-Disposition: form-data; name=\"PHPSESSID\"\r\n\r\n$sid\r\n"
+      . "--$boundary\r\n"
+      . "Content-Disposition: form-data; name=\"PHP_SESSION_UPLOAD_PROGRESS\"\r\n\r\nkey\r\n"
+      . "--$boundary\r\n"
+      . "Content-Disposition: form-data; name=\"file1\"; filename=\"file1.txt\"\r\n\r\n1\r\n"
+      . "--$boundary--\r\n";
+
+$ini = [
+    'error_reporting' => 'E_ALL',
+    'html_errors' => '0',
+    'file_uploads' => '1',
+    'upload_max_filesize' => '1024',
+    'session.save_path' => $dir,
+    'session.name' => 'PHPSESSID',
+    'session.use_strict_mode' => '0',
+    'session.use_cookies' => '0',
+    'session.use_only_cookies' => '0',
+    'session.cache_limiter' => '',
+    'session.save_handler' => 'files',
+    'session.serialize_handler' => 'php',
+    'session.upload_progress.enabled' => '1',
+    'session.upload_progress.cleanup' => '0',
+    'session.upload_progress.prefix' => 'upload_progress_',
+    'session.upload_progress.name' => 'PHP_SESSION_UPLOAD_PROGRESS',
+    'session.upload_progress.freq' => '1%',
+    'session.upload_progress.min_freq' => '0.000000001',
+];
+
+/* php-cgi is started with -n, so a shared session module has to be named again. */
+$extension_dir = ini_get('extension_dir');
+foreach (['session.' . PHP_SHLIB_SUFFIX, 'php_session.' . PHP_SHLIB_SUFFIX] as $module) {
+    if ($extension_dir !== false && file_exists("$extension_dir/$module")) {
+        $ini['extension_dir'] = $extension_dir;
+        $ini['extension'] = $module;
+        break;
+    }
+}
+
+$cmd = [getenv('TEST_PHP_CGI_EXECUTABLE'), '-n', '-q'];
+foreach ($ini as $name => $value) {
+    $cmd[] = '-d';
+    $cmd[] = "$name=$value";
+}
+$cmd[] = '-f';
+$cmd[] = $worker;
+
+$env = getenv();
+$env['REDIRECT_STATUS'] = '1';
+$env['REQUEST_METHOD'] = 'POST';
+$env['CONTENT_TYPE'] = "multipart/form-data; boundary=$boundary";
+$env['CONTENT_LENGTH'] = (string) strlen($body);
+$env['SCRIPT_FILENAME'] = $worker;
+$env['PATH_TRANSLATED'] = $worker;
+$env['QUERY_STRING'] = '';
+
+foreach ($cases as $label => $record) {
+    file_put_contents($file, $record);
+
+    $pipes = [];
+    $proc = proc_open(
+        $cmd,
+        [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
+        $pipes,
+        $dir,
+        $env
+    );
+    fwrite($pipes[0], $body);
+    fclose($pipes[0]);
+    $out = stream_get_contents($pipes[1]) . stream_get_contents($pipes[2]);
+    fclose($pipes[1]);
+    fclose($pipes[2]);
+    proc_close($proc);
+
+    /* A refused decode must leave exactly one warning: the handler stops there
+     * instead of writing the progress into the empty session it was left with,
+     * which would add a second warning about the save path. */
+    $kept = file_exists($file) && str_starts_with(file_get_contents($file), $record);
+    printf("%-16s warnings: %d, decode refused: %-5s record kept: %s\n", $label,
+        substr_count($out, 'Warning:'),
+        var_export(str_contains($out, 'Session has been destroyed'), true),
+        var_export($kept, true));
+}
+
+?>
+--CLEAN--
+<?php
+$dir = __DIR__ . '/rfc1867_incomplete_class_reserved_props';
+@unlink("$dir/worker.php");
+foreach (glob("$dir/sess_*") as $f) {
+    unlink($f);
+}
+@rmdir($dir);
+?>
+--EXPECT--
+control          warnings: 0, decode refused: false record kept: true
+Class_Name       warnings: 1, decode refused: true  record kept: false
+Class_Serialized warnings: 1, decode refused: true  record kept: false
+Enum_Case        warnings: 1, decode refused: true  record kept: false
+nested           warnings: 1, decode refused: true  record kept: false
+mangled          warnings: 0, decode refused: false record kept: true
+corrupt          warnings: 1, decode refused: true  record kept: false
diff --git a/ext/session/tests/rfc1867_no_user_code.phpt b/ext/session/tests/rfc1867_no_user_code.phpt
new file mode 100644
index 00000000000..5fce976d982
--- /dev/null
+++ b/ext/session/tests/rfc1867_no_user_code.phpt
@@ -0,0 +1,217 @@
+--TEST--
+session rfc1867: the upload progress update resolves no classes, keeps the session data intact and leaves no $_SESSION behind
+--EXTENSIONS--
+session
+--SKIPIF--
+<?php
+include('skipif.inc');
+if (!getenv('TEST_PHP_CGI_EXECUTABLE')) {
+    die('skip CGI not available');
+}
+?>
+--FILE--
+<?php
+
+$dir = __DIR__ . '/rfc1867_no_user_code';
+@mkdir($dir);
+$sid = 'rfc1867nousercode';
+$file = "$dir/sess_$sid";
+$worker = "$dir/worker.php";
+
+/* A hand-written record, as session_encode() produces it for the `php` handler:
+ * an object with __wakeup(), one with __unserialize(), a Serializable payload
+ * (C:), an enum shared through a back-reference (E: and r:), and a class that
+ * does not exist. Writing it directly leaves the session module untouched until
+ * the upload progress handler runs, as in a real request. */
+$record = 'w|O:1:"W":1:{s:1:"a";i:1;}'
+        . 'u|O:1:"U":1:{s:1:"a";i:2;}'
+        . 'c|C:4:"Cust":7:{payload}'
+        . 'e|a:2:{i:0;E:11:"Suit:Hearts";i:1;r:7;}'
+        . 'm|O:7:"Missing":1:{s:1:"x";i:3;}';
+file_put_contents($file, $record);
+
+/* None of these classes exist while the handler decodes the record: the request
+ * script is not even compiled yet. With the real classes, E:11:"Suit:Hearts"
+ * fails the decode outright, which destroys the session, and C:4:"Cust" loses
+ * its payload. The handler keeps every object as __PHP_Incomplete_Class instead
+ * and writes it back unchanged. */
+file_put_contents($worker, <<<'PHP'
+<?php
+$record = getenv('TEST_SESSION_RECORD');
+$file = getenv('TEST_SESSION_FILE');
+
+spl_autoload_register(function ($class) { echo "autoload($class)\n"; });
+
+enum Suit: string {
+    case Hearts = 'H';
+}
+
+class W {
+    public $a = 1;
+    function __wakeup() {
+        echo "W::__wakeup\n";
+    }
+    function __sleep() {
+        echo "W::__sleep\n"; return ['a'];
+    }
+}
+
+class U {
+    public $a = 2;
+    function __unserialize(array $d) {
+        echo "U::__unserialize\n";
+        $this->a = $d['a'];
+    }
+    function __serialize(): array {
+        echo "U::__serialize\n";
+        return ['a' => $this->a];
+    }
+}
+
+class Cust implements Serializable {
+    public $x = 'payload';
+    function serialize() {
+        return $this->x;
+    }
+    function unserialize($d) {
+        $this->x = $d;
+    }
+}
+
+/* The handler needs a $_SESSION to decode the record into, and registers it in
+ * the global symbol table. This script has not called session_start(), so it
+ * must not be left behind: it would otherwise expose the session data of
+ * whatever id the request body carried. */
+var_dump(array_key_exists('_SESSION', $GLOBALS));
+var_dump(session_status() === PHP_SESSION_NONE);
+
+echo "--- record after the update:\n";
+$after = file_get_contents($file);
+var_dump(substr($after, 0, strlen($record)) === $record);
+var_dump(substr($after, strlen($record), strlen('upload_progress_key|')));
+
+echo "--- session_start():\n";
+session_start();
+var_dump(session_id());
+var_dump($_SESSION['w']->a, $_SESSION['u']->a, $_SESSION['c']->x);
+var_dump($_SESSION['e'][0] === Suit::Hearts, $_SESSION['e'][1] === Suit::Hearts);
+var_dump($_SESSION['m']);
+var_dump(isset($_SESSION['upload_progress_key']['bytes_processed']));
+session_destroy();
+PHP);
+
+$boundary = '---------------------------20896060251896012921717172737';
+$body = "--$boundary\r\n"
+      . "Content-Disposition: form-data; name=\"PHPSESSID\"\r\n\r\n$sid\r\n"
+      . "--$boundary\r\n"
+      . "Content-Disposition: form-data; name=\"PHP_SESSION_UPLOAD_PROGRESS\"\r\n\r\nkey\r\n"
+      . "--$boundary\r\n"
+      . "Content-Disposition: form-data; name=\"file1\"; filename=\"file1.txt\"\r\n\r\n1\r\n"
+      . "--$boundary--\r\n";
+
+$ini = [
+    'error_reporting' => 'E_ALL&~E_DEPRECATED',
+    'file_uploads' => '1',
+    'upload_max_filesize' => '1024',
+    'session.save_path' => $dir,
+    'session.name' => 'PHPSESSID',
+    'session.use_strict_mode' => '0',
+    'session.use_cookies' => '0',
+    'session.use_only_cookies' => '0',
+    'session.cache_limiter' => '',
+    'session.save_handler' => 'files',
+    'session.serialize_handler' => 'php',
+    'session.upload_progress.enabled' => '1',
+    'session.upload_progress.cleanup' => '0',
+    'session.upload_progress.prefix' => 'upload_progress_',
+    'session.upload_progress.name' => 'PHP_SESSION_UPLOAD_PROGRESS',
+    'session.upload_progress.freq' => '1%',
+    'session.upload_progress.min_freq' => '0.000000001',
+];
+
+/* php-cgi is started with -n, so a shared session module has to be named again. */
+$extension_dir = ini_get('extension_dir');
+foreach (['session.' . PHP_SHLIB_SUFFIX, 'php_session.' . PHP_SHLIB_SUFFIX] as $module) {
+    if ($extension_dir !== false && file_exists("$extension_dir/$module")) {
+        $ini['extension_dir'] = $extension_dir;
+        $ini['extension'] = $module;
+        break;
+    }
+}
+
+$cmd = [getenv('TEST_PHP_CGI_EXECUTABLE'), '-n', '-q'];
+foreach ($ini as $name => $value) {
+    $cmd[] = '-d';
+    $cmd[] = "$name=$value";
+}
+$cmd[] = '-f';
+$cmd[] = $worker;
+
+$env = getenv();
+$env['REDIRECT_STATUS'] = '1';
+$env['REQUEST_METHOD'] = 'POST';
+$env['CONTENT_TYPE'] = "multipart/form-data; boundary=$boundary";
+$env['CONTENT_LENGTH'] = (string) strlen($body);
+$env['SCRIPT_FILENAME'] = $worker;
+$env['PATH_TRANSLATED'] = $worker;
+$env['QUERY_STRING'] = '';
+$env['TEST_SESSION_RECORD'] = $record;
+$env['TEST_SESSION_FILE'] = $file;
+
+$pipes = [];
+$proc = proc_open(
+    $cmd,
+    [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
+    $pipes,
+    $dir,
+    $env
+);
+fwrite($pipes[0], $body);
+fclose($pipes[0]);
+$out = stream_get_contents($pipes[1]);
+$err = stream_get_contents($pipes[2]);
+fclose($pipes[1]);
+fclose($pipes[2]);
+proc_close($proc);
+
+echo $out;
+if ($err !== '') {
+    echo "--- stderr:\n$err";
+}
+
+?>
+--CLEAN--
+<?php
+$dir = __DIR__ . '/rfc1867_no_user_code';
+@unlink("$dir/worker.php");
+foreach (glob("$dir/sess_*") as $f) {
+    unlink($f);
+}
+@rmdir($dir);
+?>
+--EXPECTF--
+X-Powered-By: PHP/%s
+Content-type: text/html; charset=UTF-8
+
+bool(false)
+bool(true)
+--- record after the update:
+bool(true)
+string(20) "upload_progress_key|"
+--- session_start():
+autoload(Missing)
+W::__wakeup
+U::__unserialize
+string(17) "rfc1867nousercode"
+int(1)
+int(2)
+string(7) "payload"
+bool(true)
+bool(true)
+object(__PHP_Incomplete_Class)#%d (2) {
+  ["__PHP_Incomplete_Class_Name"]=>
+  string(7) "Missing"
+  ["x"]=>
+  int(3)
+}
+bool(true)
diff --git a/ext/session/tests/rfc1867_sid_invalid.phpt b/ext/session/tests/rfc1867_sid_invalid.phpt
index fe01b5a8ba2..dd77a2ed9ac 100644
--- a/ext/session/tests/rfc1867_sid_invalid.phpt
+++ b/ext/session/tests/rfc1867_sid_invalid.phpt
@@ -44,7 +44,7 @@
 var_dump(session_id());
 var_dump(basename(__FILE__) == $_POST[ini_get("session.upload_progress.name")]);
 var_dump($_FILES);
-var_dump($_SESSION["upload_progress_" . basename(__FILE__)]);
+var_dump($_SESSION);
 session_destroy();
 ?>
 --CLEAN--
@@ -56,13 +56,9 @@

 Warning: PHP Request Startup: Failed to read session data: files (path: ) in Unknown on line 0

-Warning: PHP Request Startup: Failed to write session data (files). Please verify that the current setting of session.save_path is correct () in Unknown on line 0
-
 Warning: PHP Request Startup: Session ID is too long or contains illegal characters. Only the A-Z, a-z, 0-9, "-", and "," characters are allowed in Unknown on line 0

 Warning: PHP Request Startup: Failed to read session data: files (path: ) in Unknown on line 0
-
-Warning: PHP Request Startup: Failed to write session data (files). Please verify that the current setting of session.save_path is correct () in Unknown on line 0
 string(%d) ""
 bool(true)
 array(2) {
@@ -97,50 +93,4 @@
     int(1)
   }
 }
-array(5) {
-  ["start_time"]=>
-  int(%d)
-  ["content_length"]=>
-  int(469)
-  ["bytes_processed"]=>
-  int(469)
-  ["done"]=>
-  bool(true)
-  ["files"]=>
-  array(2) {
-    [0]=>
-    array(7) {
-      ["field_name"]=>
-      string(5) "file1"
-      ["name"]=>
-      string(9) "file1.txt"
-      ["tmp_name"]=>
-      string(%d) "%s"
-      ["error"]=>
-      int(0)
-      ["done"]=>
-      bool(true)
-      ["start_time"]=>
-      int(%d)
-      ["bytes_processed"]=>
-      int(1)
-    }
-    [1]=>
-    array(7) {
-      ["field_name"]=>
-      string(5) "file2"
-      ["name"]=>
-      string(9) "file2.txt"
-      ["tmp_name"]=>
-      string(%d) "%s"
-      ["error"]=>
-      int(0)
-      ["done"]=>
-      bool(true)
-      ["start_time"]=>
-      int(%d)
-      ["bytes_processed"]=>
-      int(1)
-    }
-  }
-}
+NULL
diff --git a/ext/shmop/shmop.c b/ext/shmop/shmop.c
index ce810a78239..e7306e2b557 100644
--- a/ext/shmop/shmop.c
+++ b/ext/shmop/shmop.c
@@ -243,7 +243,10 @@ PHP_FUNCTION(shmop_read)
 	startaddr = shmop->addr + start;
 	bytes = count ? count : shmop->size - start;

-	return_string = zend_string_init(startaddr, bytes, 0);
+	return_string = zend_string_finit(startaddr, bytes, /* persistent */ false, /* fallible */ true);
+	if (UNEXPECTED(!return_string)) {
+		RETURN_OOM();
+	}

 	RETURN_NEW_STR(return_string);
 }
diff --git a/ext/simplexml/simplexml.c b/ext/simplexml/simplexml.c
index 7e1dcf9eb2a..6ad25143974 100644
--- a/ext/simplexml/simplexml.c
+++ b/ext/simplexml/simplexml.c
@@ -916,7 +916,7 @@ static inline zend_string *sxe_xmlNodeListGetString(xmlDocPtr doc, xmlNodePtr li
 	zend_string *res;

 	if (tmp) {
-		res = zend_string_init((char*)tmp, strlen((char *)tmp), 0);
+		res = zend_string_finit((char*)tmp, strlen((char *)tmp), /* persistent */ false, /* fallible */ true);
 		xmlFree(tmp);
 	} else {
 		res = ZSTR_EMPTY_ALLOC();
@@ -927,17 +927,16 @@ static inline zend_string *sxe_xmlNodeListGetString(xmlDocPtr doc, xmlNodePtr li
 /* }}} */

 /* {{{ _get_base_node_value() */
-static void _get_base_node_value(php_sxe_object *sxe_ref, xmlNodePtr node, zval *value, xmlChar *nsprefix, int isprefix)
+static bool _get_base_node_value(php_sxe_object *sxe_ref, xmlNodePtr node, zval *value, xmlChar *nsprefix, int isprefix)
 {
 	php_sxe_object *subnode;
-	xmlChar        *contents;

 	if (node->children && node->children->type == XML_TEXT_NODE && !xmlIsBlankNode(node->children)) {
-		contents = xmlNodeListGetString(node->doc, node->children, 1);
-		if (contents) {
-			ZVAL_STRING(value, (char *)contents);
-			xmlFree(contents);
+		zend_string *str = sxe_xmlNodeListGetString(node->doc, node->children, 1);
+		if (UNEXPECTED(!str)) {
+			return false;
 		}
+		ZVAL_STR(value, str);
 	} else {
 		subnode = php_sxe_object_new(sxe_ref->zo.ce, sxe_ref->fptr_count);
 		subnode->document = sxe_ref->document;
@@ -951,6 +950,7 @@ static void _get_base_node_value(php_sxe_object *sxe_ref, xmlNodePtr node, zval
 		ZVAL_OBJ(value, &subnode->zo);
 		/*zval_add_ref(value);*/
 	}
+	return true;
 }
 /* }}} */

@@ -1081,10 +1081,7 @@ static HashTable *sxe_get_prop_hash(zend_object *object, int is_debug) /* {{{ */
 	xmlAttrPtr       attr;
 	int              namelen;
 	int              test;
-	char 		 	 use_iter;
-	zval             iter_data;
-
-	use_iter = 0;
+	zval             iter_data_zv, *iter_data = NULL;

 	sxe = php_sxe_fetch_object(object);

@@ -1112,13 +1109,20 @@ static HashTable *sxe_get_prop_hash(zend_object *object, int is_debug) /* {{{ */
 			test = sxe->iter.name && sxe->iter.type == SXE_ITER_ATTRLIST;
 			while (attr) {
 				if ((!test || !xmlStrcmp(attr->name, sxe->iter.name)) && match_ns(sxe, (xmlNodePtr)attr, sxe->iter.nsprefix, sxe->iter.isprefix)) {
-					ZVAL_STR(&value, sxe_xmlNodeListGetString((xmlDocPtr) sxe->document->ptr, attr->children, 1));
+					zend_string *str = sxe_xmlNodeListGetString((xmlDocPtr) sxe->document->ptr, attr->children, 1);
+					if (UNEXPECTED(!str)) {
+						goto oom;
+					}
+					ZVAL_STR(&value, str);
 					namelen = xmlStrlen(attr->name);
 					if (Z_ISUNDEF(zattr)) {
 						array_init(&zattr);
 						sxe_properties_add(rv, "@attributes", sizeof("@attributes") - 1, &zattr);
 					}
 					add_assoc_zval_ex(&zattr, (char*)attr->name, namelen, &value);
+					if (ZEND_HAS_OOM()) {
+						goto oom;
+					}
 				}
 				attr = attr->next;
 			}
@@ -1130,7 +1134,11 @@ static HashTable *sxe_get_prop_hash(zend_object *object, int is_debug) /* {{{ */

 	if (node && sxe->iter.type != SXE_ITER_ATTRLIST) {
 		if (node->type == XML_ATTRIBUTE_NODE) {
-			ZVAL_STR(&value, sxe_xmlNodeListGetString(node->doc, node->children, 1));
+			zend_string *str = sxe_xmlNodeListGetString(node->doc, node->children, 1);
+			if (UNEXPECTED(!str)) {
+				goto oom;
+			}
+			ZVAL_STR(&value, str);
 			zend_hash_next_index_insert(rv, &value);
 			node = NULL;
 		} else if (sxe->iter.type != SXE_ITER_CHILD) {
@@ -1138,12 +1146,11 @@ static HashTable *sxe_get_prop_hash(zend_object *object, int is_debug) /* {{{ */
 			if ( sxe->iter.type == SXE_ITER_NONE || !node->children || !node->parent || !node->next || node->children->next || node->children->children || node->parent->children == node->parent->last ) {
 				node = node->children;
 			} else {
-				ZVAL_COPY_VALUE(&iter_data, &sxe->iter.data);
+				iter_data = &iter_data_zv;
+				ZVAL_COPY_VALUE(iter_data, &sxe->iter.data);
 				ZVAL_UNDEF(&sxe->iter.data);

 				node = php_sxe_reset_iterator(sxe, 0);
-
-				use_iter = 1;
 			}
 		}

@@ -1155,8 +1162,15 @@ static HashTable *sxe_get_prop_hash(zend_object *object, int is_debug) /* {{{ */
 					const xmlChar *cur = node->content;

 					if (*cur != 0) {
-						ZVAL_STR(&value, sxe_xmlNodeListGetString(node->doc, node, 1));
+						zend_string *str = sxe_xmlNodeListGetString(node->doc, node, 1);
+						if (UNEXPECTED(!str)) {
+							goto oom;
+						}
+						ZVAL_STR(&value, str);
 						zend_hash_next_index_insert(rv, &value);
+						if (ZEND_HAS_OOM()) {
+							goto oom;
+						}
 					}
 					goto next_iter;
 				}
@@ -1173,13 +1187,18 @@ static HashTable *sxe_get_prop_hash(zend_object *object, int is_debug) /* {{{ */
 				namelen = xmlStrlen(node->name);
 			}

-			_get_base_node_value(sxe, node, &value, sxe->iter.nsprefix, sxe->iter.isprefix);
+			if (UNEXPECTED(!_get_base_node_value(sxe, node, &value, sxe->iter.nsprefix, sxe->iter.isprefix))) {
+				goto oom;
+			}

-			if ( use_iter ) {
+			if (iter_data) {
 				zend_hash_next_index_insert(rv, &value);
 			} else {
 				sxe_properties_add(rv, name, namelen, &value);
 			}
+			if (ZEND_HAS_OOM()) {
+				goto oom;
+			}
 next_iter:
 			if (UNEXPECTED(node->type == XML_ENTITY_DECL)) {
 				/* Entity decls are linked together via the next pointer.
@@ -1187,7 +1206,7 @@ static HashTable *sxe_get_prop_hash(zend_object *object, int is_debug) /* {{{ */
 				 * If we then continue iterating, we'll end up in the DTD. Even worse, if the entities reference each other we'll infinite loop. */
 				break;
 			}
-			if (use_iter) {
+			if (iter_data) {
 				node = php_sxe_iterator_fetch(sxe, node->next, 0);
 			} else {
 				node = node->next;
@@ -1195,14 +1214,24 @@ static HashTable *sxe_get_prop_hash(zend_object *object, int is_debug) /* {{{ */
 		}
 	}

-	if (use_iter) {
+	if (iter_data) {
 		if (!Z_ISUNDEF(sxe->iter.data)) {
 			zval_ptr_dtor(&sxe->iter.data);
 		}
-		ZVAL_COPY_VALUE(&sxe->iter.data, &iter_data);
+		ZVAL_COPY_VALUE(&sxe->iter.data, iter_data);
 	}

 	return rv;
+
+oom:
+	zend_hash_clean(rv);
+	if (iter_data) {
+		if (!Z_ISUNDEF(sxe->iter.data)) {
+			zval_ptr_dtor(&sxe->iter.data);
+		}
+		ZVAL_COPY_VALUE(&sxe->iter.data, iter_data);
+	}
+	return rv;
 }
 /* }}} */

@@ -1319,7 +1348,10 @@ PHP_METHOD(SimpleXMLElement, xpath)
 	result = retval->nodesetval;

 	if (result != NULL) {
-		array_init(return_value);
+		array_init_size(return_value, result->nodeNr);
+		if (zend_hash_real_finit_packed(Z_ARRVAL_P(return_value), true) != SUCCESS) {
+			goto oom;
+		}

 		for (i = 0; i < result->nodeNr; ++i) {
 			nodeptr = result->nodeTab[i];
@@ -1338,6 +1370,9 @@ PHP_METHOD(SimpleXMLElement, xpath)
 				}

 				add_next_index_zval(return_value, &value);
+				if (ZEND_HAS_OOM()) {
+					goto oom;
+				}
 			}
 		}
 	} else {
@@ -1345,6 +1380,13 @@ PHP_METHOD(SimpleXMLElement, xpath)
 	}

 	xmlXPathFreeObject(retval);
+	return;
+
+oom:
+	xmlXPathFreeObject(retval);
+	zval_ptr_dtor(return_value);
+	ZVAL_NULL(return_value);
+	RETURN_OOM();
 }
 /* }}} */

@@ -1658,7 +1700,11 @@ PHP_METHOD(SimpleXMLElement, getName)
 	node = php_sxe_get_first_node(sxe, node);
 	if (node) {
 		namelen = xmlStrlen(node->name);
-		RETURN_STRINGL((char*)node->name, namelen);
+		zend_string *str = zend_string_finit_fast((const char *)node->name, namelen, /* fallible */ true);
+		if (!str) {
+			RETURN_OOM();
+		}
+		RETURN_STR(str);
 	} else {
 		RETURN_EMPTY_STRING();
 	}
@@ -1834,7 +1880,9 @@ PHP_METHOD(SimpleXMLElement, addAttribute)
 static zend_result cast_object(zval *object, int type, char *contents)
 {
 	if (contents) {
-		ZVAL_STRINGL(object, contents, strlen(contents));
+		if (UNEXPECTED(!ZVAL_FSTRINGL(object, contents, strlen(contents)))) {
+			return FAILURE;
+		}
 	} else {
 		ZVAL_NULL(object);
 	}
@@ -2069,7 +2117,7 @@ PHP_METHOD(SimpleXMLElement, key)
 	}

 	curnode = (xmlNodePtr)((php_libxml_node_ptr *)intern->node)->node;
-	RETURN_STRINGL((char*)curnode->name, xmlStrlen(curnode->name));
+	RETURN_FSTRINGL((const char *)curnode->name, xmlStrlen(curnode->name));
 }
 /* }}} */

diff --git a/ext/simplexml/tests/oom_array_cast.phpt b/ext/simplexml/tests/oom_array_cast.phpt
new file mode 100644
index 00000000000..91e4aff49ac
--- /dev/null
+++ b/ext/simplexml/tests/oom_array_cast.phpt
@@ -0,0 +1,20 @@
+--TEST--
+Array cast of large tree doesn't reach hard memory limit
+--EXTENSIONS--
+simplexml
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+$path = __DIR__ . '/oom_array_cast.xml';
+file_put_contents($path, '<r>' . str_repeat('<a/>', 750000) . '</r>');
+
+$xml = simplexml_load_file($path);
+$result = (array) $xml;
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/oom_array_cast.xml');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 67108864 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/simplexml/tests/oom_json_encode.phpt b/ext/simplexml/tests/oom_json_encode.phpt
new file mode 100644
index 00000000000..a304da6106e
--- /dev/null
+++ b/ext/simplexml/tests/oom_json_encode.phpt
@@ -0,0 +1,20 @@
+--TEST--
+json_encode() of large tree doesn't reach hard memory limit
+--EXTENSIONS--
+simplexml
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+$path = __DIR__ . '/oom_json_encode.xml';
+file_put_contents($path, '<r>' . str_repeat('<a/>', 750000) . '</r>');
+
+$xml = simplexml_load_file($path);
+$result = json_encode($xml);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/oom_json_encode.xml');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 67108864 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/simplexml/tests/oom_xpath.phpt b/ext/simplexml/tests/oom_xpath.phpt
new file mode 100644
index 00000000000..2e4bf52fcce
--- /dev/null
+++ b/ext/simplexml/tests/oom_xpath.phpt
@@ -0,0 +1,20 @@
+--TEST--
+SimpleXMLElement::xpath() with large result set doesn't reach hard memory limit
+--EXTENSIONS--
+simplexml
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+$path = __DIR__ . '/oom_xpath.xml';
+file_put_contents($path, '<r>' . str_repeat('<a/>', 750000) . '</r>');
+
+$xml = simplexml_load_file($path);
+$result = $xml->xpath("//a");
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/oom_xpath.xml');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 67108864 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/soap/php_encoding.c b/ext/soap/php_encoding.c
index 553ab7cc3ae..916d992f55d 100644
--- a/ext/soap/php_encoding.c
+++ b/ext/soap/php_encoding.c
@@ -2631,6 +2631,11 @@ static zval *to_zval_array(zval *ret, encodeTypePtr type, xmlNodePtr data)
 					ar = zend_hash_index_update(Z_ARRVAL_P(ar), pos[i], &tmpAr);
 				}
 				i++;
+				if (ZEND_HAS_OOM()) {
+					zval_ptr_dtor(ret);
+					ZVAL_NULL(ret);
+					goto ret;
+				}
 			}
 			zend_hash_index_update(Z_ARRVAL_P(ar), pos[i], &tmpVal);

@@ -2652,6 +2657,8 @@ static zval *to_zval_array(zval *ret, encodeTypePtr type, xmlNodePtr data)
 		}
 		trav = trav->next;
 	}
+
+ret:;
 	efree(dims);
 	efree(pos);
 	return ret;
diff --git a/ext/soap/php_http.c b/ext/soap/php_http.c
index ad2a3a8b06f..91b882e6b49 100644
--- a/ext/soap/php_http.c
+++ b/ext/soap/php_http.c
@@ -1481,9 +1481,17 @@ static zend_string* get_http_body(php_stream *stream, int close, char *headers)
 					}

 					if (http_buf) {
-						http_buf = zend_string_safe_realloc(http_buf, 1, http_buf_size, buf_size, false);
+						zend_string *new_http_buf = zend_string_safe_frealloc(http_buf, 1, http_buf_size, buf_size, /* persistent */ false, /* fallible */ true);
+						if (UNEXPECTED(!new_http_buf)) {
+							zend_string_release_ex(http_buf, false);
+							return NULL;
+						}
+						http_buf = new_http_buf;
 					} else {
-						http_buf = zend_string_alloc(buf_size, 0);
+						http_buf = zend_string_falloc(buf_size, /* persistent */ false, /* fallible */ true);
+						if (UNEXPECTED(!http_buf)) {
+							return NULL;
+						}
 					}

 					while (len_size < buf_size) {
@@ -1543,7 +1551,10 @@ static zend_string* get_http_body(php_stream *stream, int close, char *headers)
 		if (header_length < 0 || header_length >= ZSTR_MAX_LEN) {
 			return NULL;
 		}
-		http_buf = zend_string_alloc(header_length, 0);
+		http_buf = zend_string_falloc(header_length, /* persistent */ false, /* fallible */ true);
+		if (UNEXPECTED(!http_buf)) {
+			return NULL;
+		}
 		while (http_buf_size < header_length) {
 			ssize_t len_read = php_stream_read(stream, http_buf->val + http_buf_size, header_length - http_buf_size);
 			if (len_read <= 0) {
@@ -1559,7 +1570,12 @@ static zend_string* get_http_body(php_stream *stream, int close, char *headers)
 					zend_string_efree(http_buf);
 					return NULL;
 				}
-				http_buf = zend_string_realloc(http_buf, http_buf_size + 4096, false);
+				zend_string *new_http_buf = zend_string_safe_frealloc(http_buf, 1, http_buf_size, 4096, /* persistent */ false, /* fallible */ true);
+				if (UNEXPECTED(!new_http_buf)) {
+					zend_string_release_ex(http_buf, false);
+					return NULL;
+				}
+				http_buf = new_http_buf;
 			} else {
 				http_buf = zend_string_alloc(4096, 0);
 			}
@@ -1592,6 +1608,10 @@ static zend_string *get_http_headers(php_stream *stream)

 		/* add header to collection */
 		smart_str_appends(&tmp_response, headerbuf);
+
+		if (ZEND_HAS_OOM()) {
+			break;
+		}
 	}

 	smart_str_free(&tmp_response);
diff --git a/ext/soap/soap.c b/ext/soap/soap.c
index 8850895ed59..f78ec5f55d4 100644
--- a/ext/soap/soap.c
+++ b/ext/soap/soap.c
@@ -1722,7 +1722,10 @@ static void soap_server_fault_ex(sdlFunctionPtr function, zval* fault, soapHeade

 	xmlFreeDoc(doc_return);
 	xmlFree(buf);
-	zend_clear_exception();
+
+	if (!ZEND_HAS_OOM()) {
+		zend_clear_exception();
+	}
 }
 /* }}} */

@@ -1754,7 +1757,9 @@ static zend_never_inline ZEND_COLD void soap_real_error_handler(int error_num, z
 	    instanceof_function(Z_OBJCE(SOAP_GLOBAL(error_object)), soap_class_entry)) {
 		bool use_exceptions =
 			Z_TYPE_P(Z_CLIENT_EXCEPTIONS_P(&SOAP_GLOBAL(error_object))) != IS_FALSE;
-		if ((error_num & E_FATAL_ERRORS) && use_exceptions) {
+		if (ZEND_HAS_OOM()) {
+			old_error_handler(error_num, error_filename, error_lineno, message);
+		} else if ((error_num & E_FATAL_ERRORS) && use_exceptions) {
 			zval fault;
 			char *code = SOAP_GLOBAL(error_code);
 			if (code == NULL) {
@@ -2893,7 +2898,12 @@ static void deserialize_parameters(xmlNodePtr params, sdlFunctionPtr function, i
 		    strcmp((char *)params->name, function->functionName) == 0) {
 			num_of_params = 0;
 		} else if (num_of_params > 0) {
-			tmp_parameters = safe_emalloc(num_of_params, sizeof(zval), 0);
+			tmp_parameters = safe_femalloc(num_of_params, sizeof(zval), 0, /* fallible */ true);
+			if (!tmp_parameters) {
+				*parameters = NULL;
+				*num_params = 0;
+				return;
+			}

 			trav = params;
 			while (trav != 0 && cur_param < num_of_params) {
@@ -2911,6 +2921,15 @@ static void deserialize_parameters(xmlNodePtr params, sdlFunctionPtr function, i
 					}
 					master_to_zval(&tmp_parameters[cur_param], enc, trav);
 					cur_param++;
+					if (ZEND_HAS_OOM()) {
+						for (int i = 0; i < cur_param; i++) {
+							zval_ptr_dtor(&tmp_parameters[i]);
+						}
+						efree(tmp_parameters);
+						*parameters = NULL;
+						*num_params = 0;
+						return;
+					}
 				}
 				trav = trav->next;
 			}
@@ -3200,6 +3219,10 @@ static sdlFunctionPtr deserialize_function_call(sdlPtr sdl, xmlDocPtr request, c
 				}
 				last = h;
 			}
+			if (ZEND_HAS_OOM()) {
+				encode_finish();
+				return NULL;
+			}
 ignore_header:
 			trav = trav->next;
 		}
@@ -3989,7 +4012,7 @@ static xmlNodePtr serialize_zval(zval *val, sdlParamPtr param, char *paramName,
 	}
 	xmlParam = master_to_xml(enc, val, style, parent);
 	zval_ptr_dtor(&defval);
-	if (xmlParam != NULL) {
+	if (xmlParam != NULL) {
 		if (xmlParam->name == NULL || strcmp((char*)xmlParam->name, "BOGUS") == 0) {
 			xmlNodeSetName(xmlParam, BAD_CAST(paramName));
 		}
diff --git a/ext/soap/tests/oom_http_server.inc b/ext/soap/tests/oom_http_server.inc
new file mode 100644
index 00000000000..f9aa31e32ea
--- /dev/null
+++ b/ext/soap/tests/oom_http_server.inc
@@ -0,0 +1,69 @@
+<?php
+
+/* Minimal single-shot raw HTTP server for OOM tests */
+function oom_soap_http_server(string $response, string $repeat = '', int $times = 0): array
+{
+    $code = <<<'PHP'
+        $server = stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr);
+        if (!$server) {
+            fwrite(STDERR, "bind failed: $errstr\n");
+            exit(1);
+        }
+        echo stream_socket_get_name($server, false), "\n";
+        $conn = stream_socket_accept($server, 10);
+        if ($conn === false) {
+            exit(1);
+        }
+        $length = 0;
+        while (($line = fgets($conn)) !== false && rtrim($line, "\r\n") !== '') {
+            if (preg_match('/^Content-Length:\s*(\d+)/i', $line, $m)) {
+                $length = (int) $m[1];
+            }
+        }
+        while ($length > 0 && !feof($conn)) {
+            $chunk = fread($conn, $length);
+            if ($chunk === false || $chunk === '') {
+                break;
+            }
+            $length -= strlen($chunk);
+        }
+        fwrite($conn, $argv[1]);
+        /* Optionally keep appending $argv[2] so a test can make the server
+         * produce far more than the client's memory limit without the parent
+         * having to hold it, or pass it on the command line. */
+        for ($i = 0; $i < (int) $argv[3]; $i++) {
+            if (@fwrite($conn, $argv[2]) === false) {
+                break;
+            }
+        }
+        fflush($conn);
+        fclose($conn);
+        PHP;
+
+    $proc = proc_open(
+        [PHP_BINARY, '-n', '-d', 'memory_limit=-1', '-r', $code, $response, $repeat, (string) $times],
+        [1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
+        $pipes,
+    );
+    if (!is_resource($proc)) {
+        die('could not start the helper server');
+    }
+    $address = trim((string) fgets($pipes[1]));
+    if ($address === '') {
+        die('helper server did not report an address: ' . stream_get_contents($pipes[2]));
+    }
+
+    return ['proc' => $proc, 'uri' => 'http://' . $address];
+}
+
+function oom_soap_http_server_skipif(): void
+{
+    if (!function_exists('proc_open')) {
+        die('skip proc_open() not available');
+    }
+    $server = @stream_socket_server('tcp://127.0.0.1:0');
+    if (!$server) {
+        die('skip stream_socket_server() failed');
+    }
+    fclose($server);
+}
diff --git a/ext/soap/tests/oom_soap_response.inc b/ext/soap/tests/oom_soap_response.inc
new file mode 100644
index 00000000000..ab7bea9b8ca
--- /dev/null
+++ b/ext/soap/tests/oom_soap_response.inc
@@ -0,0 +1,33 @@
+<?php
+
+/* Builds a compact SOAP response for OOM tests */
+function oom_soap_response(int $dimensions, int $items): string
+{
+    $arrayType = 'xsd:string[' . implode(',', array_fill(0, $dimensions, 1)) . ']';
+    $items = str_repeat('<i/>', $items);
+
+    return <<<XML
+        <?xml version="1.0"?>
+        <SOAP-ENV:Envelope
+            xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
+            xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            xmlns:ns1="http://test-uri/">
+          <SOAP-ENV:Body>
+            <ns1:fResponse><p SOAP-ENC:arrayType="$arrayType">$items</p></ns1:fResponse>
+          </SOAP-ENV:Body>
+        </SOAP-ENV:Envelope>
+        XML;
+}
+
+/* A SoapClient whose transport is replaced by a canned response, so that the
+ * reproducer exercises only the response decoder. */
+class OomSoapClient extends SoapClient
+{
+    public string $response = '';
+
+    public function __doRequest($request, $location, $action, $version, $oneWay = false): ?string
+    {
+        return $this->response;
+    }
+}
diff --git a/ext/soap/tests/oom_soapclient_call.phpt b/ext/soap/tests/oom_soapclient_call.phpt
new file mode 100644
index 00000000000..fc4da208514
--- /dev/null
+++ b/ext/soap/tests/oom_soapclient_call.phpt
@@ -0,0 +1,16 @@
+--TEST--
+SoapClient::__call() doesn't reach hard limit
+--EXTENSIONS--
+soap
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+require __DIR__ . '/oom_soap_response.inc';
+
+$client = new OomSoapClient(null, ['location' => 'http://test-uri/', 'uri' => 'http://test-uri/']);
+$client->response = oom_soap_response(1000, 1000);
+$client->f();
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 67108864 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/soap/tests/oom_soapclient_dorequest_chunked.phpt b/ext/soap/tests/oom_soapclient_dorequest_chunked.phpt
new file mode 100644
index 00000000000..8e17caa5418
--- /dev/null
+++ b/ext/soap/tests/oom_soapclient_dorequest_chunked.phpt
@@ -0,0 +1,24 @@
+--TEST--
+SoapClient::__doRequest() doesn't reach hard limit through a chunk size
+--EXTENSIONS--
+soap
+--SKIPIF--
+<?php
+require __DIR__ . '/oom_http_server.inc';
+oom_soap_http_server_skipif();
+?>
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+require __DIR__ . '/oom_http_server.inc';
+
+$server = oom_soap_http_server(
+    "HTTP/1.1 200 OK\r\nContent-Type: text/xml\r\nTransfer-Encoding: chunked\r\n\r\n77359400\r\n"
+);
+
+$client = new SoapClient(null, ['location' => $server['uri'], 'uri' => 'http://test-uri/']);
+$client->__doRequest('<?xml version="1.0"?><E/>', $server['uri'], 'f', SOAP_1_1);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 67108864 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/soap/tests/oom_soapclient_dorequest_close.phpt b/ext/soap/tests/oom_soapclient_dorequest_close.phpt
new file mode 100644
index 00000000000..186e4d9edd5
--- /dev/null
+++ b/ext/soap/tests/oom_soapclient_dorequest_close.phpt
@@ -0,0 +1,28 @@
+--TEST--
+SoapClient::__doRequest() doesn't reach hard limit through a connection-close body
+--EXTENSIONS--
+soap
+--SKIPIF--
+<?php
+require __DIR__ . '/oom_http_server.inc';
+oom_soap_http_server_skipif();
+?>
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+require __DIR__ . '/oom_http_server.inc';
+
+/* No Content-Length and no Transfer-Encoding, so get_http_body() takes the
+ * header_close branch and grows the buffer 4096 bytes at a time until EOF. */
+$server = oom_soap_http_server(
+    "HTTP/1.1 200 OK\r\nContent-Type: text/xml\r\nConnection: close\r\n\r\n",
+    str_repeat('x', 32),
+    1024 * 1024
+);
+
+$client = new SoapClient(null, ['location' => $server['uri'], 'uri' => 'http://test-uri/']);
+$client->__doRequest('<?xml version="1.0"?><E/>', $server['uri'], 'f', SOAP_1_1);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/soap/tests/oom_soapclient_dorequest_content_length.phpt b/ext/soap/tests/oom_soapclient_dorequest_content_length.phpt
new file mode 100644
index 00000000000..2825a9e7b98
--- /dev/null
+++ b/ext/soap/tests/oom_soapclient_dorequest_content_length.phpt
@@ -0,0 +1,24 @@
+--TEST--
+SoapClient::__doRequest() doesn't reach hard limit through Content-Length
+--EXTENSIONS--
+soap
+--SKIPIF--
+<?php
+require __DIR__ . '/oom_http_server.inc';
+oom_soap_http_server_skipif();
+?>
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+require __DIR__ . '/oom_http_server.inc';
+
+$server = oom_soap_http_server(
+    "HTTP/1.1 200 OK\r\nContent-Type: text/xml\r\nContent-Length: 2000000000\r\n\r\n"
+);
+
+$client = new SoapClient(null, ['location' => $server['uri'], 'uri' => 'http://test-uri/']);
+$client->__doRequest('<?xml version="1.0"?><E/>', $server['uri'], 'f', SOAP_1_1);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 67108864 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/soap/tests/oom_soapclient_response_headers.phpt b/ext/soap/tests/oom_soapclient_response_headers.phpt
new file mode 100644
index 00000000000..73d2ceaf178
--- /dev/null
+++ b/ext/soap/tests/oom_soapclient_response_headers.phpt
@@ -0,0 +1,26 @@
+--TEST--
+SoapClient::__doRequest() doesn't reach hard limit through response headers
+--EXTENSIONS--
+soap
+--SKIPIF--
+<?php
+require __DIR__ . '/oom_http_server.inc';
+oom_soap_http_server_skipif();
+?>
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+require __DIR__ . '/oom_http_server.inc';
+
+$server = oom_soap_http_server(
+    "HTTP/1.1 200 OK\r\n",
+    "X-Pad: " . str_repeat('a', 4000) . "\r\n",
+    8 * 1024
+);
+
+$client = new SoapClient(null, ['location' => $server['uri'], 'uri' => 'http://test-uri/']);
+$client->__doRequest('<?xml version="1.0"?><E/>', $server['uri'], 'f', SOAP_1_1);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/soap/tests/oom_soapclient_soapcall.phpt b/ext/soap/tests/oom_soapclient_soapcall.phpt
new file mode 100644
index 00000000000..dde3932616c
--- /dev/null
+++ b/ext/soap/tests/oom_soapclient_soapcall.phpt
@@ -0,0 +1,16 @@
+--TEST--
+SoapClient::__soapCall() doesn't reach hard limit
+--EXTENSIONS--
+soap
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+require __DIR__ . '/oom_soap_response.inc';
+
+$client = new OomSoapClient(null, ['location' => 'http://test-uri/', 'uri' => 'http://test-uri/']);
+$client->response = oom_soap_response(1000, 1000);
+$client->__soapCall('f', []);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 67108864 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/soap/tests/oom_soapserver_handle_array.phpt b/ext/soap/tests/oom_soapserver_handle_array.phpt
new file mode 100644
index 00000000000..f442d8b6a08
--- /dev/null
+++ b/ext/soap/tests/oom_soapserver_handle_array.phpt
@@ -0,0 +1,30 @@
+--TEST--
+SoapServer::handle() doesn't reach hard limit through a SOAP-ENC array
+--EXTENSIONS--
+soap
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+function f() {}
+
+$arrayType = 'xsd:string[' . implode(',', array_fill(0, 1000, 1)) . ']';
+$items = str_repeat('<i/>', 1000);
+$request = <<<XML
+    <?xml version="1.0"?>
+    <SOAP-ENV:Envelope
+        xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
+        xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
+        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+        xmlns:ns1="http://test-uri/">
+      <SOAP-ENV:Body><ns1:f><p SOAP-ENC:arrayType="$arrayType">$items</p></ns1:f></SOAP-ENV:Body>
+    </SOAP-ENV:Envelope>
+    XML;
+
+$server = new SoapServer(null, ['uri' => 'http://test-uri/']);
+$server->addFunction('f');
+$server->handle($request);
+?>
+--EXPECTF--
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>Allowed memory size of 67108864 bytes exhausted %S(tried to allocate %d bytes)</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope>
diff --git a/ext/soap/tests/oom_soapserver_handle_headers.phpt b/ext/soap/tests/oom_soapserver_handle_headers.phpt
new file mode 100644
index 00000000000..7df24dfed7a
--- /dev/null
+++ b/ext/soap/tests/oom_soapserver_handle_headers.phpt
@@ -0,0 +1,26 @@
+--TEST--
+SoapServer::handle() doesn't reach hard limit through SOAP headers
+--EXTENSIONS--
+soap
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+function f() {}
+
+$headers = str_repeat('<ns1:h/>', 20000);
+$request = <<<XML
+    <?xml version="1.0"?>
+    <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://test-uri/">
+      <SOAP-ENV:Header>$headers</SOAP-ENV:Header>
+      <SOAP-ENV:Body><ns1:f/></SOAP-ENV:Body>
+    </SOAP-ENV:Envelope>
+    XML;
+
+$server = new SoapServer(null, ['uri' => 'http://test-uri/']);
+$server->addFunction('f');
+$server->handle($request);
+?>
+--EXPECTF--
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>Allowed memory size of 67108864 bytes exhausted %S(tried to allocate %d bytes)</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope>
diff --git a/ext/soap/tests/oom_soapserver_handle_parameters.phpt b/ext/soap/tests/oom_soapserver_handle_parameters.phpt
new file mode 100644
index 00000000000..e388b9133da
--- /dev/null
+++ b/ext/soap/tests/oom_soapserver_handle_parameters.phpt
@@ -0,0 +1,25 @@
+--TEST--
+SoapServer::handle() doesn't reach hard limit through request parameters
+--EXTENSIONS--
+soap
+--INI--
+memory_limit=64M
+--FILE--
+<?php
+function f() {}
+
+$params = str_repeat('<p><a/></p>', 400000);
+$request = <<<XML
+    <?xml version="1.0"?>
+    <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://test-uri/">
+      <SOAP-ENV:Body><ns1:f>$params</ns1:f></SOAP-ENV:Body>
+    </SOAP-ENV:Envelope>
+    XML;
+
+$server = new SoapServer(null, ['uri' => 'http://test-uri/']);
+$server->addFunction('f');
+$server->handle($request);
+?>
+--EXPECTF--
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>Allowed memory size of 67108864 bytes exhausted %S(tried to allocate %d bytes)</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope>
diff --git a/ext/spl/spl_fixedarray.c b/ext/spl/spl_fixedarray.c
index bbee2b93706..a1f848c4a03 100644
--- a/ext/spl/spl_fixedarray.c
+++ b/ext/spl/spl_fixedarray.c
@@ -105,23 +105,34 @@ static void spl_fixedarray_init_elems(spl_fixedarray *array, zend_long from, zen
 	}
 }

-static void spl_fixedarray_init_non_empty_struct(spl_fixedarray *array, zend_long size)
+static zend_result spl_fixedarray_init_non_empty_struct(spl_fixedarray *array, zend_long size)
 {
 	array->size = 0; /* reset size in case ecalloc() fails */
-	array->elements = size ? safe_emalloc(size, sizeof(zval), 0) : NULL;
-	array->size = size;
 	array->should_rebuild_properties = true;
 	array->cached_resize = -1;
+	if (size) {
+		array->elements = safe_femalloc(size, sizeof(zval), 0, /* fallible */ true);
+		if (UNEXPECTED(!array->elements)) {
+			return FAILURE;
+		}
+		array->size = size;
+	} else {
+		array->elements = NULL;
+	}
+	return SUCCESS;
 }

-static void spl_fixedarray_init(spl_fixedarray *array, zend_long size)
+static zend_result spl_fixedarray_init(spl_fixedarray *array, zend_long size)
 {
 	if (size > 0) {
-		spl_fixedarray_init_non_empty_struct(array, size);
+		if (UNEXPECTED(spl_fixedarray_init_non_empty_struct(array, size) == FAILURE)) {
+			return FAILURE;
+		}
 		spl_fixedarray_init_elems(array, 0, size);
 	} else {
 		spl_fixedarray_default_ctor(array);
 	}
+	return SUCCESS;
 }

 /* Copies the range [begin, end) into the fixedarray, beginning at `offset`.
@@ -138,14 +149,17 @@ static void spl_fixedarray_copy_range(spl_fixedarray *array, zend_long offset, z
 	}
 }

-static void spl_fixedarray_copy_ctor(spl_fixedarray *to, spl_fixedarray *from)
+static zend_result spl_fixedarray_copy_ctor(spl_fixedarray *to, spl_fixedarray *from)
 {
 	zend_long size = from->size;
-	spl_fixedarray_init(to, size);
+	if (UNEXPECTED(spl_fixedarray_init(to, size) == FAILURE)) {
+		return FAILURE;
+	}
 	if (size != 0) {
 		zval *begin = from->elements, *end = from->elements + size;
 		spl_fixedarray_copy_range(to, 0, begin, end);
 	}
+	return SUCCESS;
 }

 /* Destructs the elements in the range [from, to).
@@ -176,25 +190,24 @@ static void spl_fixedarray_dtor(spl_fixedarray *array)
 	}
 }

-static void spl_fixedarray_resize(spl_fixedarray *array, zend_long size)
+static zend_result spl_fixedarray_resize(spl_fixedarray *array, zend_long size)
 {
 	if (size == array->size) {
 		/* nothing to do */
-		return;
+		return SUCCESS;
 	}
 	array->should_rebuild_properties = true;

 	/* first initialization */
 	if (array->size == 0) {
-		spl_fixedarray_init(array, size);
-		return;
+		return spl_fixedarray_init(array, size);
 	}

 	if (UNEXPECTED(array->cached_resize >= 0)) {
 		/* We're already resizing, so just remember the desired size.
 		 * The resize will happen later. */
 		array->cached_resize = size;
-		return;
+		return SUCCESS;
 	}
 	array->cached_resize = size;

@@ -204,7 +217,12 @@ static void spl_fixedarray_resize(spl_fixedarray *array, zend_long size)
 		array->elements = NULL;
 		array->size = 0;
 	} else if (size > array->size) {
-		array->elements = safe_erealloc(array->elements, size, sizeof(zval), 0);
+		zval *elements = safe_ferealloc(array->elements, size, sizeof(zval), 0, /* fallible */ true);
+		if (UNEXPECTED(!elements)) {
+			array->cached_resize = -1;
+			return FAILURE;
+		}
+		array->elements = elements;
 		spl_fixedarray_init_elems(array, array->size, size);
 		array->size = size;
 	} else { /* size < array->size */
@@ -217,8 +235,10 @@ static void spl_fixedarray_resize(spl_fixedarray *array, zend_long size)
 	zend_long cached_resize = array->cached_resize;
 	array->cached_resize = -1;
 	if (cached_resize != size) {
-		spl_fixedarray_resize(array, cached_resize);
+		return spl_fixedarray_resize(array, cached_resize);
 	}
+
+	return SUCCESS;
 }

 static HashTable* spl_fixedarray_object_get_gc(zend_object *obj, zval **table, int *n)
@@ -296,11 +316,6 @@ static zend_object *spl_fixedarray_object_new_ex(zend_class_entry *class_type, z
 	zend_object_std_init(&intern->std, class_type);
 	object_properties_init(&intern->std, class_type);

-	if (orig && clone_orig) {
-		spl_fixedarray_object *other = spl_fixed_array_from_obj(orig);
-		spl_fixedarray_copy_ctor(&intern->array, &other->array);
-	}
-
 	while (parent) {
 		if (parent == spl_ce_SplFixedArray) {
 			intern->std.handlers = &spl_handler_SplFixedArray;
@@ -311,6 +326,14 @@ static zend_object *spl_fixedarray_object_new_ex(zend_class_entry *class_type, z
 		inherited = true;
 	}

+	if (orig && clone_orig) {
+		spl_fixedarray_object *other = spl_fixed_array_from_obj(orig);
+		if (UNEXPECTED(spl_fixedarray_copy_ctor(&intern->array, &other->array) == FAILURE)) {
+			ZEND_ASSERT(intern->std.handlers);
+			return &intern->std;
+		}
+	}
+
 	ZEND_ASSERT(parent);

 	if (UNEXPECTED(inherited)) {
@@ -580,7 +603,9 @@ PHP_METHOD(SplFixedArray, __construct)
 		return;
 	}

-	spl_fixedarray_init(&intern->array, size);
+	if (UNEXPECTED(spl_fixedarray_init(&intern->array, size) == FAILURE)) {
+		RETURN_OOM();
+	}
 }

 PHP_METHOD(SplFixedArray, __wakeup)
@@ -597,7 +622,9 @@ PHP_METHOD(SplFixedArray, __wakeup)
 		int index = 0;
 		int size = zend_hash_num_elements(intern_ht);

-		spl_fixedarray_init(&intern->array, size);
+		if (UNEXPECTED(spl_fixedarray_init(&intern->array, size) == FAILURE)) {
+			RETURN_OOM();
+		}

 		ZEND_HASH_FOREACH_VAL(intern_ht, data) {
 			ZVAL_COPY(&intern->array.elements[index], data);
@@ -657,7 +684,9 @@ PHP_METHOD(SplFixedArray, __unserialize)

 	if (intern->array.size == 0) {
 		size = zend_hash_num_elements(data);
-		spl_fixedarray_init_non_empty_struct(&intern->array, size);
+		if (UNEXPECTED(spl_fixedarray_init_non_empty_struct(&intern->array, size) == FAILURE)) {
+			RETURN_OOM();
+		}
 		if (!size) {
 			return;
 		}
@@ -758,7 +787,9 @@ PHP_METHOD(SplFixedArray, fromArray)
 			zend_throw_exception_ex(spl_ce_InvalidArgumentException, 0, "integer overflow detected");
 			RETURN_THROWS();
 		}
-		spl_fixedarray_init(&array, tmp);
+		if (UNEXPECTED(spl_fixedarray_init(&array, tmp) == FAILURE)) {
+			RETURN_OOM();
+		}

 		ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(data), num_index, str_index, element) {
 			ZVAL_COPY_DEREF(&array.elements[num_index], element);
@@ -768,14 +799,18 @@ PHP_METHOD(SplFixedArray, fromArray)
 		zval *element;
 		zend_long i = 0;

-		spl_fixedarray_init(&array, num);
+		if (UNEXPECTED(spl_fixedarray_init(&array, num) == FAILURE)) {
+			RETURN_OOM();
+		}

 		ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(data), element) {
 			ZVAL_COPY_DEREF(&array.elements[i], element);
 			i++;
 		} ZEND_HASH_FOREACH_END();
 	} else {
-		spl_fixedarray_init(&array, 0);
+		if (UNEXPECTED(spl_fixedarray_init(&array, 0) == FAILURE)) {
+			RETURN_OOM();
+		}
 	}

 	object_init_ex(return_value, spl_ce_SplFixedArray);
@@ -814,7 +849,10 @@ PHP_METHOD(SplFixedArray, setSize)

 	intern = Z_SPLFIXEDARRAY_P(object);

-	spl_fixedarray_resize(&intern->array, size);
+	if (UNEXPECTED(spl_fixedarray_resize(&intern->array, size) == FAILURE)) {
+		RETURN_OOM();
+	}
+
 	RETURN_TRUE;
 }

diff --git a/ext/standard/array.c b/ext/standard/array.c
index 30b9a425b20..7183c8b3852 100644
--- a/ext/standard/array.c
+++ b/ext/standard/array.c
@@ -2591,8 +2591,9 @@ PHP_FUNCTION(array_fill)
 			/* create packed array */
 			zval *zv;

-			array_init_size(return_value, (uint32_t)(start_key + num));
-			zend_hash_real_init_packed(Z_ARRVAL_P(return_value));
+			if (UNEXPECTED(!array_finit_size_packed(return_value, (uint32_t)(start_key + num)))) {
+				RETURN_OOM();
+			}
 			Z_ARRVAL_P(return_value)->nNumUsed = (uint32_t)(start_key + num);
 			Z_ARRVAL_P(return_value)->nNumOfElements = (uint32_t)num;
 			Z_ARRVAL_P(return_value)->nNextFreeElement = (zend_long)(start_key + num);
@@ -2613,8 +2614,9 @@ PHP_FUNCTION(array_fill)
 			}
 		} else {
 			/* create hash */
-			array_init_size(return_value, (uint32_t)num);
-			zend_hash_real_init_mixed(Z_ARRVAL_P(return_value));
+			if (UNEXPECTED(!array_finit_size_mixed(return_value, (uint32_t)num))) {
+				RETURN_OOM();
+			}
 			if (Z_REFCOUNTED_P(val)) {
 				GC_ADDREF_EX(Z_COUNTED_P(val), (uint32_t)num);
 			}
@@ -2670,7 +2672,9 @@ PHP_FUNCTION(array_fill_keys)
 		} \
 		size = (uint32_t)_php_math_round(__calc_size, 0, PHP_ROUND_HALF_UP); \
 		array_init_size(return_value, size); \
-		zend_hash_real_init_packed(Z_ARRVAL_P(return_value)); \
+		if (UNEXPECTED(zend_hash_real_finit_packed(Z_ARRVAL_P(return_value), /* fallible */ true) == FAILURE)) { \
+			RETURN_OOM(); \
+		} \
 	} while (0)

 #define RANGE_CHECK_LONG_INIT_ARRAY(start, end) do { \
@@ -2682,7 +2686,9 @@ PHP_FUNCTION(array_fill_keys)
 		} \
 		size = (uint32_t)(__calc_size + 1); \
 		array_init_size(return_value, size); \
-		zend_hash_real_init_packed(Z_ARRVAL_P(return_value)); \
+		if (UNEXPECTED(zend_hash_real_finit_packed(Z_ARRVAL_P(return_value), /* fallible */ true) == FAILURE)) { \
+			RETURN_OOM(); \
+		} \
 	} while (0)

 /* {{{ Create an array containing the range of integers or characters from low to high (inclusive) */
@@ -4373,6 +4379,14 @@ PHP_FUNCTION(array_reverse)
 }
 /* }}} */

+/* Releases references taken up-front for pad values that were not inserted. */
+static zend_always_inline void php_array_pad_release(zval *pad_value, zend_long num_pads)
+{
+	if (Z_REFCOUNTED_P(pad_value)) {
+		GC_DELREF_EX(Z_COUNTED_P(pad_value), num_pads);
+	}
+}
+
 /* {{{ Returns a copy of input array padded with pad_value to size pad_size */
 PHP_FUNCTION(array_pad)
 {
@@ -4413,7 +4427,10 @@ PHP_FUNCTION(array_pad)

 	array_init_size(return_value, pad_size_abs);
 	if (HT_IS_PACKED(Z_ARRVAL_P(input))) {
-		zend_hash_real_init_packed(Z_ARRVAL_P(return_value));
+		if (UNEXPECTED(zend_hash_real_finit_packed(Z_ARRVAL_P(return_value), /* fallible */ true) == FAILURE)) {
+			php_array_pad_release(pad_value, num_pads);
+			RETURN_OOM();
+		}

 		if (pad_size < 0) {
 			ZEND_HASH_FILL_PACKED(Z_ARRVAL_P(return_value)) {
@@ -4440,22 +4457,36 @@ PHP_FUNCTION(array_pad)
 	} else {
 		if (pad_size < 0) {
 			for (i = 0; i < num_pads; i++) {
-				zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), pad_value);
+				if (UNEXPECTED(zend_hash_next_index_finsert_new(Z_ARRVAL_P(return_value), pad_value) == HASH_NOMEM)) {
+					php_array_pad_release(pad_value, num_pads - i);
+					RETURN_OOM();
+				}
 			}
 		}

 		ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(input), key, value) {
 			Z_TRY_ADDREF_P(value);
+			zval *result;
 			if (key) {
-				zend_hash_add_new(Z_ARRVAL_P(return_value), key, value);
+				result = zend_hash_fadd_new(Z_ARRVAL_P(return_value), key, value);
 			} else {
-				zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), value);
+				result = zend_hash_next_index_finsert_new(Z_ARRVAL_P(return_value), value);
+			}
+			if (UNEXPECTED(result == HASH_NOMEM)) {
+				Z_TRY_DELREF_P(value);
+				if (pad_size > 0) {
+					php_array_pad_release(pad_value, num_pads);
+				}
+				RETURN_OOM();
 			}
 		} ZEND_HASH_FOREACH_END();

 		if (pad_size > 0) {
 			for (i = 0; i < num_pads; i++) {
-				zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), pad_value);
+				if (UNEXPECTED(zend_hash_next_index_finsert_new(Z_ARRVAL_P(return_value), pad_value) == HASH_NOMEM)) {
+					php_array_pad_release(pad_value, num_pads - i);
+					RETURN_OOM();
+				}
 			}
 		}
 	}
@@ -6260,7 +6291,10 @@ PHP_FUNCTION(array_map)
 			}
 		}

-		array_init_size(return_value, maxlen);
+		if (UNEXPECTED(!array_finit_size_packed(return_value, maxlen))) {
+			efree(array_pos);
+			RETURN_OOM();
+		}

 		if (!ZEND_FCI_INITIALIZED(fci)) {
 			zval zv;
@@ -6270,6 +6304,10 @@ PHP_FUNCTION(array_map)

 				/* If no callback, the result will be an array, consisting of current
 				 * entries from all arrays. */
+				if (ZEND_HAS_OOM()) {
+					efree(array_pos);
+					RETURN_OOM();
+				}
 				array_init_size(&result, n_arrays);

 				for (i = 0; i < n_arrays; i++) {
@@ -6446,13 +6484,18 @@ PHP_FUNCTION(array_chunk)
 		size = num_in;
 	}

-	array_init_size(return_value, (uint32_t)(((num_in - 1) / size) + 1));
+	if (UNEXPECTED(!array_finit_size_packed(return_value, (uint32_t)(((num_in - 1) / size) + 1)))) {
+		RETURN_OOM();
+	}

 	ZVAL_UNDEF(&chunk);

 	ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(input), num_key, str_key, entry) {
 		/* If new chunk, create and initialize it. */
 		if (Z_TYPE(chunk) == IS_UNDEF) {
+			if (ZEND_HAS_OOM()) {
+				RETURN_OOM();
+			}
 			array_init_size(&chunk, (uint32_t)size);
 		}

diff --git a/ext/standard/assert.c b/ext/standard/assert.c
index f318b1c8340..8ce70b00991 100644
--- a/ext/standard/assert.c
+++ b/ext/standard/assert.c
@@ -200,7 +200,9 @@ PHP_FUNCTION(assert)
 			 * exception so we can avoid bailout and use unwind_exit. */
 			zend_exception_error(EG(exception), E_WARNING);
 		}
-		zend_throw_unwind_exit();
+		if (!EG(exception)) {
+			zend_throw_unwind_exit();
+		}
 		RETURN_THROWS();
 	} else {
 		RETURN_FALSE;
diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c
index b2282750fd0..6edfb5d653f 100755
--- a/ext/standard/basic_functions.c
+++ b/ext/standard/basic_functions.c
@@ -501,6 +501,18 @@ PHP_RSHUTDOWN_FUNCTION(basic) /* {{{ */
 }
 /* }}} */

+/* Called from php_request_startup(), before any request data is processed. */
+PHPAPI void php_basic_early_rinit(void)
+{
+	/* The session upload progress handler (un)serializes the session and
+	 * registers the session id with the URL rewriter from
+	 * php_hash_environment(), before RINIT. */
+	BG(serialize_lock) = 0;
+	memset(&BG(serialize), 0, sizeof(BG(serialize)));
+	memset(&BG(unserialize), 0, sizeof(BG(unserialize)));
+	php_url_scanner_early_rinit();
+}
+
 PHP_MINFO_FUNCTION(basic) /* {{{ */
 {
 	php_info_print_table_start();
diff --git a/ext/standard/basic_functions.h b/ext/standard/basic_functions.h
index 34cec376e76..60983bdfb87 100644
--- a/ext/standard/basic_functions.h
+++ b/ext/standard/basic_functions.h
@@ -135,6 +135,7 @@ PHPAPI extern bool register_user_shutdown_function(const char *function_name, si
 PHPAPI extern bool remove_user_shutdown_function(const char *function_name, size_t function_len);
 PHPAPI extern bool append_user_shutdown_function(php_shutdown_function_entry *shutdown_function_entry);

+PHPAPI void php_basic_early_rinit(void);
 PHPAPI void php_call_shutdown_functions(void);
 PHPAPI void php_free_shutdown_functions(void);

diff --git a/ext/standard/file.c b/ext/standard/file.c
index 30f05683b59..0e378db53ee 100644
--- a/ext/standard/file.c
+++ b/ext/standard/file.c
@@ -657,12 +657,18 @@ PHP_FUNCTION(file)
 			do {
 				p++;
 parse_eol:
+				if (ZEND_HAS_OOM()) {
+					break;
+				}
 				add_index_stringl(return_value, i++, s, p-s);
 				s = p;
 			} while ((p = memchr(p, eol_marker, (e-p))));
 		} else {
 			do {
 				int windows_eol = 0;
+				if (ZEND_HAS_OOM()) {
+					break;
+				}
 				if (p != ZSTR_VAL(target_buf) && eol_marker == '\n' && *(p - 1) == '\r') {
 					windows_eol++;
 				}
@@ -676,7 +682,7 @@ PHP_FUNCTION(file)
 		}

 		/* handle any left overs of files without new lines */
-		if (s != e) {
+		if (s != e && !ZEND_HAS_OOM()) {
 			p = e;
 			goto parse_eol;
 		}
@@ -910,8 +916,13 @@ PHPAPI PHP_FUNCTION(fgets)
 			RETURN_FALSE;
 		}
 		// TODO: avoid reallocation ???
-		RETVAL_STRINGL(buf, line_len);
+		/* This copy can double an already large buffer. */
+		str = zend_string_finit(buf, line_len, /* persistent */ false, /* fallible */ true);
 		efree(buf);
+		if (UNEXPECTED(!str)) {
+			RETURN_OOM();
+		}
+		RETVAL_STR(str);
 	} else {
 		if (len <= 0) {
 			zend_argument_value_error(2, "must be greater than 0");
@@ -1965,6 +1976,11 @@ PHPAPI HashTable *php_fgetcsv(php_stream *stream, char delimiter, char enclosure
 		char *comp_end, *hunk_begin;
 		char *tptr = temp;

+		/* Each field costs a zval plus a zend_string, so stop once over the limit. */
+		if (ZEND_HAS_OOM()) {
+			break;
+		}
+
 		inc_len = (bptr < limit ? (*bptr == '\0' ? 1 : php_mblen(bptr, limit - bptr)): 0);
 		if (inc_len == 1) {
 			char *tmp = bptr;
diff --git a/ext/standard/head.c b/ext/standard/head.c
index 5bdae98dfce..4652138ab22 100644
--- a/ext/standard/head.c
+++ b/ext/standard/head.c
@@ -137,12 +137,20 @@ PHPAPI zend_result php_setcookie(zend_string *name, zend_string *value, time_t e
 		smart_str_appends(&buf, "Set-Cookie: ");
 		smart_str_append(&buf, name);
 		smart_str_appendc(&buf, '=');
+		/* The encoded value is up to three copies of the input. */
 		if (url_encode) {
 			zend_string *encoded_value = php_raw_url_encode(ZSTR_VAL(value), ZSTR_LEN(value));
-			smart_str_append(&buf, encoded_value);
+			bool appended = smart_str_fappendl_ex(&buf, ZSTR_VAL(encoded_value),
+					ZSTR_LEN(encoded_value), /* persistent */ false, /* fallible */ true);
 			zend_string_release_ex(encoded_value, 0);
-		} else {
-			smart_str_append(&buf, value);
+			if (UNEXPECTED(!appended)) {
+				smart_str_free(&buf);
+				return FAILURE;
+			}
+		} else if (UNEXPECTED(!smart_str_fappendl_ex(&buf, ZSTR_VAL(value), ZSTR_LEN(value),
+				/* persistent */ false, /* fallible */ true))) {
+			smart_str_free(&buf);
+			return FAILURE;
 		}

 		if (expires > 0) {
diff --git a/ext/standard/html.c b/ext/standard/html.c
index e3e91bb3a27..b4b3f7ddfcb 100644
--- a/ext/standard/html.c
+++ b/ext/standard/html.c
@@ -1149,7 +1149,10 @@ PHPAPI zend_string *php_escape_html_entities_ex(const unsigned char *old, size_t
 		maxlen = zend_safe_addmult(oldlen, 2, 0, "html_entities");
 	}

-	replaced = zend_string_alloc(maxlen, 0);
+	replaced = zend_string_falloc(maxlen, /* persistent */ false, /* fallible */ true);
+	if (!replaced) {
+		return ZSTR_EMPTY_ALLOC();
+	}
 	len = 0;
 	cursor = 0;
 	while (cursor < oldlen) {
@@ -1162,7 +1165,12 @@ PHPAPI zend_string *php_escape_html_entities_ex(const unsigned char *old, size_t
 		/* guarantee we have at least 40 bytes to write.
 		 * In HTML5, entities may take up to 33 bytes */
 		if (len > maxlen - 40) { /* maxlen can never be smaller than 128 */
-			replaced = zend_string_safe_realloc(replaced, maxlen, 1, 128, 0);
+			zend_string *new_replaced = zend_string_safe_frealloc(replaced, maxlen, 1, 128, /* persistent */ false, /* fallible */ true);
+			if (!new_replaced) {
+				zend_string_release(replaced);
+				return ZSTR_EMPTY_ALLOC();
+			}
+			replaced = new_replaced;
 			maxlen += 128;
 		}

diff --git a/ext/standard/image.c b/ext/standard/image.c
index 15761364c34..58f45e66c6c 100644
--- a/ext/standard/image.c
+++ b/ext/standard/image.c
@@ -204,7 +204,14 @@ static struct gfxinfo *php_handle_swc(php_stream * stream)

 		do {
 			szlength = ZSTR_LEN(bufz) * (1<<factor++);
-			buf = erealloc(buf, szlength);
+			unsigned char *new_buf = safe_ferealloc(buf, 1, szlength, 0, /* fallible */ true);
+			if (UNEXPECTED(!new_buf)) {
+				efree(b);
+				zend_string_release_ex(bufz, 0);
+				efree(buf);
+				return NULL;
+			}
+			buf = new_buf;
 			status = uncompress(buf, &szlength, (unsigned char *) ZSTR_VAL(bufz), ZSTR_LEN(bufz));
 		} while ((status==Z_BUF_ERROR)&&(factor<maxfactor));

@@ -1000,6 +1007,12 @@ static int php_get_xbm(php_stream *stream, struct gfxinfo **result)
 		return 0;
 	}
 	while ((fline=php_stream_gets(stream, NULL, 0)) != NULL) {
+		/* The line is bounded by the stream, and iname duplicates it. */
+		if (ZEND_HAS_OOM()) {
+			efree(fline);
+			fline = NULL;
+			break;
+		}
 		iname = estrdup(fline); /* simple way to get necessary buffer of required size */
 		if (sscanf(fline, "#define %s %d", iname, &value) == 2) {
 			if (!(type = strrchr(iname, '_'))) {
diff --git a/ext/standard/levenshtein.c b/ext/standard/levenshtein.c
index b336be22208..477c27384f2 100644
--- a/ext/standard/levenshtein.c
+++ b/ext/standard/levenshtein.c
@@ -19,21 +19,31 @@

 /* {{{ reference_levdist
  * reference implementation, only optimized for memory usage, not speed */
-static zend_long reference_levdist(const zend_string *string1, const zend_string *string2, zend_long cost_ins, zend_long cost_rep, zend_long cost_del )
+static bool reference_levdist(const zend_string *string1, const zend_string *string2, zend_long cost_ins, zend_long cost_rep, zend_long cost_del, zend_long *result)
 {
 	zend_long *p1, *p2, *tmp;
 	zend_long c0, c1, c2;
 	size_t i1, i2;

 	if (ZSTR_LEN(string1) == 0) {
-		return ZSTR_LEN(string2) * cost_ins;
+		*result = ZSTR_LEN(string2) * cost_ins;
+		return true;
 	}
 	if (ZSTR_LEN(string2) == 0) {
-		return ZSTR_LEN(string1) * cost_del;
+		*result = ZSTR_LEN(string1) * cost_del;
+		return true;
 	}

-	p1 = safe_emalloc((ZSTR_LEN(string2) + 1), sizeof(zend_long), 0);
-	p2 = safe_emalloc((ZSTR_LEN(string2) + 1), sizeof(zend_long), 0);
+	/* Each row is eight times the length of string2. */
+	p1 = safe_femalloc((ZSTR_LEN(string2) + 1), sizeof(zend_long), 0, /* fallible */ true);
+	if (UNEXPECTED(!p1)) {
+		return false;
+	}
+	p2 = safe_femalloc((ZSTR_LEN(string2) + 1), sizeof(zend_long), 0, /* fallible */ true);
+	if (UNEXPECTED(!p2)) {
+		efree(p1);
+		return false;
+	}

 	for (i2 = 0; i2 <= ZSTR_LEN(string2); i2++) {
 		p1[i2] = i2 * cost_ins;
@@ -62,7 +72,8 @@ static zend_long reference_levdist(const zend_string *string1, const zend_string
 	efree(p1);
 	efree(p2);

-	return c0;
+	*result = c0;
+	return true;
 }
 /* }}} */

@@ -79,6 +90,10 @@ PHP_FUNCTION(levenshtein)
 	}


-	RETURN_LONG(reference_levdist(string1, string2, cost_ins, cost_rep, cost_del));
+	zend_long distance;
+	if (UNEXPECTED(!reference_levdist(string1, string2, cost_ins, cost_rep, cost_del, &distance))) {
+		RETURN_OOM();
+	}
+	RETURN_LONG(distance);
 }
 /* }}} */
diff --git a/ext/standard/pack.c b/ext/standard/pack.c
index 24d116d3020..e9711d900c5 100644
--- a/ext/standard/pack.c
+++ b/ext/standard/pack.c
@@ -878,6 +878,11 @@ PHP_FUNCTION(unpack)
 		/* Do actual unpacking */
 		for (i = 0; i != repetitions; i++ ) {

+			/* Each repetition costs a name and a value, so stop once over the limit. */
+			if (ZEND_HAS_OOM()) {
+				RETURN_OOM();
+			}
+
 			if (size != 0 && size != -1 && INT_MAX - size + 1 < inputpos) {
 				php_error_docref(NULL, E_WARNING, "Type %c: integer overflow", type);
 				zend_array_destroy(Z_ARR_P(return_value));
diff --git a/ext/standard/php_incomplete_class.h b/ext/standard/php_incomplete_class.h
index 07eabf60d5d..9ed43e88dab 100644
--- a/ext/standard/php_incomplete_class.h
+++ b/ext/standard/php_incomplete_class.h
@@ -44,6 +44,11 @@ extern PHPAPI zend_class_entry *php_ce_incomplete_class;

 #define INCOMPLETE_CLASS "__PHP_Incomplete_Class"
 #define MAGIC_MEMBER "__PHP_Incomplete_Class_Name"
+/* Set by php_var_unserialize() in incomplete_classes_only mode: the raw payload
+ * of a C: value, or the case name of an E: value. The serializer writes them
+ * back unchanged. */
+#define MAGIC_MEMBER_SERIALIZED "__PHP_Incomplete_Class_Serialized"
+#define MAGIC_MEMBER_ENUM_CASE "__PHP_Incomplete_Enum_Case"

 #ifdef __cplusplus
 extern "C" {
diff --git a/ext/standard/php_var.h b/ext/standard/php_var.h
index b816da8e19d..94617b16d89 100644
--- a/ext/standard/php_var.h
+++ b/ext/standard/php_var.h
@@ -37,11 +37,13 @@ PHPAPI int php_var_unserialize_ref(zval *rval, const unsigned char **p, const un
 PHPAPI int php_var_unserialize_intern(zval *rval, const unsigned char **p, const unsigned char *max, php_unserialize_data_t *var_hash);

 PHPAPI php_serialize_data_t php_var_serialize_init(void);
+PHPAPI void php_var_serialize_set_incomplete_classes_only(php_serialize_data_t d, bool incomplete_classes_only);
 PHPAPI void php_var_serialize_destroy(php_serialize_data_t d);
 PHPAPI php_unserialize_data_t php_var_unserialize_init(void);
 PHPAPI void php_var_unserialize_destroy(php_unserialize_data_t d);
 PHPAPI HashTable *php_var_unserialize_get_allowed_classes(php_unserialize_data_t d);
 PHPAPI void php_var_unserialize_set_allowed_classes(php_unserialize_data_t d, HashTable *classes);
+PHPAPI void php_var_unserialize_set_incomplete_classes_only(php_unserialize_data_t d, bool incomplete_classes_only);
 PHPAPI void php_var_unserialize_set_max_depth(php_unserialize_data_t d, zend_long max_depth);
 PHPAPI zend_long php_var_unserialize_get_max_depth(php_unserialize_data_t d);
 PHPAPI void php_var_unserialize_set_cur_depth(php_unserialize_data_t d, zend_long cur_depth);
diff --git a/ext/standard/string.c b/ext/standard/string.c
index cd23e67bdea..19b6e9d92ea 100644
--- a/ext/standard/string.c
+++ b/ext/standard/string.c
@@ -877,7 +877,10 @@ PHP_FUNCTION(wordwrap)
 	/* Special case for a single-character break as it needs no
 	   additional storage space */
 	if (breakchar_len == 1 && !docut) {
-		newtext = zend_string_init(ZSTR_VAL(text), ZSTR_LEN(text), 0);
+		newtext = zend_string_finit(ZSTR_VAL(text), ZSTR_LEN(text), 0, /* fallible */ true);
+		if (UNEXPECTED(!newtext)) {
+			RETURN_OOM();
+		}

 		laststart = lastspace = 0;
 		for (current = 0; current < (zend_long)ZSTR_LEN(text); current++) {
@@ -900,12 +903,18 @@ PHP_FUNCTION(wordwrap)
 		/* Multiple character line break or forced cut */
 		if (linelength > 0) {
 			chk = (size_t)(ZSTR_LEN(text)/linelength + 1);
-			newtext = zend_string_safe_alloc(chk, breakchar_len, ZSTR_LEN(text), 0);
+			newtext = zend_string_safe_falloc(chk, breakchar_len, ZSTR_LEN(text), 0, /* fallible */ true);
+			if (UNEXPECTED(!newtext)) {
+				RETURN_OOM();
+			}
 			alloced = ZSTR_LEN(text) + chk * breakchar_len + 1;
 		} else {
 			chk = ZSTR_LEN(text);
 			alloced = ZSTR_LEN(text) * (breakchar_len + 1) + 1;
-			newtext = zend_string_safe_alloc(ZSTR_LEN(text), breakchar_len + 1, 0, 0);
+			newtext = zend_string_safe_falloc(ZSTR_LEN(text), breakchar_len + 1, 0, 0, /* fallible */ true);
+			if (UNEXPECTED(!newtext)) {
+				RETURN_OOM();
+			}
 		}

 		/* now keep track of the actual new text length */
@@ -915,7 +924,10 @@ PHP_FUNCTION(wordwrap)
 		for (current = 0; current < (zend_long)ZSTR_LEN(text); current++) {
 			if (chk == 0) {
 				alloced += (size_t) (((ZSTR_LEN(text) - current + 1)/linelength + 1) * breakchar_len) + 1;
-				newtext = zend_string_extend(newtext, alloced, 0);
+				newtext = zend_string_fextend(newtext, alloced, 0, /* fallible */ true);
+				if (UNEXPECTED(!newtext)) {
+					RETURN_OOM();
+				}
 				chk = (size_t) ((ZSTR_LEN(text) - current)/linelength) + 1;
 			}
 			/* when we hit an existing break, copy to new buffer, and
@@ -976,7 +988,10 @@ PHP_FUNCTION(wordwrap)

 		ZSTR_VAL(newtext)[newtextlen] = '\0';
 		/* free unused memory */
-		newtext = zend_string_truncate(newtext, newtextlen, 0);
+		newtext = zend_string_ftruncate(newtext, newtextlen, 0, /* fallible */ true);
+		if (UNEXPECTED(!newtext)) {
+			RETURN_OOM();
+		}

 		RETURN_NEW_STR(newtext);
 	}
@@ -998,6 +1013,11 @@ PHPAPI void php_explode(const zend_string *delim, zend_string *str, zval *return
 		zend_hash_real_init_packed(Z_ARRVAL_P(return_value));
 		ZEND_HASH_FILL_PACKED(Z_ARRVAL_P(return_value)) {
 			do {
+				/* Each piece costs a zval plus a zend_string, so stop once over the limit. */
+				if (ZEND_HAS_OOM()) {
+					ZEND_HASH_FILL_FINISH();
+					return;
+				}
 				ZEND_HASH_FILL_GROW();
 				ZEND_HASH_FILL_SET_STR(zend_string_init_fast(p1, p2 - p1));
 				ZEND_HASH_FILL_NEXT();
@@ -1036,6 +1056,11 @@ PHPAPI void php_explode_negative_limit(const zend_string *delim, zend_string *st

 		positions[found++] = p1;
 		do {
+			/* One pointer per delimiter, then a zval and a zend_string per piece. */
+			if (ZEND_HAS_OOM()) {
+				efree((void *)positions);
+				return;
+			}
 			if (found >= allocated) {
 				allocated = found + EXPLODE_ALLOC_STEP;/* make sure we have enough memory */
 				positions = erealloc(ZEND_VOIDP(positions), allocated*sizeof(char *));
@@ -1047,6 +1072,9 @@ PHPAPI void php_explode_negative_limit(const zend_string *delim, zend_string *st
 		to_return = limit + found;
 		/* limit is at least -1 therefore no need of bounds checking : i will be always less than found */
 		for (i = 0; i < to_return; i++) { /* this checks also for to_return > 0 */
+			if (ZEND_HAS_OOM()) {
+				break;
+			}
 			ZVAL_STRINGL(&tmp, positions[i], (positions[i+1] - ZSTR_LEN(delim)) - positions[i]);
 			zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &tmp);
 		}
@@ -1154,7 +1182,18 @@ PHPAPI void php_implode(const zend_string *glue, HashTable *pieces, zval *return
 	} ZEND_HASH_FOREACH_END();

 	/* numelems cannot be 0, we checked above */
-	str = zend_string_safe_alloc(numelems - 1, ZSTR_LEN(glue), len, 0);
+	/* The glue is repeated once per element, so the result can dwarf the input. */
+	str = zend_string_safe_falloc(numelems - 1, ZSTR_LEN(glue), len, 0, /* fallible */ true);
+	if (UNEXPECTED(!str)) {
+		while (ptr > strings) {
+			ptr--;
+			if (ptr->str && ptr->lval) {
+				zend_string_release_ex(ptr->str, 0);
+			}
+		}
+		free_alloca(strings, use_heap);
+		RETURN_OOM();
+	}
 	cptr = ZSTR_VAL(str) + ZSTR_LEN(str);
 	*cptr = 0;

@@ -2023,7 +2062,10 @@ static zend_string *php_chunk_split(const char *src, size_t srclen, const char *
 		chunks++;
 	}

-	dest = zend_string_safe_alloc(chunks, endlen, srclen, 0);
+	dest = zend_string_safe_falloc(chunks, endlen, srclen, 0, /* fallible */ true);
+	if (UNEXPECTED(!dest)) {
+		return NULL;
+	}

 	for (p = src, q = ZSTR_VAL(dest); p < (src + srclen - chunklen + 1); ) {
 		memcpy(q, p, chunklen);
@@ -2070,7 +2112,10 @@ PHP_FUNCTION(chunk_split)

 	if ((size_t)chunklen > ZSTR_LEN(str)) {
 		/* to maintain BC, we must return original string + ending */
-		result = zend_string_safe_alloc(ZSTR_LEN(str), 1, endlen, 0);
+		result = zend_string_safe_falloc(ZSTR_LEN(str), 1, endlen, 0, /* fallible */ true);
+		if (UNEXPECTED(!result)) {
+			RETURN_OOM();
+		}
 		memcpy(ZSTR_VAL(result), ZSTR_VAL(str), ZSTR_LEN(str));
 		memcpy(ZSTR_VAL(result) + ZSTR_LEN(str), end, endlen);
 		ZSTR_VAL(result)[ZSTR_LEN(result)] = '\0';
@@ -2082,6 +2127,9 @@ PHP_FUNCTION(chunk_split)
 	}

 	result = php_chunk_split(ZSTR_VAL(str), ZSTR_LEN(str), end, endlen, (size_t)chunklen);
+	if (UNEXPECTED(!result)) {
+		RETURN_OOM();
+	}

 	RETURN_STR(result);
 }
@@ -2269,7 +2317,13 @@ PHP_FUNCTION(substr_replace)

 		ZEND_HASH_FOREACH_KEY_VAL(str_ht, num_index, str_index, tmp_str) {
 			zend_string *tmp_orig_str;
-			zend_string *orig_str = zval_get_tmp_string(tmp_str, &tmp_orig_str);
+			zend_string *orig_str;
+
+			/* The replacement is repeated once per element, so stop once over the limit. */
+			if (ZEND_HAS_OOM()) {
+				RETURN_OOM();
+			}
+			orig_str = zval_get_tmp_string(tmp_str, &tmp_orig_str);

 			if (from_ht) {
 				if (HT_IS_PACKED(from_ht)) {
@@ -2824,6 +2878,15 @@ static void php_strtr_array(zval *return_value, zend_string *input, HashTable *p

 	old_pos = pos = 0;
 	while (pos <= slen - minlen) {
+		/* One replacement is appended per match, so stop once over the limit. */
+		if (ZEND_HAS_OOM()) {
+			smart_str_free(&result);
+			if (pats == &str_hash) {
+				zend_hash_destroy(&str_hash);
+			}
+			efree(num_bitset);
+			RETURN_OOM();
+		}
 		key = str + pos;
 		if (bitset[((unsigned char)key[0]) / sizeof(zend_ulong)] & (Z_UL(1) << (((unsigned char)key[0]) % sizeof(zend_ulong)))) {
 			len = maxlen;
@@ -2944,7 +3007,11 @@ static zend_string* php_char_to_str_ex(zend_string *str, char from, char *to, si
 	}

 	if (to_len > 0) {
-		result = zend_string_safe_alloc(char_count, to_len - 1, ZSTR_LEN(str), 0);
+		/* The replacement is repeated once per match, so the result can dwarf the input. */
+		result = zend_string_safe_falloc(char_count, to_len - 1, ZSTR_LEN(str), 0, /* fallible */ true);
+		if (UNEXPECTED(!result)) {
+			return NULL;
+		}
 	} else {
 		result = zend_string_alloc(ZSTR_LEN(str) - char_count, 0);
 	}
@@ -3023,7 +3090,10 @@ static zend_string *php_str_to_str_ex(zend_string *haystack,
 				goto nothing_todo;
 			}
 			if (str_len > needle_len) {
-				new_str = zend_string_safe_alloc(count, str_len - needle_len, ZSTR_LEN(haystack), 0);
+				new_str = zend_string_safe_falloc(count, str_len - needle_len, ZSTR_LEN(haystack), 0, /* fallible */ true);
+				if (UNEXPECTED(!new_str)) {
+					return NULL;
+				}
 			} else {
 				new_str = zend_string_alloc(count * (str_len - needle_len) + ZSTR_LEN(haystack), 0);
 			}
@@ -3104,7 +3174,10 @@ static zend_string *php_str_to_str_i_ex(zend_string *haystack, const char *lc_ha
 			}

 			if (str_len > ZSTR_LEN(lc_needle)) {
-				new_str = zend_string_safe_alloc(count, str_len - ZSTR_LEN(lc_needle), ZSTR_LEN(haystack), 0);
+				new_str = zend_string_safe_falloc(count, str_len - ZSTR_LEN(lc_needle), ZSTR_LEN(haystack), 0, /* fallible */ true);
+				if (UNEXPECTED(!new_str)) {
+					return NULL;
+				}
 			} else {
 				new_str = zend_string_alloc(count * (str_len - ZSTR_LEN(lc_needle)) + ZSTR_LEN(haystack), 0);
 			}
@@ -3268,21 +3341,34 @@ PHP_FUNCTION(strtr)
 					php_error_docref(NULL, E_WARNING, "Ignoring replacement of empty string");
 					RETVAL_STR_COPY(str);
 				} else if (ZSTR_LEN(str_key) == 1) {
-					RETVAL_STR(php_char_to_str_ex(str,
+					zend_string *res = php_char_to_str_ex(str,
 								ZSTR_VAL(str_key)[0],
 								ZSTR_VAL(replace),
 								ZSTR_LEN(replace),
 								/* case_sensitive */ true,
-								NULL));
+								NULL);
+					if (UNEXPECTED(!res)) {
+						goto strtr_oom;
+					}
+					RETVAL_STR(res);
 				} else {
 					zend_long dummy = 0;
-					RETVAL_STR(php_str_to_str_ex(str,
+					zend_string *res = php_str_to_str_ex(str,
 								ZSTR_VAL(str_key), ZSTR_LEN(str_key),
-								ZSTR_VAL(replace), ZSTR_LEN(replace), &dummy));
+								ZSTR_VAL(replace), ZSTR_LEN(replace), &dummy);
+					if (UNEXPECTED(!res)) {
+						goto strtr_oom;
+					}
+					RETVAL_STR(res);
 				}
 				zend_tmp_string_release(tmp_str);
 				zend_tmp_string_release(tmp_replace);
 				return;
+
+strtr_oom:
+				zend_tmp_string_release(tmp_str);
+				zend_tmp_string_release(tmp_replace);
+				RETURN_OOM();
 			} ZEND_HASH_FOREACH_END();
 		} else {
 			php_strtr_array(return_value, str, from_ht);
@@ -4237,6 +4323,15 @@ static zend_long php_str_replace_in_subject(
 			zend_tmp_string_release(tmp_search_str);
 			zend_tmp_string_release(tmp_replace_entry_str);

+			if (UNEXPECTED(!tmp_result)) {
+				zend_string_release_ex(subject_str, 0);
+				ZVAL_EMPTY_STRING(result);
+				if (lc_subject_str) {
+					zend_string_release_ex(lc_subject_str, 0);
+				}
+				return replace_count;
+			}
+
 			if (subject_str == tmp_result) {
 				zend_string_delref(subject_str);
 			} else {
@@ -4259,24 +4354,33 @@ static zend_long php_str_replace_in_subject(
 	} else {
 		ZEND_ASSERT(search_str);
 		if (ZSTR_LEN(search_str) == 1) {
-			ZVAL_STR(result,
-				php_char_to_str_ex(subject_str,
+			tmp_result = php_char_to_str_ex(subject_str,
 							ZSTR_VAL(search_str)[0],
 							ZSTR_VAL(replace_str),
 							ZSTR_LEN(replace_str),
 							case_sensitivity,
-							&replace_count));
+							&replace_count);
+			if (UNEXPECTED(!tmp_result)) {
+				ZVAL_EMPTY_STRING(result);
+				return replace_count;
+			}
+			ZVAL_STR(result, tmp_result);
 		} else if (ZSTR_LEN(search_str) > 1) {
 			if (case_sensitivity) {
-				ZVAL_STR(result, php_str_to_str_ex(subject_str,
+				tmp_result = php_str_to_str_ex(subject_str,
 						ZSTR_VAL(search_str), ZSTR_LEN(search_str),
-						ZSTR_VAL(replace_str), ZSTR_LEN(replace_str), &replace_count));
+						ZSTR_VAL(replace_str), ZSTR_LEN(replace_str), &replace_count);
 			} else {
 				lc_subject_str = zend_string_tolower(subject_str);
-				ZVAL_STR(result, php_str_to_str_i_ex(subject_str, ZSTR_VAL(lc_subject_str),
-						search_str, ZSTR_VAL(replace_str), ZSTR_LEN(replace_str), &replace_count));
+				tmp_result = php_str_to_str_i_ex(subject_str, ZSTR_VAL(lc_subject_str),
+						search_str, ZSTR_VAL(replace_str), ZSTR_LEN(replace_str), &replace_count);
 				zend_string_release_ex(lc_subject_str, 0);
 			}
+			if (UNEXPECTED(!tmp_result)) {
+				ZVAL_EMPTY_STRING(result);
+				return replace_count;
+			}
+			ZVAL_STR(result, tmp_result);
 		} else {
 			ZVAL_STR_COPY(result, subject_str);
 		}
@@ -4565,7 +4669,10 @@ PHP_FUNCTION(nl2br)
 	{
 		size_t repl_len = is_xhtml ? (sizeof("<br />") - 1) : (sizeof("<br>") - 1);

-		result = zend_string_safe_alloc(repl_cnt, repl_len, ZSTR_LEN(str), 0);
+		result = zend_string_safe_falloc(repl_cnt, repl_len, ZSTR_LEN(str), 0, /* fallible */ true);
+		if (UNEXPECTED(!result)) {
+			RETURN_OOM();
+		}
 		target = ZSTR_VAL(result);
 	}

@@ -5258,7 +5365,11 @@ PHP_FUNCTION(str_repeat)
 		RETURN_EMPTY_STRING();

 	/* Initialize the result string */
-	result = zend_string_safe_alloc(ZSTR_LEN(input_str), mult, 0, 0);
+	result = zend_string_safe_falloc(ZSTR_LEN(input_str), mult, 0, /*persistent */ false, /* fallible */ true);
+	if (UNEXPECTED(!result)) {
+		RETURN_OOM();
+	}
+
 	result_len = ZSTR_LEN(input_str) * mult;

 	/* Heavy optimization for situations where input string is 1 byte long */
@@ -5544,7 +5655,10 @@ PHP_FUNCTION(str_pad)
 	}

 	num_pad_chars = pad_length - ZSTR_LEN(input);
-	result = zend_string_safe_alloc(1, ZSTR_LEN(input), num_pad_chars, 0);
+	result = zend_string_safe_falloc(1, ZSTR_LEN(input), num_pad_chars, 0, /* falllible */ true);
+	if (UNEXPECTED(!result)) {
+		RETURN_OOM();
+	}
 	ZSTR_LEN(result) = 0;

 	/* We need to figure out the left/right padding lengths. */
@@ -5824,6 +5938,10 @@ PHP_FUNCTION(str_word_count)
 	}

 	while (p < e) {
+		/* Each word costs a zval plus a zend_string, so stop once over the limit. */
+		if (type && ZEND_HAS_OOM()) {
+			RETURN_OOM();
+		}
 		s = p;
 		while (p < e && (isalpha((unsigned char)*p) || (char_list && ch[(unsigned char)*p]) || *p == '\'' || *p == '-')) {
 			p++;
@@ -5881,12 +5999,18 @@ PHP_FUNCTION(str_split)
 		return;
 	}

-	array_init_size(return_value, (uint32_t)(((ZSTR_LEN(str) - 1) / split_length) + 1));
+	if (UNEXPECTED(!array_finit_size_packed(return_value, (uint32_t)(((ZSTR_LEN(str) - 1) / split_length) + 1)))) {
+		RETURN_OOM();
+	}

 	n_reg_segments = ZSTR_LEN(str) / split_length;
 	p = ZSTR_VAL(str);

 	while (n_reg_segments-- > 0) {
+		/* Each segment costs a zval plus a zend_string, so stop once over the limit. */
+		if (ZEND_HAS_OOM()) {
+			RETURN_OOM();
+		}
 		add_next_index_stringl(return_value, p, split_length);
 		p += split_length;
 	}
diff --git a/ext/standard/tests/GHSA-96wq-48vp-hh57.phpt b/ext/standard/tests/GHSA-96wq-48vp-hh57.phpt
index cf9a40062f8..8d8364bfb10 100644
--- a/ext/standard/tests/GHSA-96wq-48vp-hh57.phpt
+++ b/ext/standard/tests/GHSA-96wq-48vp-hh57.phpt
@@ -8,7 +8,8 @@
 <?php
 if (!getenv('RUN_RESOURCE_HEAVY_TESTS')) die('skip resource-heavy test');
 if (getenv('SKIP_SLOW_TESTS')) die('skip slow test');
-if (PHP_INT_SIZE != 8) echo 'skip 64-bit only';
+if (PHP_INT_SIZE != 8) die('skip 64-bit only');
+if (PHP_DEBUG) die('skip Times out in debug builds');
 ?>
 --FILE--
 <?php
diff --git a/ext/standard/tests/array/oom_array_chunk.phpt b/ext/standard/tests/array/oom_array_chunk.phpt
new file mode 100644
index 00000000000..b16740dce57
--- /dev/null
+++ b/ext/standard/tests/array/oom_array_chunk.phpt
@@ -0,0 +1,12 @@
+--TEST--
+array_chunk() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$input = array_fill(0, 250000, 0);
+array_chunk($input, 1);
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/array/oom_array_map_zip.phpt b/ext/standard/tests/array/oom_array_map_zip.phpt
new file mode 100644
index 00000000000..9fae0e32d7d
--- /dev/null
+++ b/ext/standard/tests/array/oom_array_map_zip.phpt
@@ -0,0 +1,12 @@
+--TEST--
+array_map() without a callback doesn't reach hard limit
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+$input = array_fill(0, 250000, 0);
+array_map(null, $input, $input);
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/file/fread_oom_chunk_size.phpt b/ext/standard/tests/file/fread_oom_chunk_size.phpt
new file mode 100644
index 00000000000..fa8abc7363d
--- /dev/null
+++ b/ext/standard/tests/file/fread_oom_chunk_size.phpt
@@ -0,0 +1,16 @@
+--TEST--
+fread() with a large chunk size doesn't reach hard limit
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+$fp = fopen(__FILE__, 'rb');
+stream_set_chunk_size($fp, 100 * 1024 * 1024);
+var_dump(fread($fp, 1));
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/file/fread_oom_filter_chunk_size.phpt b/ext/standard/tests/file/fread_oom_filter_chunk_size.phpt
new file mode 100644
index 00000000000..5c1fa6d833a
--- /dev/null
+++ b/ext/standard/tests/file/fread_oom_filter_chunk_size.phpt
@@ -0,0 +1,17 @@
+--TEST--
+fread() with a filter and a large chunk size doesn't reach hard limit
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+$fp = fopen(__FILE__, 'rb');
+stream_filter_append($fp, "string.rot13");
+stream_set_chunk_size($fp, 100 * 1024 * 1024);
+var_dump(fread($fp, 1));
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/file/fread_oom_length.phpt b/ext/standard/tests/file/fread_oom_length.phpt
new file mode 100644
index 00000000000..68693107fc4
--- /dev/null
+++ b/ext/standard/tests/file/fread_oom_length.phpt
@@ -0,0 +1,15 @@
+--TEST--
+fread() with a large length doesn't reach hard limit
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+$fp = fopen(__FILE__, 'rb');
+var_dump(fread($fp, 100 * 1024 * 1024));
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/file/oom_file.phpt b/ext/standard/tests/file/oom_file.phpt
new file mode 100644
index 00000000000..b3bc4111e79
--- /dev/null
+++ b/ext/standard/tests/file/oom_file.phpt
@@ -0,0 +1,22 @@
+--TEST--
+file() doesn't reach hard limit
+--INI--
+memory_limit=64M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+$path = __DIR__ . '/oom_file.txt';
+file_put_contents($path, str_repeat("a\n", 1024 * 1024));
+// Each one byte line becomes a zval plus a zend_string.
+ini_set('memory_limit', '8M');
+var_dump(file($path));
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/oom_file.txt");
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/file/oom_file_get_contents.phpt b/ext/standard/tests/file/oom_file_get_contents.phpt
new file mode 100644
index 00000000000..2864f44e011
--- /dev/null
+++ b/ext/standard/tests/file/oom_file_get_contents.phpt
@@ -0,0 +1,21 @@
+--TEST--
+file_get_contents() doesn't reach hard limit
+--INI--
+memory_limit=64M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+$path = __DIR__ . '/oom_file_get_contents.txt';
+file_put_contents($path, str_repeat('x', 12 * 1024 * 1024));
+ini_set('memory_limit', '4M');
+var_dump(file_get_contents($path));
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/oom_file_get_contents.txt");
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/image/oom_getimagesize_cws.inc b/ext/standard/tests/image/oom_getimagesize_cws.inc
new file mode 100644
index 00000000000..157b9f8fe52
--- /dev/null
+++ b/ext/standard/tests/image/oom_getimagesize_cws.inc
@@ -0,0 +1,17 @@
+<?php
+
+function generate_file(string $path) {
+    $file = fopen($path, 'wb');
+    $uncompressedSize = 320 * 1024 * 1024;
+    fwrite($file, "CWS\x06" . pack('V', $uncompressedSize + 8));
+    $deflate = deflate_init(ZLIB_ENCODING_DEFLATE, ['level' => 9]);
+    $chunk = str_repeat('A', 1024 * 1024);
+    for ($i = 0; $i < 320; $i++) {
+        fwrite($file, deflate_add(
+            $deflate,
+            $chunk,
+            $i === 319 ? ZLIB_FINISH : ZLIB_NO_FLUSH,
+        ));
+    }
+    fclose($file);
+}
diff --git a/ext/standard/tests/image/oom_getimagesize_cws.phpt b/ext/standard/tests/image/oom_getimagesize_cws.phpt
new file mode 100644
index 00000000000..79d8b9abc62
--- /dev/null
+++ b/ext/standard/tests/image/oom_getimagesize_cws.phpt
@@ -0,0 +1,27 @@
+--TEST--
+getimagesize() decompression doesn't reach hard limit
+--INI--
+memory_limit=128M
+--SKIPIF--
+<?php
+$php = getenv('TEST_PHP_EXECUTABLE');
+
+/* CWS support in ext/standard is only enabled when zlib is built in. */
+$process = proc_open([$php, '-n', '-r', 'exit(extension_loaded("zlib") ? 0 : 1);'], [STDIN, STDOUT, STDERR], $pipes);
+if (proc_close($process) !== 0) {
+    die('skip built-in zlib not available');
+}
+?>
+--FILE--
+<?php
+require __DIR__ . '/oom_getimagesize_cws.inc';
+$path = __DIR__ . '/oom_getimagesize_cws.swf';
+generate_file($path);
+getimagesize($path);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/oom_getimagesize_cws.swf');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 134217728 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/image/oom_getimagesize_xbm.phpt b/ext/standard/tests/image/oom_getimagesize_xbm.phpt
new file mode 100644
index 00000000000..292ece12c2a
--- /dev/null
+++ b/ext/standard/tests/image/oom_getimagesize_xbm.phpt
@@ -0,0 +1,23 @@
+--TEST--
+getimagesize() doesn't reach hard limit on a file without newlines
+--INI--
+memory_limit=64M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+$path = __DIR__ . '/oom_getimagesize_xbm.txt';
+file_put_contents($path, str_repeat('x', 12 * 1024 * 1024));
+
+// The XBM probe reads a whole line, then duplicates it.
+ini_set('memory_limit', '4M');
+var_dump(getimagesize($path));
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/oom_getimagesize_xbm.txt");
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/image/oom_getimagesizefromstring_cws.phpt b/ext/standard/tests/image/oom_getimagesizefromstring_cws.phpt
new file mode 100644
index 00000000000..5f7ead31100
--- /dev/null
+++ b/ext/standard/tests/image/oom_getimagesizefromstring_cws.phpt
@@ -0,0 +1,27 @@
+--TEST--
+getimagesizefromstring() decompression doesn't reach hard limit
+--INI--
+memory_limit=128M
+--SKIPIF--
+<?php
+$php = getenv('TEST_PHP_EXECUTABLE');
+
+/* CWS support in ext/standard is only enabled when zlib is built in. */
+$process = proc_open([$php, '-n', '-r', 'exit(extension_loaded("zlib") ? 0 : 1);'], [STDIN, STDOUT, STDERR], $pipes);
+if (proc_close($process) !== 0) {
+    die('skip built-in zlib not available');
+}
+?>
+--FILE--
+<?php
+require __DIR__ . '/oom_getimagesize_cws.inc';
+$path = __DIR__ . '/oom_getimagesizefromstring_cws.swf';
+generate_file($path);
+getimagesizefromstring(file_get_contents($path));
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/oom_getimagesizefromstring_cws.swf');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 134217728 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/serialize/incomplete_class_raw_payload_markers.phpt b/ext/standard/tests/serialize/incomplete_class_raw_payload_markers.phpt
new file mode 100644
index 00000000000..3c175fd202e
--- /dev/null
+++ b/ext/standard/tests/serialize/incomplete_class_raw_payload_markers.phpt
@@ -0,0 +1,15 @@
+--TEST--
+serialize() keeps O: for incomplete classes whose properties look like raw C:/E: payloads
+--FILE--
+<?php
+enum Suit { case Hearts; }
+
+$raw = 'x:i:0;a:1:{i:0;s:3:"pwn";};m:a:0:{}';
+$untrusted = 'a:2:{i:0;O:11:"ArrayObject":1:{s:33:"__PHP_Incomplete_Class_Serialized";s:' . strlen($raw) . ':"' . $raw . '";}'
+    . 'i:1;O:4:"Suit":1:{s:26:"__PHP_Incomplete_Enum_Case";s:6:"Hearts";}}';
+
+$stored = serialize(unserialize($untrusted, ['allowed_classes' => false]));
+var_dump($stored === $untrusted);
+?>
+--EXPECT--
+bool(true)
diff --git a/ext/standard/tests/streams/bug61115-1.phpt b/ext/standard/tests/streams/bug61115-1.phpt
index 892b0f80e57..baf847be061 100644
--- a/ext/standard/tests/streams/bug61115-1.phpt
+++ b/ext/standard/tests/streams/bug61115-1.phpt
@@ -5,6 +5,9 @@
 if (getenv("USE_ZEND_ALLOC") === "0") {
     die("skip Zend MM disabled");
 }
+if (PHP_ZTS) {
+    die("skip No hard limit in ZTS");
+}
 ?>
 --FILE--
 <?php
diff --git a/ext/standard/tests/streams/oom_stream_copy_to_stream_memory.phpt b/ext/standard/tests/streams/oom_stream_copy_to_stream_memory.phpt
new file mode 100644
index 00000000000..034d38ae73e
--- /dev/null
+++ b/ext/standard/tests/streams/oom_stream_copy_to_stream_memory.phpt
@@ -0,0 +1,22 @@
+--TEST--
+stream_copy_to_stream() into php://memory doesn't reach hard limit
+--INI--
+memory_limit=64M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+$path = __DIR__ . '/oom_stream_copy_to_stream_memory.txt';
+file_put_contents($path, str_repeat('x', 12 * 1024 * 1024));
+$dest = fopen('php://memory', 'r+');
+ini_set('memory_limit', '4M');
+var_dump(stream_copy_to_stream(fopen($path, 'rb'), $dest));
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/oom_stream_copy_to_stream_memory.txt");
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/streams/oom_stream_get_contents.phpt b/ext/standard/tests/streams/oom_stream_get_contents.phpt
new file mode 100644
index 00000000000..415750aaa9a
--- /dev/null
+++ b/ext/standard/tests/streams/oom_stream_get_contents.phpt
@@ -0,0 +1,21 @@
+--TEST--
+stream_get_contents() doesn't reach hard limit
+--INI--
+memory_limit=64M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+$path = __DIR__ . '/oom_stream_get_contents.txt';
+file_put_contents($path, str_repeat('x', 12 * 1024 * 1024));
+ini_set('memory_limit', '4M');
+var_dump(stream_get_contents(fopen($path, 'rb')));
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/oom_stream_get_contents.txt");
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/streams/stream_get_line_oom_chunk_size.phpt b/ext/standard/tests/streams/stream_get_line_oom_chunk_size.phpt
new file mode 100644
index 00000000000..faf201a6509
--- /dev/null
+++ b/ext/standard/tests/streams/stream_get_line_oom_chunk_size.phpt
@@ -0,0 +1,16 @@
+--TEST--
+stream_get_line() with a large chunk size doesn't reach hard limit
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+$fp = fopen(__FILE__, 'rb');
+stream_set_chunk_size($fp, 100 * 1024 * 1024);
+var_dump(stream_get_line($fp, 1024, "\n"));
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/strings/chunk_split_variation3.phpt b/ext/standard/tests/strings/chunk_split_variation3.phpt
index 56c524ef02d..049ee3db0a3 100644
--- a/ext/standard/tests/strings/chunk_split_variation3.phpt
+++ b/ext/standard/tests/strings/chunk_split_variation3.phpt
@@ -1,24 +1,18 @@
 --TEST--
-Test chunk_split() function : usage variations - unexpected large number of chunks
+Test chunk_split() function : usage variations - OOM with chunk length larger than str
 --SKIPIF--
 <?php
 if (getenv("USE_ZEND_ALLOC") === "0") die("skip ZMM is disabled");
 ?>
+--INI--
+memory_limit=128M
 --FILE--
 <?php
-$chunk_length = 1;

-echo "*** Testing chunk_split() : unexpected large 'end' string argument variation 2 ***\n";
+$str = str_repeat('a', 100*1024*1024);

-echo "Body generation\n";
-$body = str_repeat("Hello", 10000000);
+var_dump(chunk_split($str, 1024*1024*1024, $str));

-echo "Using chunk_split()\n";
-var_dump(chunk_split($body, $chunk_length));
 ?>
 --EXPECTF--
-*** Testing chunk_split() : unexpected large 'end' string argument variation 2 ***
-Body generation
-Using chunk_split()
-
-Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d
+Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line 5
diff --git a/ext/standard/tests/strings/oom_explode.phpt b/ext/standard/tests/strings/oom_explode.phpt
new file mode 100644
index 00000000000..35217e6728b
--- /dev/null
+++ b/ext/standard/tests/strings/oom_explode.phpt
@@ -0,0 +1,17 @@
+--TEST--
+explode() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+// Each piece costs a zval plus a zend_string, so the result is many times
+// the size of the input.
+$s = str_repeat('a,', 512*1024);
+var_dump(explode(',', $s));
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/strings/oom_implode.phpt b/ext/standard/tests/strings/oom_implode.phpt
new file mode 100644
index 00000000000..27131b93141
--- /dev/null
+++ b/ext/standard/tests/strings/oom_implode.phpt
@@ -0,0 +1,15 @@
+--TEST--
+implode() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+// The glue is repeated once per element.
+var_dump(implode(str_repeat('-', 1024*1024), array_fill(0, 32, 'x')));
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/strings/oom_levenshtein.phpt b/ext/standard/tests/strings/oom_levenshtein.phpt
new file mode 100644
index 00000000000..53a6e00b9ce
--- /dev/null
+++ b/ext/standard/tests/strings/oom_levenshtein.phpt
@@ -0,0 +1,15 @@
+--TEST--
+levenshtein() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+// Each workspace row is eight times the length of the second string.
+var_dump(levenshtein('a', str_repeat('b', 2*1024*1024)));
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/strings/oom_nl2br.phpt b/ext/standard/tests/strings/oom_nl2br.phpt
new file mode 100644
index 00000000000..c0d6cf71009
--- /dev/null
+++ b/ext/standard/tests/strings/oom_nl2br.phpt
@@ -0,0 +1,15 @@
+--TEST--
+nl2br() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+// Every newline expands to six bytes.
+var_dump(nl2br(str_repeat("\n", 2 * 1024 * 1024)));
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/strings/oom_setcookie_value.phpt b/ext/standard/tests/strings/oom_setcookie_value.phpt
new file mode 100644
index 00000000000..a955071ff49
--- /dev/null
+++ b/ext/standard/tests/strings/oom_setcookie_value.phpt
@@ -0,0 +1,15 @@
+--TEST--
+setcookie() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+// The value is URL encoded to three times its size, then copied into the header.
+var_dump(setcookie("c", str_repeat("\xff", 2 * 1024 * 1024)));
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/strings/oom_str_getcsv.phpt b/ext/standard/tests/strings/oom_str_getcsv.phpt
new file mode 100644
index 00000000000..24a9dde093e
--- /dev/null
+++ b/ext/standard/tests/strings/oom_str_getcsv.phpt
@@ -0,0 +1,15 @@
+--TEST--
+str_getcsv() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+// Each field costs a zval plus a zend_string.
+var_dump(str_getcsv(str_repeat('a,', 512*1024)));
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/strings/oom_str_ireplace.phpt b/ext/standard/tests/strings/oom_str_ireplace.phpt
new file mode 100644
index 00000000000..f4984485a1e
--- /dev/null
+++ b/ext/standard/tests/strings/oom_str_ireplace.phpt
@@ -0,0 +1,15 @@
+--TEST--
+str_ireplace() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+// The replacement is repeated once per match.
+var_dump(str_ireplace('A', str_repeat('b', 16), str_repeat('a', 1024*1024)));
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/strings/oom_str_replace.phpt b/ext/standard/tests/strings/oom_str_replace.phpt
new file mode 100644
index 00000000000..97689a57ff3
--- /dev/null
+++ b/ext/standard/tests/strings/oom_str_replace.phpt
@@ -0,0 +1,15 @@
+--TEST--
+str_replace() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+// The replacement is repeated once per match.
+var_dump(str_replace('a', str_repeat('b', 16), str_repeat('a', 1024*1024)));
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/strings/oom_str_split.phpt b/ext/standard/tests/strings/oom_str_split.phpt
new file mode 100644
index 00000000000..dfe375b44e4
--- /dev/null
+++ b/ext/standard/tests/strings/oom_str_split.phpt
@@ -0,0 +1,17 @@
+--TEST--
+str_split() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+// Each piece costs a zval plus a zend_string, so the result is many times
+// the size of the input.
+$s = str_repeat('a', 1024*1024);
+var_dump(str_split($s, 1));
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/strings/oom_str_word_count.phpt b/ext/standard/tests/strings/oom_str_word_count.phpt
new file mode 100644
index 00000000000..0dc54905309
--- /dev/null
+++ b/ext/standard/tests/strings/oom_str_word_count.phpt
@@ -0,0 +1,17 @@
+--TEST--
+str_word_count() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+// Each piece costs a zval plus a zend_string, so the result is many times
+// the size of the input.
+$s = str_repeat('ab ', 350*1024);
+var_dump(str_word_count($s, 1));
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/strings/oom_strtr.phpt b/ext/standard/tests/strings/oom_strtr.phpt
new file mode 100644
index 00000000000..bf09d6bb558
--- /dev/null
+++ b/ext/standard/tests/strings/oom_strtr.phpt
@@ -0,0 +1,15 @@
+--TEST--
+strtr() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+// The replacement is repeated once per match.
+var_dump(strtr(str_repeat('a', 1024*1024), ['a' => str_repeat('b', 16)]));
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/strings/oom_substr_replace.phpt b/ext/standard/tests/strings/oom_substr_replace.phpt
new file mode 100644
index 00000000000..f7ff468e42a
--- /dev/null
+++ b/ext/standard/tests/strings/oom_substr_replace.phpt
@@ -0,0 +1,15 @@
+--TEST--
+substr_replace() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+// The replacement is repeated once per array element.
+var_dump(substr_replace(array_fill(0, 16, 'x'), str_repeat('y', 2*1024*1024), 0, 1));
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/strings/oom_unpack.phpt b/ext/standard/tests/strings/oom_unpack.phpt
new file mode 100644
index 00000000000..f44c221a3bd
--- /dev/null
+++ b/ext/standard/tests/strings/oom_unpack.phpt
@@ -0,0 +1,17 @@
+--TEST--
+unpack() doesn't reach hard limit
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+// Each piece costs a zval plus a zend_string, so the result is many times
+// the size of the input.
+$s = str_repeat('a', 1024*1024);
+var_dump(unpack('C*', $s));
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/strings/str_pad_variation1.phpt b/ext/standard/tests/strings/str_pad_variation1.phpt
index cb71e61156f..1b075448f2f 100644
--- a/ext/standard/tests/strings/str_pad_variation1.phpt
+++ b/ext/standard/tests/strings/str_pad_variation1.phpt
@@ -33,4 +33,4 @@
 *** Testing str_pad() function: with large value for for 'pad_length' argument ***
 str_pad(): Argument #2 ($length) must be of type int, float given

-Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d
+Fatal error: Allowed memory size of 134217728 bytes exhausted%s(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/standard/tests/strings/wordwrap_memory_limit_002.phpt b/ext/standard/tests/strings/wordwrap_memory_limit_002.phpt
new file mode 100644
index 00000000000..4f926703ecc
--- /dev/null
+++ b/ext/standard/tests/strings/wordwrap_memory_limit_002.phpt
@@ -0,0 +1,19 @@
+--TEST--
+wordwrap() OOM - single character break
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") {
+    die("skip Zend MM disabled");
+}
+?>
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+$str = str_repeat('a', 6*1024*1024);
+var_dump(wordwrap($str, 80, "\n", false));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted%s(tried to allocate %d bytes) in %s on line 4
diff --git a/ext/standard/tests/strings/wordwrap_memory_limit_003.phpt b/ext/standard/tests/strings/wordwrap_memory_limit_003.phpt
new file mode 100644
index 00000000000..01b6d6705bb
--- /dev/null
+++ b/ext/standard/tests/strings/wordwrap_memory_limit_003.phpt
@@ -0,0 +1,19 @@
+--TEST--
+wordwrap() OOM - multi character break
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") {
+    die("skip Zend MM disabled");
+}
+?>
+--INI--
+memory_limit=10M
+--FILE--
+<?php
+
+$str = str_repeat('a', 6*1024*1024);
+var_dump(wordwrap($str, 80, "\r\n", false));
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted%s(tried to allocate %d bytes) in %s on line 4
diff --git a/ext/standard/tests/strings/wordwrap_memory_limit_004.phpt b/ext/standard/tests/strings/wordwrap_memory_limit_004.phpt
new file mode 100644
index 00000000000..3ff71e5a982
--- /dev/null
+++ b/ext/standard/tests/strings/wordwrap_memory_limit_004.phpt
@@ -0,0 +1,21 @@
+--TEST--
+wordwrap() OOM - extend
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") {
+    die("skip Zend MM disabled");
+}
+?>
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+
+// Existing break chars in str cause the initialize allocation to be too short.
+// OOM while extending string.
+$str = str_repeat("\r\n", 2*1024*1024);
+wordwrap($str, strlen($str), "\r\n", false);
+
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted%s(tried to allocate %d bytes) in %s on line 6
diff --git a/ext/standard/url_scanner_ex.h b/ext/standard/url_scanner_ex.h
index 2e469ccb47f..aeb2f6c36d9 100644
--- a/ext/standard/url_scanner_ex.h
+++ b/ext/standard/url_scanner_ex.h
@@ -22,6 +22,7 @@ PHP_MSHUTDOWN_FUNCTION(url_scanner_ex);

 PHP_RINIT_FUNCTION(url_scanner_ex);
 PHP_RSHUTDOWN_FUNCTION(url_scanner_ex);
+void php_url_scanner_early_rinit(void);

 PHPAPI char *php_url_scanner_adapt_single_url(const char *url, size_t urllen, const char *name, const char *value, size_t *newlen, int encode);
 PHPAPI int php_url_scanner_add_session_var(const char *name, size_t name_len, const char *value, size_t value_len, int encode);
diff --git a/ext/standard/url_scanner_ex.re b/ext/standard/url_scanner_ex.re
index 866b9bcc0db..1eb1cb98c33 100644
--- a/ext/standard/url_scanner_ex.re
+++ b/ext/standard/url_scanner_ex.re
@@ -680,6 +680,13 @@ static int php_url_scanner_ex_deactivate(int type)
 	return SUCCESS;
 }

+void php_url_scanner_early_rinit(void)
+{
+	/* See php_basic_early_rinit() */
+	php_url_scanner_ex_activate(1);
+	php_url_scanner_ex_activate(0);
+}
+
 static inline void php_url_scanner_session_handler_impl(char *output, size_t output_len, char **handled_output, size_t *handled_output_len, int mode, int type)
 {
 	size_t len;
diff --git a/ext/standard/var.c b/ext/standard/var.c
index 61abc3dce70..429e90d48f1 100644
--- a/ext/standard/var.c
+++ b/ext/standard/var.c
@@ -33,6 +33,9 @@
 struct php_serialize_data {
 	HashTable ht;
 	uint32_t n;
+	/* Write back the C: and E: payloads kept by php_var_unserialize() in
+	 * incomplete_classes_only mode, see php_var_serialize_incomplete_raw() */
+	bool incomplete_classes_only;
 };

 #define COMMON (is_ref ? "&" : "")
@@ -767,6 +770,56 @@ static inline bool php_var_serialize_class_name(smart_str *buf, zval *struc) /*
 }
 /* }}} */

+static bool php_var_serialize_incomplete_raw(smart_str *buf, zval *struc)
+{
+	HashTable *props = Z_OBJ_P(struc)->properties;
+	zend_string *class_name;
+	zval *member;
+
+	if (!props) {
+		return false;
+	}
+
+	member = zend_hash_str_find(props, MAGIC_MEMBER_SERIALIZED, sizeof(MAGIC_MEMBER_SERIALIZED) - 1);
+	if (member && Z_TYPE_P(member) == IS_STRING) {
+		class_name = php_lookup_class_name(Z_OBJ_P(struc));
+		if (!class_name) {
+			return false;
+		}
+		smart_str_appendl(buf, "C:", 2);
+		smart_str_append_unsigned(buf, ZSTR_LEN(class_name));
+		smart_str_appendl(buf, ":\"", 2);
+		smart_str_append(buf, class_name);
+		smart_str_appendl(buf, "\":", 2);
+		smart_str_append_unsigned(buf, Z_STRLEN_P(member));
+		smart_str_appendl(buf, ":{", 2);
+		smart_str_append(buf, Z_STR_P(member));
+		smart_str_appendc(buf, '}');
+		zend_string_release_ex(class_name, 0);
+		return true;
+	}
+
+	member = zend_hash_str_find(props, MAGIC_MEMBER_ENUM_CASE, sizeof(MAGIC_MEMBER_ENUM_CASE) - 1);
+	if (member && Z_TYPE_P(member) == IS_STRING) {
+		class_name = php_lookup_class_name(Z_OBJ_P(struc));
+		if (!class_name) {
+			return false;
+		}
+		smart_str_appendl(buf, "E:", 2);
+		smart_str_append_unsigned(buf, ZSTR_LEN(class_name) + strlen(":") + Z_STRLEN_P(member));
+		smart_str_appendl(buf, ":\"", 2);
+		smart_str_append(buf, class_name);
+		smart_str_appendc(buf, ':');
+		smart_str_append(buf, Z_STR_P(member));
+		smart_str_appendl(buf, "\";", 2);
+		zend_string_release_ex(class_name, 0);
+		return true;
+	}
+
+	/* Other incomplete classes are handled using the normal path */
+	return false;
+}
+
 static HashTable* php_var_serialize_call_sleep(zend_object *obj, zend_function *fn) /* {{{ */
 {
 	zend_result res;
@@ -1061,6 +1114,10 @@ static void php_var_serialize_intern(smart_str *buf, zval *struc, php_serialize_
 				bool incomplete_class;
 				uint32_t count;

+				if (ce == PHP_IC_ENTRY && var_hash->incomplete_classes_only && php_var_serialize_incomplete_raw(buf, struc)) {
+					return;
+				}
+
 				if (ce->ce_flags & ZEND_ACC_NOT_SERIALIZABLE) {
 					zend_throw_exception_ex(NULL, 0, "Serialization of '%s' is not allowed",
 						ZSTR_VAL(ce->name));
@@ -1284,6 +1341,7 @@ PHPAPI php_serialize_data_t php_var_serialize_init(void) {
 		d = emalloc(sizeof(struct php_serialize_data));
 		zend_hash_init(&d->ht, 16, NULL, ZVAL_PTR_DTOR, 0);
 		d->n = 0;
+		d->incomplete_classes_only = false;
 		if (!BG(serialize_lock)) {
 			BG(serialize).data = d;
 			BG(serialize).level = 1;
@@ -1295,6 +1353,10 @@ PHPAPI php_serialize_data_t php_var_serialize_init(void) {
 	return d;
 }

+PHPAPI void php_var_serialize_set_incomplete_classes_only(php_serialize_data_t d, bool incomplete_classes_only) {
+	d->incomplete_classes_only = incomplete_classes_only;
+}
+
 PHPAPI void php_var_serialize_destroy(php_serialize_data_t d) {
 	/* fprintf(stderr, "SERIALIZE_DESTROY   == lock: %u, level: %u\n", BG(serialize_lock), BG(serialize).level); */
 	if (BG(serialize_lock) || BG(serialize).level == 1) {
diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re
index 313d7fadeb3..f60dfbb63f7 100644
--- a/ext/standard/var_unserializer.re
+++ b/ext/standard/var_unserializer.re
@@ -54,6 +54,11 @@ struct php_unserialize_data {
 	HashTable        *ref_props;
 	zend_long         cur_depth;
 	zend_long         max_depth;
+	/* Represent every object as __PHP_Incomplete_Class, without resolving its
+	 * class: no autoloading, no unserialize_callback_func, no __wakeup() or
+	 * __unserialize(). O:, C: and E: values all serialize back to the same
+	 * bytes. */
+	bool              incomplete_classes_only;
 	var_entries       entries;
 };

@@ -68,6 +73,7 @@ PHPAPI php_unserialize_data_t php_var_unserialize_init(void) {
 		d->ref_props = NULL;
 		d->cur_depth = 0;
 		d->max_depth = BG(unserialize_max_depth);
+		d->incomplete_classes_only = false;
 		d->entries.used_slots = 0;
 		d->entries.next = NULL;
 		if (!BG(serialize_lock)) {
@@ -99,6 +105,10 @@ PHPAPI void php_var_unserialize_set_allowed_classes(php_unserialize_data_t d, Ha
 	d->allowed_classes = classes;
 }

+PHPAPI void php_var_unserialize_set_incomplete_classes_only(php_unserialize_data_t d, bool incomplete_classes_only) {
+	d->incomplete_classes_only = incomplete_classes_only;
+}
+
 PHPAPI void php_var_unserialize_set_max_depth(php_unserialize_data_t d, zend_long max_depth) {
 	d->max_depth = max_depth;
 }
@@ -275,7 +285,7 @@ PHPAPI void var_destroy(php_unserialize_data_t *var_hashx)
 					fci_cache.called_scope = fci.object->ce;

 					BG(serialize_lock)++;
-					if (zend_call_function(&fci, &fci_cache) == FAILURE || Z_ISUNDEF(retval)) {
+					if (zend_call_function(&fci, &fci_cache) == FAILURE || Z_ISUNDEF(retval) || ZEND_HAS_OOM()) {
 						delayed_call_failed = 1;
 						GC_ADD_FLAGS(Z_OBJ_P(zv), IS_OBJ_DESTRUCTOR_CALLED);
 					}
@@ -294,7 +304,7 @@ PHPAPI void var_destroy(php_unserialize_data_t *var_hashx)
 					BG(serialize_lock)++;
 					zend_call_known_instance_method_with_1_params(
 						Z_OBJCE_P(zv)->__unserialize, Z_OBJ_P(zv), NULL, &param);
-					if (EG(exception)) {
+					if (EG(exception) || ZEND_HAS_OOM()) {
 						delayed_call_failed = 1;
 						GC_ADD_FLAGS(Z_OBJ_P(zv), IS_OBJ_DESTRUCTOR_CALLED);
 					}
@@ -323,7 +333,10 @@ PHPAPI void var_destroy(php_unserialize_data_t *var_hashx)
 static zend_string *unserialize_str(const unsigned char **p, size_t len, size_t maxlen)
 {
 	size_t i, j;
-	zend_string *str = zend_string_safe_alloc(1, len, 0, 0);
+	zend_string *str = zend_string_falloc(len, 0, /* fallible */ true);
+	if (UNEXPECTED(!str)) {
+		return NULL;
+	}
 	unsigned char *end = *(unsigned char **)p+maxlen;

 	if (end < *p) {
@@ -522,6 +535,10 @@ numeric_key:
 			(*p)--;
 			goto failure;
 		}
+
+		if (ZEND_HAS_OOM()) {
+			goto failure;
+		}
 	}

 	if (var_hash) {
@@ -573,6 +590,17 @@ static int is_property_visibility_changed(zend_class_entry *ce, zval *key)
 }


+/* The members that incomplete_classes_only mode writes itself: the class name of
+ * every object, and the payload kept for C: and E: entries. Data that already
+ * carries one of them cannot be told apart from that bookkeeping, so it is
+ * rejected rather than silently overwritten. */
+static zend_always_inline bool is_incomplete_class_reserved_member(const zend_string *name)
+{
+	return zend_string_equals_literal(name, MAGIC_MEMBER)
+		|| zend_string_equals_literal(name, MAGIC_MEMBER_SERIALIZED)
+		|| zend_string_equals_literal(name, MAGIC_MEMBER_ENUM_CASE);
+}
+
 static zend_always_inline int process_nested_object_data(UNSERIALIZE_PARAMETER, HashTable *ht, zend_long elements, zend_object *obj)
 {
 	if (var_hash) {
@@ -598,6 +626,13 @@ static zend_always_inline int process_nested_object_data(UNSERIALIZE_PARAMETER,
 			goto failure;
 		}

+		if (UNEXPECTED(var_hash && (*var_hash)->incomplete_classes_only)
+		 && Z_TYPE(key) == IS_STRING
+		 && UNEXPECTED(is_incomplete_class_reserved_member(Z_STR(key)))) {
+			zval_ptr_dtor(&key);
+			goto failure;
+		}
+
 		if (EXPECTED(Z_TYPE(key) == IS_STRING)) {
 string_key:
 			data = zend_hash_find(ht, Z_STR(key));
@@ -713,6 +748,10 @@ second_try:
 			(*p)--;
 			goto failure;
 		}
+
+		if (ZEND_HAS_OOM()) {
+			goto failure;
+		}
 	}

 	if (var_hash) {
@@ -737,7 +776,7 @@ static inline int finish_nested_data(UNSERIALIZE_PARAMETER)
 	return 1;
 }

-static inline int object_custom(UNSERIALIZE_PARAMETER, zend_class_entry *ce)
+static inline int object_custom(UNSERIALIZE_PARAMETER, zend_class_entry *ce, bool keep_payload)
 {
 	zend_long datalen;

@@ -771,13 +810,28 @@ static inline int object_custom(UNSERIALIZE_PARAMETER, zend_class_entry *ce)
 		return 0;
 	}

-	if (ce->unserialize == NULL) {
+	if (keep_payload) {
+		/* The class was not resolved (see php_var_unserialize_set_incomplete_classes_only()):
+		 * keep the payload verbatim, so that the object serializes back to the same bytes. */
+		zval payload;
+
+		ZEND_ASSERT(ce == PHP_IC_ENTRY);
+		if (UNEXPECTED(!ZVAL_FSTRINGL(&payload, (const char *) *p, datalen))) {
+			return 0;
+		}
+		object_init_ex(rval, ce);
+		zend_hash_str_update(Z_OBJPROP_P(rval), MAGIC_MEMBER_SERIALIZED, sizeof(MAGIC_MEMBER_SERIALIZED) - 1, &payload);
+	} else if (ce->unserialize == NULL) {
 		zend_error(E_WARNING, "Class %s has no unserializer", ZSTR_VAL(ce->name));
 		object_init_ex(rval, ce);
 	} else if (ce->unserialize(rval, ce, (const unsigned char*)*p, datalen, (zend_unserialize_data *)var_hash) != SUCCESS) {
 		return 0;
 	}

+	if (ZEND_HAS_OOM()) {
+		return 0;
+	}
+
 	(*p) += datalen + 1; /* +1 for '}' */
 	return 1;
 }
@@ -797,9 +851,10 @@ static inline int object_common(UNSERIALIZE_PARAMETER, zend_long elements, bool
 			return 0;
 		}

-		array_init_size(&ary, elements);
 		/* Avoid reallocation due to packed -> mixed conversion. */
-		zend_hash_real_init_mixed(Z_ARRVAL(ary));
+		if (UNEXPECTED(!array_finit_size_mixed(&ary, elements))) {
+			return 0;
+		}
 		if (!process_nested_array_data(UNSERIALIZE_PASSTHRU, Z_ARRVAL(ary), elements)) {
 			ZVAL_DEREF(rval);
 			GC_ADD_FLAGS(Z_OBJ_P(rval), IS_OBJ_DESTRUCTOR_CALLED);
@@ -827,7 +882,9 @@ static inline int object_common(UNSERIALIZE_PARAMETER, zend_long elements, bool
 		return 0;
 	}

-	zend_hash_extend(ht, zend_hash_num_elements(ht) + elements, HT_FLAGS(ht) & HASH_FLAG_PACKED);
+	if (UNEXPECTED(zend_hash_fextend(ht, zend_hash_num_elements(ht) + elements, HT_FLAGS(ht) & HASH_FLAG_PACKED) == FAILURE)) {
+		return 0;
+	}
 	if (!process_nested_object_data(UNSERIALIZE_PASSTHRU, ht, elements, Z_OBJ_P(rval))) {
 		if (has_wakeup) {
 			ZVAL_DEREF(rval);
@@ -1050,12 +1107,21 @@ use_double:
 	YYCURSOR += 2;
 	*p = YYCURSOR;

+	zend_string *zstr;
+
 	if (!var_hash) {
 		/* Array or object key unserialization */
-		ZVAL_STR(rval, zend_string_init_existing_interned(str, len, 0));
+		zstr = zend_string_finit_existing_interned(str, len, /* permanent */ false, /* fallible */ true);
 	} else {
-		ZVAL_STRINGL_FAST(rval, str, len);
+		zstr = zend_string_finit_fast(str, len, /* fallible */ true);
 	}
+
+	if (UNEXPECTED(!zstr)) {
+		return 0;
+	}
+
+	ZVAL_STR(rval, zstr);
+
 	return 1;
 }

@@ -1104,10 +1170,11 @@ use_double:
 	}

 	if (elements) {
-		array_init_size(rval, elements);
 		/* we can't convert from packed to hash during unserialization, because
 		   reference to some zvals might be kept in var_hash (to support references) */
-		zend_hash_real_init_mixed(Z_ARRVAL_P(rval));
+		if (UNEXPECTED(!array_finit_size_mixed(rval, elements))) {
+			return 0;
+		}
 	} else {
 		ZVAL_EMPTY_ARRAY(rval);
 		return finish_nested_data(UNSERIALIZE_PASSTHRU);
@@ -1180,11 +1247,25 @@ object ":" uiv ":" ["]	{
 		return 0;
 	}

-	class_name = zend_string_init_interned(str, len, 0);
+	class_name = zend_string_finit_existing_interned(str, len, /* permanent */ false, /* fallible */ true);
+	if (UNEXPECTED(!class_name)) {
+		return 0;
+	}

 	do {
 		zend_string *lc_name;

+		if ((*var_hash)->incomplete_classes_only) {
+			/* Do not look the class up at all, so that no user code can run. */
+			if (!zend_is_valid_class_name(class_name)) {
+				zend_string_release_ex(class_name, 0);
+				return 0;
+			}
+			incomplete_class = 1;
+			ce = PHP_IC_ENTRY;
+			break;
+		}
+
 		if (!(*var_hash)->allowed_classes && ZSTR_HAS_CE_CACHE(class_name)) {
 			ce = ZSTR_GET_CE_CACHE(class_name);
 			if (ce) {
@@ -1192,7 +1273,11 @@ object ":" uiv ":" ["]	{
 			}
 		}

-		lc_name = zend_string_tolower(class_name);
+		lc_name = zend_string_ftolower(class_name);
+		if (UNEXPECTED(!lc_name)) {
+			zend_string_release_ex(class_name, 0);
+			return 0;
+		}
 		if(!unserialize_allowed_class(lc_name, var_hash)) {
 			zend_string_release_ex(lc_name, 0);
 			if (!zend_is_valid_class_name(class_name)) {
@@ -1286,7 +1371,7 @@ object ":" uiv ":" ["]	{
 	if (custom_object) {
 		int ret;

-		ret = object_custom(UNSERIALIZE_PASSTHRU, ce);
+		ret = object_custom(UNSERIALIZE_PASSTHRU, ce, (*var_hash)->incomplete_classes_only);

 		if (ret && incomplete_class) {
 			php_store_class_name(rval, class_name);
@@ -1336,6 +1421,11 @@ object ":" uiv ":" ["]	{
 		return 0;
 	}

+	if (ZEND_HAS_OOM()) {
+		zend_string_release_ex(class_name, 0);
+		return 0;
+	}
+
 	if (incomplete_class) {
 		php_store_class_name(rval, class_name);
 	}
@@ -1380,6 +1470,25 @@ object ":" uiv ":" ["]	{
 		goto fail;
 	}

+	if ((*var_hash)->incomplete_classes_only) {
+		/* Do not load the enum: keep the case name, so that the value serializes
+		 * back to the same bytes. */
+		zval case_zv;
+
+		YYCURSOR += 2;
+		*p = YYCURSOR;
+
+		object_init_ex(rval, PHP_IC_ENTRY);
+		if (ZEND_HAS_OOM()) {
+			goto fail;
+		}
+		php_store_class_name(rval, enum_name);
+		ZVAL_STR(&case_zv, case_name);
+		zend_hash_str_update(Z_OBJPROP_P(rval), MAGIC_MEMBER_ENUM_CASE, sizeof(MAGIC_MEMBER_ENUM_CASE) - 1, &case_zv);
+		zend_string_release_ex(enum_name, 0);
+		return 1;
+	}
+
 	zend_class_entry *ce = zend_lookup_class(enum_name);
 	if (!ce) {
 		php_error_docref(NULL, E_WARNING, "Class '%s' not found", ZSTR_VAL(enum_name));
diff --git a/ext/tokenizer/tests/token_get_all_TOKEN_PARSE_oom_feedback.phpt b/ext/tokenizer/tests/token_get_all_TOKEN_PARSE_oom_feedback.phpt
new file mode 100644
index 00000000000..acaf0321a19
--- /dev/null
+++ b/ext/tokenizer/tests/token_get_all_TOKEN_PARSE_oom_feedback.phpt
@@ -0,0 +1,15 @@
+--TEST--
+TOKEN_PARSE feedback after a token is dropped on OOM
+--EXTENSIONS--
+tokenizer
+--ENV--
+USE_ZEND_ALLOC=0
+USE_TRACKED_ALLOC=1
+--FILE--
+<?php
+$code = '<?php X::continue;';
+ini_set('memory_limit', memory_get_usage() + 1024);
+token_get_all($code, TOKEN_PARSE);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/tokenizer/tokenizer.c b/ext/tokenizer/tokenizer.c
index 20124dbf911..c4be6b163cf 100644
--- a/ext/tokenizer/tokenizer.c
+++ b/ext/tokenizer/tokenizer.c
@@ -112,7 +112,7 @@ PHP_METHOD(PhpToken, tokenize)
 	}

 	if (!tokenize_common(return_value, source, flags, token_class)) {
-		RETURN_THROWS();
+		return;
 	}
 }

@@ -272,23 +272,30 @@ static zend_string *make_str(unsigned char *text, size_t leng, HashTable *intern
 		if (interned_str) {
 			return zend_string_copy(interned_str);
 		}
-		interned_str = zend_string_init((char *) text, leng, 0);
+		interned_str = zend_string_finit((char *) text, leng, /* persistent */ false, /* fallible */ true);
+		if (UNEXPECTED(!interned_str)) {
+			return NULL;
+		}
 		zend_hash_add_new_ptr(interned_strings, interned_str, interned_str);
 		return interned_str;
 	} else {
-		return zend_string_init((char *) text, leng, 0);
+		return zend_string_finit((char *) text, leng, /* persistent */ false, /* fallible */ true);
 	}
 }

-static void add_token(
+static bool add_token(
 		zval *return_value, int token_type, unsigned char *text, size_t leng, int lineno,
 		zend_class_entry *token_class, HashTable *interned_strings) {
 	zval token;
 	if (token_class) {
+		zend_string *value = make_str(text, leng, interned_strings);
+		if (UNEXPECTED(!value)) {
+			return false;
+		}
 		zend_object *obj = zend_objects_new(token_class);
 		ZVAL_OBJ(&token, obj);
 		ZVAL_LONG(OBJ_PROP_NUM(obj, 0), token_type);
-		ZVAL_STR(OBJ_PROP_NUM(obj, 1), make_str(text, leng, interned_strings));
+		ZVAL_STR(OBJ_PROP_NUM(obj, 1), value);
 		ZVAL_LONG(OBJ_PROP_NUM(obj, 2), lineno);
 		ZVAL_LONG(OBJ_PROP_NUM(obj, 3), text - LANG_SCNG(yy_start));

@@ -306,17 +313,28 @@ static void add_token(
 		array_init_size(&token, 3);
 		zend_hash_real_init_packed(Z_ARRVAL(token));
 		ZEND_HASH_FILL_PACKED(Z_ARRVAL(token)) {
+			zend_string *value = make_str(text, leng, interned_strings);
+			if (UNEXPECTED(!value)) {
+				ZEND_HASH_FILL_FINISH();
+				zval_ptr_dtor(&token);
+				return false;
+			}
 			ZEND_HASH_FILL_SET_LONG(token_type);
 			ZEND_HASH_FILL_NEXT();
-			ZEND_HASH_FILL_SET_STR(make_str(text, leng, interned_strings));
+			ZEND_HASH_FILL_SET_STR(value);
 			ZEND_HASH_FILL_NEXT();
 			ZEND_HASH_FILL_SET_LONG(lineno);
 			ZEND_HASH_FILL_NEXT();
 		} ZEND_HASH_FILL_END();
 	} else {
-		ZVAL_STR(&token, make_str(text, leng, interned_strings));
+		zend_string *value = make_str(text, leng, interned_strings);
+		if (UNEXPECTED(!value)) {
+			return false;
+		}
+		ZVAL_STR(&token, value);
 	}
 	zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &token);
+	return true;
 }

 static bool tokenize(zval *return_value, zend_string *source, zend_class_entry *token_class)
@@ -328,6 +346,7 @@ static bool tokenize(zval *return_value, zend_string *source, zend_class_entry *
 	int token_line = 1;
 	int need_tokens = -1; /* for __halt_compiler lexing. -1 = disabled */
 	HashTable interned_strings;
+	bool success = true;

 	ZVAL_STR_COPY(&source_zval, source);
 	zend_save_lexical_state(&original_lex_state);
@@ -341,9 +360,12 @@ static bool tokenize(zval *return_value, zend_string *source, zend_class_entry *
 	while ((token_type = lex_scan(&token, NULL))) {
 		ZEND_ASSERT(token_type != T_ERROR);

-		add_token(
+		if (!add_token(
 			return_value, token_type, zendtext, zendleng, token_line,
-			token_class, &interned_strings);
+			token_class, &interned_strings)) {
+			success = false;
+			break;
+		}

 		if (Z_TYPE(token) != IS_UNDEF) {
 			zval_ptr_dtor_nogc(&token);
@@ -358,9 +380,12 @@ static bool tokenize(zval *return_value, zend_string *source, zend_class_entry *
 			) {
 				/* fetch the rest into a T_INLINE_HTML */
 				if (zendcursor < zendlimit) {
-					add_token(
+					if (!add_token(
 						return_value, T_INLINE_HTML, zendcursor, zendlimit - zendcursor,
-						token_line, token_class, &interned_strings);
+						token_line, token_class, &interned_strings)) {
+						success = false;
+						break;
+					}
 				}
 				break;
 			}
@@ -380,12 +405,13 @@ static bool tokenize(zval *return_value, zend_string *source, zend_class_entry *
 	zend_restore_lexical_state(&original_lex_state);
 	zend_hash_destroy(&interned_strings);

-	return 1;
+	return success;
 }

 struct event_context {
 	zval *tokens;
 	zend_class_entry *token_class;
+	bool oom;
 };

 static zval *extract_token_id_to_replace(zval *token_zv, const char *text, size_t length) {
@@ -416,6 +442,10 @@ void on_event(
 {
 	struct event_context *ctx = context;

+	if (UNEXPECTED(ctx->oom)) {
+		return;
+	}
+
 	switch (event) {
 		case ON_TOKEN:
 			if (token == END) break;
@@ -425,8 +455,11 @@ void on_event(
 			} else if (token == T_ECHO && LANG_SCNG(yy_leng) == sizeof("<?=") - 1) {
 				token = T_OPEN_TAG_WITH_ECHO;
 			}
-			add_token(
-				ctx->tokens, token, (unsigned char *) text, length, line, ctx->token_class, NULL);
+			if (UNEXPECTED(!add_token(
+				ctx->tokens, token, (unsigned char *) text, length, line, ctx->token_class, NULL))) {
+				ZEND_ASSERT(ZEND_HAS_OOM());
+				ctx->oom = true;
+			}
 			break;
 		case ON_FEEDBACK: {
 			HashTable *tokens_ht = Z_ARRVAL_P(ctx->tokens);
@@ -472,6 +505,7 @@ static bool tokenize_parse(

 	ctx.tokens = &token_stream;
 	ctx.token_class = token_class;
+	ctx.oom = false;

 	CG(ast) = NULL;
 	CG(ast_arena) = zend_arena_create(1024 * 32);
@@ -525,7 +559,7 @@ PHP_FUNCTION(token_get_all)
 	ZEND_PARSE_PARAMETERS_END();

 	if (!tokenize_common(return_value, source, flags, /* token_class */ NULL)) {
-		RETURN_THROWS();
+		return;
 	}
 }
 /* }}} */
diff --git a/ext/xml/tests/oom_xml_parse_attributes.phpt b/ext/xml/tests/oom_xml_parse_attributes.phpt
new file mode 100644
index 00000000000..aeb27f54427
--- /dev/null
+++ b/ext/xml/tests/oom_xml_parse_attributes.phpt
@@ -0,0 +1,22 @@
+--TEST--
+xml_parse() doesn't reach hard limit on attributes
+--EXTENSIONS--
+xml
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+// ~1 MB of markup, well under memory_limit / 4, as 100K empty attributes
+$data = "<r";
+for ($i = 0; $i < 100000; $i++) {
+    $data .= " a$i=\"\"";
+}
+$data .= "/>";
+
+$parser = xml_parser_create();
+xml_set_element_handler($parser, function () {}, function () {});
+xml_parse($parser, $data, true);
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/xml/tests/oom_xml_parse_into_struct.phpt b/ext/xml/tests/oom_xml_parse_into_struct.phpt
new file mode 100644
index 00000000000..6889c94c527
--- /dev/null
+++ b/ext/xml/tests/oom_xml_parse_into_struct.phpt
@@ -0,0 +1,16 @@
+--TEST--
+xml_parse_into_struct() doesn't reach hard limit
+--EXTENSIONS--
+xml
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+// 2 MB == memory_limit / 4, as 512K empty elements
+$data = "<r>" . str_repeat("<a/>", 512 * 1024) . "</r>";
+$parser = xml_parser_create();
+xml_parse_into_struct($parser, $data, $values, $index);
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/xml/tests/oom_xml_parse_ns.phpt b/ext/xml/tests/oom_xml_parse_ns.phpt
new file mode 100644
index 00000000000..0a89969cda1
--- /dev/null
+++ b/ext/xml/tests/oom_xml_parse_ns.phpt
@@ -0,0 +1,27 @@
+--TEST--
+xml_parse() doesn't reach hard limit on namespaced attributes
+--EXTENSIONS--
+xml
+--SKIPIF--
+<?php
+if (!@xml_parser_create_ns()) die("skip xml_parser_create_ns() not supported");
+?>
+--INI--
+memory_limit=8M
+--FILE--
+<?php
+// 8 KB URI x 4000 attributes == ~32 MB of expanded names from ~50 KB of input
+$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);
+echo "not reached\n";
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/xml/xml.c b/ext/xml/xml.c
index f10b8704da6..451b4f3da40 100644
--- a/ext/xml/xml.c
+++ b/ext/xml/xml.c
@@ -322,8 +322,11 @@ static void xml_parser_free_ltags(xml_parser *parser)
 {
 	if (parser->ltags) {
 		int inx;
-		for (inx = 0; ((inx < parser->level) && (inx < XML_MAXLEVEL)); inx++)
-			efree(parser->ltags[ inx ]);
+		for (inx = 0; ((inx < parser->level) && (inx < XML_MAXLEVEL)); inx++) {
+			if (parser->ltags[ inx ]) {
+				efree(parser->ltags[ inx ]);
+			}
+		}
 		efree(parser->ltags);
 	}
 }
@@ -589,6 +592,11 @@ static zend_string *_xml_decode_tag(xml_parser *parser, const XML_Char *tag)
 }
 /* }}} */

+static zend_always_inline bool xml_handler_aborted(void)
+{
+	return EG(exception) != NULL || ZEND_HAS_OOM();
+}
+
 /* {{{ _xml_startElementHandler() */
 void _xml_startElementHandler(void *userData, const XML_Char *name, const XML_Char **attributes)
 {
@@ -601,6 +609,10 @@ void _xml_startElementHandler(void *userData, const XML_Char *name, const XML_Ch
 		return;
 	}

+	if (UNEXPECTED(xml_handler_aborted())) {
+		return;
+	}
+
 	parser->level++;

 	tag_name = _xml_decode_tag(parser, name);
@@ -622,6 +634,14 @@ void _xml_startElementHandler(void *userData, const XML_Char *name, const XML_Ch
 			attributes += 2;

 			zend_string_release_ex(att, 0);
+
+			if (ZEND_HAS_OOM()) {
+				zend_string_release_ex(tag_name, false);
+				zval_ptr_dtor(&args[0]);
+				zval_ptr_dtor(&args[1]);
+				zval_ptr_dtor(&args[2]);
+				return;
+			}
 		}

 		xml_call_handler(parser, &parser->startElementHandler, parser->startElementPtr, 3, args, &retval);
@@ -662,6 +682,13 @@ void _xml_startElementHandler(void *userData, const XML_Char *name, const XML_Ch
 				attributes += 2;

 				zend_string_release_ex(att, 0);
+
+				if (ZEND_HAS_OOM()) {
+					zend_string_release_ex(tag_name, false);
+					zval_ptr_dtor(&tag);
+					zval_ptr_dtor(&atr);
+					return;
+				}
 			}

 			if (atcnt) {
@@ -689,6 +716,10 @@ void _xml_endElementHandler(void *userData, const XML_Char *name)
 		return;
 	}

+	if (UNEXPECTED(xml_handler_aborted())) {
+		return;
+	}
+
 	zval retval, args[2];

 	zend_string *tag_name = _xml_decode_tag(parser, name);
@@ -727,6 +758,7 @@ void _xml_endElementHandler(void *userData, const XML_Char *name)

 	if ((parser->ltags) && (parser->level <= XML_MAXLEVEL)) {
 		efree(parser->ltags[parser->level-1]);
+		parser->ltags[parser->level-1] = NULL;
 	}

 	parser->level--;
@@ -742,6 +774,10 @@ void _xml_characterDataHandler(void *userData, const XML_Char *s, int len)
 		return;
 	}

+	if (UNEXPECTED(xml_handler_aborted())) {
+		return;
+	}
+
 	zval retval, args[2];

 	if (!Z_ISUNDEF(parser->characterDataHandler)) {
diff --git a/ext/xmlreader/php_xmlreader.c b/ext/xmlreader/php_xmlreader.c
index e3ca4139935..edf0eca2638 100644
--- a/ext/xmlreader/php_xmlreader.c
+++ b/ext/xmlreader/php_xmlreader.c
@@ -96,7 +96,7 @@ static int xmlreader_property_reader(xmlreader_object *obj, xmlreader_prop_handl
 	switch (hnd->type) {
 		case IS_STRING:
 			if (retchar) {
-				ZVAL_STRING(rv, (char *) retchar);
+				ZVAL_FSTRING(rv, (char *) retchar);
 			} else {
 				ZVAL_EMPTY_STRING(rv);
 			}
@@ -402,7 +402,7 @@ static void php_xmlreader_string_arg(INTERNAL_FUNCTION_PARAMETERS, xmlreader_rea
 		retchar = (char *)internal_function(intern->ptr, (const unsigned char *)name);
 	}
 	if (retchar) {
-		RETVAL_STRING(retchar);
+		RETVAL_FSTRING(retchar);
 		xmlFree(retchar);
 		return;
 	} else {
@@ -456,7 +456,7 @@ static void php_xmlreader_no_arg_string(INTERNAL_FUNCTION_PARAMETERS, xmlreader_
 		retchar = (char *)internal_function(intern->ptr);
 	}
 	if (retchar) {
-		RETVAL_STRING(retchar);
+		RETVAL_FSTRING(retchar);
 		xmlFree(retchar);
 		return;
 	} else {
@@ -568,7 +568,7 @@ PHP_METHOD(XMLReader, getAttributeNo)
 		retchar = (char *)xmlTextReaderGetAttributeNo(intern->ptr, attr_pos);
 	}
 	if (retchar) {
-		RETVAL_STRING(retchar);
+		RETVAL_FSTRING(retchar);
 		xmlFree(retchar);
 	}
 }
@@ -603,7 +603,7 @@ PHP_METHOD(XMLReader, getAttributeNs)
 		retchar = (char *)xmlTextReaderGetAttributeNs(intern->ptr, (xmlChar *)name, (xmlChar *)ns_uri);
 	}
 	if (retchar) {
-		RETVAL_STRING(retchar);
+		RETVAL_FSTRING(retchar);
 		xmlFree(retchar);
 	}
 }
diff --git a/ext/xmlreader/tests/oom_xmlreader_getattribute.phpt b/ext/xmlreader/tests/oom_xmlreader_getattribute.phpt
new file mode 100644
index 00000000000..65f1b4bfec8
--- /dev/null
+++ b/ext/xmlreader/tests/oom_xmlreader_getattribute.phpt
@@ -0,0 +1,32 @@
+--TEST--
+XMLReader::getAttribute() doesn't reach hard limit
+--EXTENSIONS--
+xmlreader
+--INI--
+memory_limit=2M
+--FILE--
+<?php
+$path = __DIR__ . '/' . basename(__FILE__, '.php') . '.tmp';
+$fp = fopen($path, 'wb');
+/* A single 6 MB attribute value, written in chunks so building the
+ * document stays inside the soft limit. */
+$chunk = str_repeat("x", 512 * 1024);
+fwrite($fp, "<r a=\"");
+for ($i = 0; $i < 12; $i++) {
+    fwrite($fp, $chunk);
+}
+fwrite($fp, "\"/>");
+unset($chunk);
+fclose($fp);
+
+$reader = XMLReader::open($path);
+$reader->read();
+$reader->getAttribute("a");
+echo "not reached\n";
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.tmp');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 2097152 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/xmlreader/tests/oom_xmlreader_getattributeno.phpt b/ext/xmlreader/tests/oom_xmlreader_getattributeno.phpt
new file mode 100644
index 00000000000..19c55cbab10
--- /dev/null
+++ b/ext/xmlreader/tests/oom_xmlreader_getattributeno.phpt
@@ -0,0 +1,32 @@
+--TEST--
+XMLReader::getAttributeNo() doesn't reach hard limit
+--EXTENSIONS--
+xmlreader
+--INI--
+memory_limit=2M
+--FILE--
+<?php
+$path = __DIR__ . '/' . basename(__FILE__, '.php') . '.tmp';
+$fp = fopen($path, 'wb');
+/* A single 6 MB attribute value, written in chunks so building the
+ * document stays inside the soft limit. */
+$chunk = str_repeat("x", 512 * 1024);
+fwrite($fp, "<r a=\"");
+for ($i = 0; $i < 12; $i++) {
+    fwrite($fp, $chunk);
+}
+fwrite($fp, "\"/>");
+unset($chunk);
+fclose($fp);
+
+$reader = XMLReader::open($path);
+$reader->read();
+$reader->getAttributeNo(0);
+echo "not reached\n";
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.tmp');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 2097152 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/xmlreader/tests/oom_xmlreader_getattributens.phpt b/ext/xmlreader/tests/oom_xmlreader_getattributens.phpt
new file mode 100644
index 00000000000..62552cbe433
--- /dev/null
+++ b/ext/xmlreader/tests/oom_xmlreader_getattributens.phpt
@@ -0,0 +1,31 @@
+--TEST--
+XMLReader::getAttributeNs() doesn't reach hard limit
+--EXTENSIONS--
+xmlreader
+--INI--
+memory_limit=2M
+--FILE--
+<?php
+$path = __DIR__ . '/' . basename(__FILE__, '.php') . '.tmp';
+$fp = fopen($path, 'wb');
+/* A single 6 MB namespaced attribute value. */
+$chunk = str_repeat("x", 512 * 1024);
+fwrite($fp, "<r xmlns:p=\"urn:t\" p:a=\"");
+for ($i = 0; $i < 12; $i++) {
+    fwrite($fp, $chunk);
+}
+fwrite($fp, "\"/>");
+unset($chunk);
+fclose($fp);
+
+$reader = XMLReader::open($path);
+$reader->read();
+$reader->getAttributeNs("a", "urn:t");
+echo "not reached\n";
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.tmp');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 2097152 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/xmlreader/tests/oom_xmlreader_lookupnamespace.phpt b/ext/xmlreader/tests/oom_xmlreader_lookupnamespace.phpt
new file mode 100644
index 00000000000..09b597a06c8
--- /dev/null
+++ b/ext/xmlreader/tests/oom_xmlreader_lookupnamespace.phpt
@@ -0,0 +1,32 @@
+--TEST--
+XMLReader::lookupNamespace() doesn't reach hard limit
+--EXTENSIONS--
+xmlreader
+--INI--
+memory_limit=2M
+--FILE--
+<?php
+$path = __DIR__ . '/' . basename(__FILE__, '.php') . '.tmp';
+$fp = fopen($path, 'wb');
+/* A 6 MB namespace URI. */
+$chunk = str_repeat("x", 512 * 1024);
+fwrite($fp, "<r xmlns:p=\"");
+for ($i = 0; $i < 12; $i++) {
+    fwrite($fp, $chunk);
+}
+fwrite($fp, "\"><p:c/></r>");
+unset($chunk);
+fclose($fp);
+
+$reader = XMLReader::open($path);
+$reader->read();
+$reader->read();
+$reader->lookupNamespace("p");
+echo "not reached\n";
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.tmp');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 2097152 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/xmlreader/tests/oom_xmlreader_readinnerxml.phpt b/ext/xmlreader/tests/oom_xmlreader_readinnerxml.phpt
new file mode 100644
index 00000000000..61c23c8570d
--- /dev/null
+++ b/ext/xmlreader/tests/oom_xmlreader_readinnerxml.phpt
@@ -0,0 +1,36 @@
+--TEST--
+XMLReader::readInnerXml() doesn't reach hard limit
+--EXTENSIONS--
+xmlreader
+--INI--
+memory_limit=2M
+--SKIPIF--
+<?php
+if (getenv('SKIP_ASAN') && LIBXML_VERSION < 21000) die('skip xmlTextReaderReadInnerXml() in versions <2.10.0 is slow under ASan');
+?>
+--FILE--
+<?php
+$path = __DIR__ . '/' . basename(__FILE__, '.php') . '.tmp';
+$fp = fopen($path, 'wb');
+/* ~11 MB of small elements: no single node is huge, but the whole
+ * subtree is, and it is serialized into one Zend string. */
+$piece = str_repeat("<a>xxxxxxxx</a>", 32768);
+fwrite($fp, "<r>");
+for ($i = 0; $i < 12; $i++) {
+    fwrite($fp, $piece);
+}
+fwrite($fp, "</r>");
+unset($piece);
+fclose($fp);
+
+$reader = XMLReader::open($path);
+$reader->read();
+$reader->readInnerXml();
+echo "not reached\n";
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.tmp');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 2097152 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/xmlreader/tests/oom_xmlreader_readouterxml.phpt b/ext/xmlreader/tests/oom_xmlreader_readouterxml.phpt
new file mode 100644
index 00000000000..7cece6eb7c7
--- /dev/null
+++ b/ext/xmlreader/tests/oom_xmlreader_readouterxml.phpt
@@ -0,0 +1,32 @@
+--TEST--
+XMLReader::readOuterXml() doesn't reach hard limit
+--EXTENSIONS--
+xmlreader
+--INI--
+memory_limit=4M
+--FILE--
+<?php
+$path = __DIR__ . '/' . basename(__FILE__, '.php') . '.tmp';
+$fp = fopen($path, 'wb');
+/* ~11 MB of small elements: no single node is huge, but the whole
+ * subtree is, and it is serialized into one Zend string. */
+$piece = str_repeat("<a>xxxxxxxx</a>", 32768);
+fwrite($fp, "<r>");
+for ($i = 0; $i < 24; $i++) {
+    fwrite($fp, $piece);
+}
+fwrite($fp, "</r>");
+unset($piece);
+fclose($fp);
+
+$reader = XMLReader::open($path);
+$reader->read();
+$reader->readOuterXml();
+echo "not reached\n";
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.tmp');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 4194304 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/xmlreader/tests/oom_xmlreader_readstring.phpt b/ext/xmlreader/tests/oom_xmlreader_readstring.phpt
new file mode 100644
index 00000000000..f60b63aef01
--- /dev/null
+++ b/ext/xmlreader/tests/oom_xmlreader_readstring.phpt
@@ -0,0 +1,36 @@
+--TEST--
+XMLReader::readString() doesn't reach hard limit
+--EXTENSIONS--
+xmlreader
+--INI--
+memory_limit=2M
+--SKIPIF--
+<?php
+if (getenv('SKIP_ASAN') && LIBXML_VERSION < 21000) die('skip xmlTextReaderReadString() in versions <2.10.0 is slow under ASan');
+?>
+--FILE--
+<?php
+$path = __DIR__ . '/' . basename(__FILE__, '.php') . '.tmp';
+$fp = fopen($path, 'wb');
+/* ~11 MB of small elements: no single node is huge, but the whole
+ * subtree is, and it is serialized into one Zend string. */
+$piece = str_repeat("<a>xxxxxxxx</a>", 32768);
+fwrite($fp, "<r>");
+for ($i = 0; $i < 12; $i++) {
+    fwrite($fp, $piece);
+}
+fwrite($fp, "</r>");
+unset($piece);
+fclose($fp);
+
+$reader = XMLReader::open($path);
+$reader->read();
+$reader->readString();
+echo "not reached\n";
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.tmp');
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 2097152 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/xmlwriter/php_xmlwriter.c b/ext/xmlwriter/php_xmlwriter.c
index d7ac7387b88..6baa51ce53f 100644
--- a/ext/xmlwriter/php_xmlwriter.c
+++ b/ext/xmlwriter/php_xmlwriter.c
@@ -1005,7 +1005,11 @@ static void php_xmlwriter_flush(INTERNAL_FUNCTION_PARAMETERS, int force_string)
 		output_bytes = xmlTextWriterFlush(ptr);
 		if (buffer) {
 			const xmlChar *content = xmlBufferContent(buffer);
-			RETVAL_STRING((const char *) content);
+			zend_string *ret = zend_string_finit((const char *) content, strlen((const char *) content), /* persistent */ false, /* fallible */ true);
+			if (UNEXPECTED(!ret)) {
+				RETURN_OOM();
+			}
+			RETVAL_NEW_STR(ret);
 			if (empty) {
 				xmlBufferEmpty(buffer);
 			}
diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c
index b6f833c231f..a6cba8ddf40 100644
--- a/ext/zend_test/test.c
+++ b/ext/zend_test/test.c
@@ -31,6 +31,8 @@
 #include "zend_weakrefs.h"
 #include "Zend/Optimizer/zend_optimizer.h"
 #include "Zend/zend_alloc.h"
+#include "SAPI.h"
+#include "rfc1867.h"
 #include "test.h"
 #include "test_arginfo.h"

@@ -155,6 +157,44 @@ static ZEND_FUNCTION(zend_leak_bytes)
 	emalloc(leakbytes);
 }

+static ZEND_FUNCTION(zend_test_emalloc)
+{
+	zend_long size;
+
+	ZEND_PARSE_PARAMETERS_START(1, 1)
+		Z_PARAM_LONG(size)
+	ZEND_PARSE_PARAMETERS_END();
+
+	void *ptr = emalloc((size_t)size);
+
+	ZEND_ASSERT(ptr);
+	memset(ptr, 0, (size_t)size);
+
+	efree(ptr);
+}
+
+static ZEND_FUNCTION(zend_test_callback)
+{
+	zend_fcall_info fci = empty_fcall_info;
+	zend_fcall_info_cache fci_cache = empty_fcall_info_cache;
+
+	ZEND_PARSE_PARAMETERS_START(1, 1)
+		Z_PARAM_FUNC(fci, fci_cache)
+	ZEND_PARSE_PARAMETERS_END();
+
+	void *p = malloc(1024);
+
+	zval result;
+	fci.retval = &result;
+	fci.param_count = 0;
+	fci.params = NULL;
+	zend_call_function(&fci, &fci_cache);
+	zval_ptr_dtor(&result);
+
+	/* Reachable via unwind exit. */
+	free(p);
+}
+
 /* Leak a refcounted variable */
 static ZEND_FUNCTION(zend_leak_variable)
 {
@@ -650,6 +690,123 @@ static ZEND_FUNCTION(zend_test_log_err_debug)
 	php_log_err_with_severity(ZSTR_VAL(str), LOG_DEBUG);
 }

+static php_stream *zend_test_post_stream = NULL;
+
+/* Give the POST handler a fresh $_POST or $_FILES to fill in. A request parses
+ * its body before creating the auto globals, so the handler expects an array
+ * that nothing else holds a reference to yet. */
+static void zend_test_reset_track_vars(int track_vars, const char *name, size_t name_len)
+{
+	/* Create the auto global first, so that it is not created, and reset, on
+	 * first access after this. */
+	zend_is_auto_global_str(name, name_len);
+
+	zval *zv = &PG(http_globals)[track_vars];
+
+	zval_ptr_dtor_nogc(zv);
+	array_init(zv);
+}
+
+/* Point $_POST or $_FILES at the array the POST handler filled in. */
+static void zend_test_publish_track_vars(int track_vars, const char *name, size_t name_len)
+{
+	zval *zv = &PG(http_globals)[track_vars];
+
+	zend_hash_str_update(&EG(symbol_table), name, name_len, zv);
+	Z_ADDREF_P(zv);
+}
+
+static size_t zend_test_read_post(char *buffer, size_t count_bytes)
+{
+	ssize_t read_bytes = php_stream_read(zend_test_post_stream, buffer, count_bytes);
+
+	return read_bytes > 0 ? (size_t) read_bytes : 0;
+}
+
+/* Run the POST handler registered for the content type over the contents of a
+ * stream, as if that stream were the body of the current request. */
+static ZEND_FUNCTION(zend_test_handle_post_data)
+{
+	zval *stream_zv;
+	char *content_type;
+	size_t content_type_len;
+	zend_long content_length;
+	php_stream *stream;
+
+	ZEND_PARSE_PARAMETERS_START(3, 3)
+		Z_PARAM_RESOURCE(stream_zv);
+		Z_PARAM_STRING(content_type, content_type_len);
+		Z_PARAM_LONG(content_length);
+	ZEND_PARSE_PARAMETERS_END();
+
+	php_stream_from_zval(stream, stream_zv);
+
+	zend_test_reset_track_vars(TRACK_VARS_POST, ZEND_STRL("_POST"));
+	zend_test_reset_track_vars(TRACK_VARS_FILES, ZEND_STRL("_FILES"));
+
+	/* Tear down what a previous call set up, as the request shutdown would. */
+	if (SG(rfc1867_uploaded_files)) {
+		destroy_uploaded_files_hash();
+	}
+
+	php_stream *old_stream = zend_test_post_stream;
+	size_t (*old_read_post)(char *buffer, size_t count_bytes) = sapi_module.read_post;
+	sapi_request_info old_request_info = SG(request_info);
+	int64_t old_read_post_bytes = SG(read_post_bytes);
+	unsigned char old_post_read = SG(post_read);
+
+	zend_test_post_stream = stream;
+	sapi_module.read_post = zend_test_read_post;
+	/* php_default_post_reader() insists on a request method. */
+	SG(request_info).request_method = "POST";
+	SG(request_info).content_type = content_type;
+	SG(request_info).content_length = content_length;
+	SG(request_info).content_type_dup = NULL;
+	SG(request_info).post_entry = NULL;
+	SG(request_info).request_body = NULL;
+	SG(read_post_bytes) = 0;
+	SG(post_read) = 0;
+
+	bool bailed_out = false;
+
+	zend_try {
+		/* Pick the post handler for the content type, read the body, then run
+		 * the handler over it. Anything else is swallowed by the default post
+		 * reader, as it would be during a request. */
+		sapi_read_post_data();
+		sapi_handle_post(&PG(http_globals)[TRACK_VARS_POST]);
+	} zend_catch {
+		bailed_out = true;
+	} zend_end_try();
+
+	zend_test_publish_track_vars(TRACK_VARS_POST, ZEND_STRL("_POST"));
+	zend_test_publish_track_vars(TRACK_VARS_FILES, ZEND_STRL("_FILES"));
+
+	if (SG(request_info).request_body) {
+		php_stream_close(SG(request_info).request_body);
+	}
+	if (SG(request_info).content_type_dup) {
+		efree(SG(request_info).content_type_dup);
+	}
+
+	zend_test_post_stream = old_stream;
+	sapi_module.read_post = old_read_post;
+	SG(request_info) = old_request_info;
+	SG(read_post_bytes) = old_read_post_bytes;
+	SG(post_read) = old_post_read;
+
+	if (bailed_out) {
+		zend_bailout();
+	}
+}
+
+static ZEND_FUNCTION(zend_new_singleton_array)
+{
+	ZEND_PARSE_PARAMETERS_NONE();
+
+	array_init_singleton(return_value);
+}
+
 static zend_object *zend_test_class_new(zend_class_entry *class_type)
 {
 	zend_object *obj = zend_objects_new(class_type);
diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php
index 547e82d60ca..2df7d547e45 100644
--- a/ext/zend_test/test.stub.php
+++ b/ext/zend_test/test.stub.php
@@ -149,6 +149,10 @@ function zend_leak_variable(mixed $variable): void {}

     function zend_leak_bytes(int $bytes = 3): void {}

+    function zend_test_emalloc(int $size): void {}
+
+    function zend_test_callback(callable $callback): void {}
+
     function zend_string_or_object(object|string $param): object|string {}

     function zend_string_or_object_or_null(object|string|null $param): object|string|null {}
@@ -198,6 +202,11 @@ function zend_test_cast_fread($stream): void {}
     function zend_test_is_zend_ptr(int $addr): bool {}

     function zend_test_log_err_debug(string $str): void {}
+
+    /* @param resource $stream */
+    function zend_test_handle_post_data(mixed $stream, string $content_type, int $content_length): void {}
+
+    function zend_new_singleton_array(): array {}
 }

 namespace ZendTestNS {
diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h
index bd86f090ff1..32583414195 100644
Binary files a/ext/zend_test/test_arginfo.h and b/ext/zend_test/test_arginfo.h differ
diff --git a/ext/zend_test/tests/observer_bug81430_2.phpt b/ext/zend_test/tests/observer_bug81430_2.phpt
index 11a9f061b3d..23e66445e90 100644
--- a/ext/zend_test/tests/observer_bug81430_2.phpt
+++ b/ext/zend_test/tests/observer_bug81430_2.phpt
@@ -3,21 +3,16 @@
 --EXTENSIONS--
 zend_test
 --INI--
-memory_limit=20M
 zend_test.observer.enabled=1
 zend_test.observer.show_output=1
 zend_test.observer.observe_all=1
---SKIPIF--
-<?php
-if (getenv("USE_ZEND_ALLOC") === "0") die("skip requires zmm");
-?>
 --FILE--
 <?php

 #[\Attribute]
 class A {
     public function __construct() {
-        array_map("str_repeat", ["\xFF"], [100000000]); // cause a bailout
+        var_dump(new class { public function __debugInfo() { return ''; } }); // cause a bailout
     }
 }

@@ -40,14 +35,14 @@ function B() {}
   <ReflectionAttribute::newInstance>
     <!-- init A::__construct() -->
     <A::__construct>
-      <!-- init array_map() -->
-      <array_map>
-        <!-- init str_repeat() -->
-        <str_repeat>
+      <!-- init var_dump() -->
+      <var_dump>
+        <!-- init class@anonymous::__debugInfo() -->
+        <class@anonymous::__debugInfo>
+        </class@anonymous::__debugInfo>

-Fatal error: Allowed memory size of %d bytes exhausted %s in %s on line %d
-        </str_repeat>
-      </array_map>
+Fatal error: __debuginfo() must return an array in %s on line %d
+      </var_dump>
     </A::__construct>
   </ReflectionAttribute::newInstance>
 </file '%s'>
diff --git a/ext/zend_test/tests/observer_bug81435.phpt b/ext/zend_test/tests/observer_bug81435.phpt
index 26224e59ec5..e0c5d5bc80d 100644
--- a/ext/zend_test/tests/observer_bug81435.phpt
+++ b/ext/zend_test/tests/observer_bug81435.phpt
@@ -3,15 +3,10 @@
 --EXTENSIONS--
 zend_test
 --INI--
-memory_limit=20M
 zend_test.observer.enabled=1
 zend_test.observer.show_output=1
 zend_test.observer.observe_function_names=a,d
 opcache.optimization_level=0
---SKIPIF--
-<?php
-if (getenv("USE_ZEND_ALLOC") === "0") die("skip requires zmm");
-?>
 --FILE--
 <?php

@@ -26,7 +21,7 @@ function b() {
 }

 function bailout(...$args) {
-    array_map("str_repeat", ["\xFF"], [100000000]);
+    var_dump(new class { public function __debugInfo() { return ''; } }); // cause a bailout
 }

 function a() { // observed (first_observed_frame)
@@ -47,8 +42,8 @@ function a() { // observed (first_observed_frame)
   <d>
   </d>
   <!-- init bailout() -->
-  <!-- init array_map() -->
-  <!-- init str_repeat() -->
+  <!-- init var_dump() -->
+  <!-- init class@anonymous::__debugInfo() -->

-Fatal error: Allowed memory size of 20971520 bytes exhausted %s in %s on line %d
+Fatal error: __debuginfo() must return an array in %s on line %d
 </a>
diff --git a/ext/zend_test/tests/observer_error_01.phpt b/ext/zend_test/tests/observer_error_01.phpt
index d20a23b00b2..4f8b97bf457 100644
--- a/ext/zend_test/tests/observer_error_01.phpt
+++ b/ext/zend_test/tests/observer_error_01.phpt
@@ -7,16 +7,11 @@
 zend_test.observer.show_output=1
 zend_test.observer.observe_all=1
 zend_test.observer.show_return_value=1
-memory_limit=2M
---SKIPIF--
-<?php
-if (getenv("USE_ZEND_ALLOC") === "0") die("skip requires zmm");
-?>
 --FILE--
 <?php
 function foo()
 {
-    str_repeat('.', 1024 * 1024 * 2); // 2MB
+    var_dump(new class { public function __debugInfo() { return ''; } }); // cause a bailout
 }

 foo();
@@ -28,10 +23,13 @@ function foo()
 <file '%s'>
   <!-- init foo() -->
   <foo>
-    <!-- init str_repeat() -->
-    <str_repeat>
+    <!-- init var_dump() -->
+    <var_dump>
+      <!-- init class@anonymous::__debugInfo() -->
+      <class@anonymous::__debugInfo>
+      </class@anonymous::__debugInfo:''>

-Fatal error: Allowed memory size of 2097152 bytes exhausted%s(tried to allocate %d bytes) in %s on line %d
-    </str_repeat:NULL>
+Fatal error: __debuginfo() must return an array in %s on line %d
+    </var_dump:NULL>
   </foo:NULL>
 </file '%s%eobserver_error_%d.php'>
diff --git a/ext/zend_test/tests/observer_fiber_functions_03.phpt b/ext/zend_test/tests/observer_fiber_functions_03.phpt
index 147e6f7229d..151f50d8b5f 100644
--- a/ext/zend_test/tests/observer_fiber_functions_03.phpt
+++ b/ext/zend_test/tests/observer_fiber_functions_03.phpt
@@ -9,11 +9,6 @@
 zend_test.observer.fiber_init=1
 zend_test.observer.fiber_switch=1
 zend_test.observer.fiber_destroy=1
-memory_limit=100M
---SKIPIF--
-<?php
-if (getenv("USE_ZEND_ALLOC") === "0") die("skip requires ZendMM because it uses the memory limit");
-?>
 --FILE--
 <?php

@@ -27,7 +22,7 @@
 $fiber = new Fiber(function (): void {
     var_dump(2);
     Fiber::suspend();
-    str_repeat('A', 200_000_000);
+    var_dump(new class { public function __debugInfo() { return ''; } }); // cause a bailout
 });

 $fiber->start();
@@ -76,11 +71,13 @@
 <!-- switching from fiber %s to %s -->
 <resume '%s'>
           </Fiber::suspend>
-          <!-- init str_repeat() -->
-          <str_repeat>
+          <var_dump>
+            <!-- init class@anonymous::__debugInfo() -->
+            <class@anonymous::__debugInfo>
+            </class@anonymous::__debugInfo>

-Fatal error: Allowed memory size of 104857600 bytes exhausted %s on line %d
-          </str_repeat>
+Fatal error: __debuginfo() must return an array in %s on line %d
+          </var_dump>
         </{closure}>
 <!-- switching from fiber %s to %s -->
 <returned '%s'>
diff --git a/ext/zend_test/tests/zend_test_handle_post_data.phpt b/ext/zend_test/tests/zend_test_handle_post_data.phpt
new file mode 100644
index 00000000000..a0c8cbc9acb
--- /dev/null
+++ b/ext/zend_test/tests/zend_test_handle_post_data.phpt
@@ -0,0 +1,85 @@
+--TEST--
+zend_test_handle_post_data() parses POST data from a stream
+--EXTENSIONS--
+zend_test
+--FILE--
+<?php
+
+function stream(string $body) {
+    $stream = fopen('php://memory', 'r+');
+    fwrite($stream, $body);
+    rewind($stream);
+    return $stream;
+}
+
+$body = "--b\r\n"
+    . "Content-Disposition: form-data; name=\"foo\"\r\n"
+    . "\r\n"
+    . "bar\r\n"
+    . "--b\r\n"
+    . "Content-Disposition: form-data; name=\"file\"; filename=\"f.txt\"\r\n"
+    . "Content-Type: text/plain\r\n"
+    . "\r\n"
+    . "file contents\r\n"
+    . "--b--\r\n";
+
+zend_test_handle_post_data(stream($body), 'multipart/form-data; boundary=b', strlen($body));
+
+var_dump($_POST);
+var_dump($_FILES);
+var_dump(is_uploaded_file($_FILES['file']['tmp_name']));
+var_dump(file_get_contents($_FILES['file']['tmp_name']));
+
+// Each call starts over with a fresh $_POST and $_FILES.
+$body = "a=1&b[]=2&b[]=3";
+zend_test_handle_post_data(stream($body), 'application/x-www-form-urlencoded', strlen($body));
+
+var_dump($_POST);
+var_dump($_FILES);
+
+// No handler for this content type, the body is swallowed.
+$body = '{"a":1}';
+zend_test_handle_post_data(stream($body), 'application/json', strlen($body));
+
+var_dump($_POST);
+
+?>
+--EXPECTF--
+array(1) {
+  ["foo"]=>
+  string(3) "bar"
+}
+array(1) {
+  ["file"]=>
+  array(6) {
+    ["name"]=>
+    string(5) "f.txt"
+    ["full_path"]=>
+    string(5) "f.txt"
+    ["type"]=>
+    string(10) "text/plain"
+    ["tmp_name"]=>
+    string(%d) "%s"
+    ["error"]=>
+    int(0)
+    ["size"]=>
+    int(13)
+  }
+}
+bool(true)
+string(13) "file contents"
+array(2) {
+  ["a"]=>
+  string(1) "1"
+  ["b"]=>
+  array(2) {
+    [0]=>
+    string(1) "2"
+    [1]=>
+    string(1) "3"
+  }
+}
+array(0) {
+}
+array(0) {
+}
diff --git a/ext/zlib/tests/oom_deflate_add.phpt b/ext/zlib/tests/oom_deflate_add.phpt
new file mode 100644
index 00000000000..608762109c6
--- /dev/null
+++ b/ext/zlib/tests/oom_deflate_add.phpt
@@ -0,0 +1,34 @@
+--TEST--
+deflate_add() output buffer pre-allocation doesn't reach hard limit
+--EXTENSIONS--
+zlib
+--INI--
+memory_limit=512M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+$limit = 512 * 1024 * 1024;
+$hard = 2 * $limit;
+
+// Allocate the deflate state first so that it counts towards the baseline.
+$ctx = deflate_init(ZLIB_ENCODING_RAW);
+
+// Fill the heap to just under the memory limit.
+$subject = str_repeat("a", $limit - memory_get_usage(true) - 64 * 1024);
+
+// deflate_add() pre-allocates PHP_ZLIB_BUFFER_SIZE_GUESS() bytes for its
+// output, 1.5% more than the chunk, which leaves it too big for the gap that is
+// left before the hard limit.
+$out_size = (int) (strlen($subject) * 1.015) + 23;
+var_dump($out_size > $hard - memory_get_usage(true));
+
+deflate_add($ctx, $subject, ZLIB_FINISH);
+echo "not reached\n";
+?>
+--EXPECTF--
+bool(true)
+
+Fatal error: Allowed memory size of 536870912 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/zlib/tests/oom_gzcompress.phpt b/ext/zlib/tests/oom_gzcompress.phpt
new file mode 100644
index 00000000000..88fc919b13c
--- /dev/null
+++ b/ext/zlib/tests/oom_gzcompress.phpt
@@ -0,0 +1,32 @@
+--TEST--
+gzcompress() output buffer pre-allocation doesn't reach hard limit
+--EXTENSIONS--
+zlib
+--INI--
+memory_limit=512M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+$limit = 512 * 1024 * 1024;
+$hard = 2 * $limit;
+
+// Fill the heap to just under the memory limit.
+$subject = str_repeat("a", $limit - memory_get_usage(true) - 64 * 1024);
+
+// php_zlib_encode() pre-allocates PHP_ZLIB_BUFFER_SIZE_GUESS() bytes for its
+// output, 1.5% more than the source, which leaves it too big for the gap that
+// is left before the hard limit.
+$out_size = (int) (strlen($subject) * 1.015) + 23;
+var_dump($out_size > $hard - memory_get_usage(true));
+
+// gzdeflate(), gzencode() and zlib_encode() go through the same helper.
+gzcompress($subject);
+echo "not reached\n";
+?>
+--EXPECTF--
+bool(true)
+
+Fatal error: Allowed memory size of 536870912 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/zlib/tests/oom_gzdecode.phpt b/ext/zlib/tests/oom_gzdecode.phpt
new file mode 100644
index 00000000000..cc4577c4c88
--- /dev/null
+++ b/ext/zlib/tests/oom_gzdecode.phpt
@@ -0,0 +1,26 @@
+--TEST--
+gzdecode() doesn't reach hard limit
+--EXTENSIONS--
+zlib
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+// Expands to 32 MB, well past the 16 MB hard limit.
+$ctx = deflate_init(ZLIB_ENCODING_GZIP, ["level" => 9]);
+$chunk = str_repeat("\0", 65536);
+$gz = "";
+for ($i = 0; $i < 512; $i++) {
+    $gz .= deflate_add($ctx, $chunk, ZLIB_NO_FLUSH);
+}
+$gz .= deflate_add($ctx, "", ZLIB_FINISH);
+unset($chunk);
+
+var_dump(gzdecode($gz));
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/zlib/tests/oom_gzfile.phpt b/ext/zlib/tests/oom_gzfile.phpt
new file mode 100644
index 00000000000..989a7e8e607
--- /dev/null
+++ b/ext/zlib/tests/oom_gzfile.phpt
@@ -0,0 +1,35 @@
+--TEST--
+gzfile() doesn't reach hard limit
+--EXTENSIONS--
+zlib
+--INI--
+memory_limit=64M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+$file = __DIR__ . "/oom_gzfile.gz";
+
+// Build 12 MB of one-byte lines as a ~12 KB gzip file without holding it in memory.
+$ctx = deflate_init(ZLIB_ENCODING_GZIP, ["level" => 9]);
+$chunk = str_repeat("a\n", 32768);
+$gz = "";
+for ($i = 0; $i < 192; $i++) {
+    $gz .= deflate_add($ctx, $chunk, ZLIB_NO_FLUSH);
+}
+$gz .= deflate_add($ctx, "", ZLIB_FINISH);
+file_put_contents($file, $gz);
+unset($gz, $chunk, $ctx);
+
+ini_set("memory_limit", "8M");
+
+var_dump(gzfile($file));
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/oom_gzfile.gz");
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/zlib/tests/oom_gzgets.phpt b/ext/zlib/tests/oom_gzgets.phpt
new file mode 100644
index 00000000000..80216e8e104
--- /dev/null
+++ b/ext/zlib/tests/oom_gzgets.phpt
@@ -0,0 +1,36 @@
+--TEST--
+gzgets() doesn't reach hard limit
+--EXTENSIONS--
+zlib
+--INI--
+memory_limit=64M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+$file = __DIR__ . "/oom_gzgets.gz";
+
+// Build 12 MB of zeros as a ~12 KB gzip file without holding it in memory.
+$ctx = deflate_init(ZLIB_ENCODING_GZIP, ["level" => 9]);
+$chunk = str_repeat("\0", 65536);
+$gz = "";
+for ($i = 0; $i < 192; $i++) {
+    $gz .= deflate_add($ctx, $chunk, ZLIB_NO_FLUSH);
+}
+$gz .= deflate_add($ctx, "", ZLIB_FINISH);
+file_put_contents($file, $gz);
+unset($gz, $chunk, $ctx);
+
+ini_set("memory_limit", "4M");
+
+$fp = gzopen($file, "rb");
+var_dump(gzgets($fp));
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/oom_gzgets.gz");
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/zlib/tests/oom_gzpassthru.phpt b/ext/zlib/tests/oom_gzpassthru.phpt
new file mode 100644
index 00000000000..5384f2cb08b
--- /dev/null
+++ b/ext/zlib/tests/oom_gzpassthru.phpt
@@ -0,0 +1,37 @@
+--TEST--
+gzpassthru() doesn't reach hard limit
+--EXTENSIONS--
+zlib
+--INI--
+memory_limit=64M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+$file = __DIR__ . "/oom_gzpassthru.gz";
+
+// Build 12 MB of zeros as a ~12 KB gzip file without holding it in memory.
+$ctx = deflate_init(ZLIB_ENCODING_GZIP, ["level" => 9]);
+$chunk = str_repeat("\0", 65536);
+$gz = "";
+for ($i = 0; $i < 192; $i++) {
+    $gz .= deflate_add($ctx, $chunk, ZLIB_NO_FLUSH);
+}
+$gz .= deflate_add($ctx, "", ZLIB_FINISH);
+file_put_contents($file, $gz);
+unset($gz, $chunk, $ctx);
+
+ini_set("memory_limit", "4M");
+
+ob_start();
+$fp = gzopen($file, "rb");
+gzpassthru($fp);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/oom_gzpassthru.gz");
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/zlib/tests/oom_inflate_add.phpt b/ext/zlib/tests/oom_inflate_add.phpt
new file mode 100644
index 00000000000..536dc23832b
--- /dev/null
+++ b/ext/zlib/tests/oom_inflate_add.phpt
@@ -0,0 +1,27 @@
+--TEST--
+inflate_add() doesn't reach hard limit
+--EXTENSIONS--
+zlib
+--INI--
+memory_limit=8M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+// Expands to 32 MB, well past the 16 MB hard limit.
+$ctx = deflate_init(ZLIB_ENCODING_GZIP, ["level" => 9]);
+$chunk = str_repeat("\0", 65536);
+$gz = "";
+for ($i = 0; $i < 512; $i++) {
+    $gz .= deflate_add($ctx, $chunk, ZLIB_NO_FLUSH);
+}
+$gz .= deflate_add($ctx, "", ZLIB_FINISH);
+unset($chunk);
+
+$r = inflate_init(ZLIB_ENCODING_GZIP);
+var_dump(inflate_add($r, $gz, ZLIB_FINISH));
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/zlib/tests/oom_readgzfile.phpt b/ext/zlib/tests/oom_readgzfile.phpt
new file mode 100644
index 00000000000..b3d5ad4b2a7
--- /dev/null
+++ b/ext/zlib/tests/oom_readgzfile.phpt
@@ -0,0 +1,36 @@
+--TEST--
+readgzfile() doesn't reach hard limit
+--EXTENSIONS--
+zlib
+--INI--
+memory_limit=64M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+$file = __DIR__ . "/oom_readgzfile.gz";
+
+// Build 12 MB of zeros as a ~12 KB gzip file without holding it in memory.
+$ctx = deflate_init(ZLIB_ENCODING_GZIP, ["level" => 9]);
+$chunk = str_repeat("\0", 65536);
+$gz = "";
+for ($i = 0; $i < 192; $i++) {
+    $gz .= deflate_add($ctx, $chunk, ZLIB_NO_FLUSH);
+}
+$gz .= deflate_add($ctx, "", ZLIB_FINISH);
+file_put_contents($file, $gz);
+unset($gz, $chunk, $ctx);
+
+ini_set("memory_limit", "4M");
+
+ob_start();
+readgzfile($file);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/oom_readgzfile.gz");
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/zlib/tests/oom_zlib_inflate_filter.phpt b/ext/zlib/tests/oom_zlib_inflate_filter.phpt
new file mode 100644
index 00000000000..bc9a5d322c4
--- /dev/null
+++ b/ext/zlib/tests/oom_zlib_inflate_filter.phpt
@@ -0,0 +1,37 @@
+--TEST--
+zlib.inflate stream filter doesn't reach hard limit
+--EXTENSIONS--
+zlib
+--INI--
+memory_limit=64M
+--SKIPIF--
+<?php
+if (getenv("USE_ZEND_ALLOC") === "0") die("skip Zend MM disabled");
+?>
+--FILE--
+<?php
+$file = __DIR__ . "/oom_zlib_inflate_filter.gz";
+
+// Build 12 MB of zeros as a ~12 KB gzip file without holding it in memory.
+$ctx = deflate_init(ZLIB_ENCODING_GZIP, ["level" => 9]);
+$chunk = str_repeat("\0", 65536);
+$gz = "";
+for ($i = 0; $i < 192; $i++) {
+    $gz .= deflate_add($ctx, $chunk, ZLIB_NO_FLUSH);
+}
+$gz .= deflate_add($ctx, "", ZLIB_FINISH);
+file_put_contents($file, $gz);
+unset($gz, $chunk, $ctx);
+
+ini_set("memory_limit", "4M");
+
+$fp = fopen($file, "rb");
+stream_filter_append($fp, "zlib.inflate", STREAM_FILTER_READ, ["window" => 0x2f]);
+var_dump(fgets($fp));
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/oom_zlib_inflate_filter.gz");
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of %d bytes exhausted %S(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/zlib/zlib.c b/ext/zlib/zlib.c
index e85db94801a..7afbda46e18 100644
--- a/ext/zlib/zlib.c
+++ b/ext/zlib/zlib.c
@@ -153,6 +153,9 @@ static int php_zlib_output_encoding(void)
 	zval *enc;

 	if (!ZLIBG(compression_coding)) {
+		if (!EG(active)) {
+			return 0;
+		}
 		if ((Z_TYPE(PG(http_globals)[TRACK_VARS_SERVER]) == IS_ARRAY || zend_is_auto_global(ZSTR_KNOWN(ZEND_STR_AUTOGLOBAL_SERVER))) &&
 			(enc = zend_hash_str_find(Z_ARRVAL(PG(http_globals)[TRACK_VARS_SERVER]), "HTTP_ACCEPT_ENCODING", sizeof("HTTP_ACCEPT_ENCODING") - 1))) {
 			convert_to_string(enc);
@@ -394,7 +397,11 @@ static zend_string *php_zlib_encode(const char *in_buf, size_t in_len, int encod
 	Z.zfree = php_zlib_free;

 	if (Z_OK == (status = deflateInit2(&Z, level, Z_DEFLATED, encoding, MAX_MEM_LEVEL, Z_DEFAULT_STRATEGY))) {
-		out = zend_string_alloc(PHP_ZLIB_BUFFER_SIZE_GUESS(in_len), 0);
+		out = zend_string_falloc(PHP_ZLIB_BUFFER_SIZE_GUESS(in_len), 0, /* fallible */ true);
+		if (UNEXPECTED(!out)) {
+			deflateEnd(&Z);
+			return NULL;
+		}

 		Z.next_in = (Bytef *) in_buf;
 		Z.next_out = (Bytef *) ZSTR_VAL(out);
@@ -431,7 +438,7 @@ static inline int php_zlib_inflate_rounds(z_stream *Z, size_t max, char **buf, s
 	buffer.size = (max && (max < Z->avail_in)) ? max : Z->avail_in;

 	do {
-		if ((max && (max <= buffer.used)) || !(buffer.aptr = erealloc_recoverable(buffer.data, buffer.size))) {
+		if ((max && (max <= buffer.used)) || !(buffer.aptr = safe_ferealloc(buffer.data, 1, buffer.size, 0, /* fallible */ true))) {
 			status = Z_MEM_ERROR;
 		} else {
 			buffer.data = buffer.aptr;
@@ -638,6 +645,10 @@ PHP_FUNCTION(gzfile)
 	memset(buf, 0, sizeof(buf));

 	while (php_stream_gets(stream, buf, sizeof(buf) - 1) != NULL) {
+		if (ZEND_HAS_OOM()) {
+			php_stream_close(stream);
+			RETURN_OOM();
+		}
 		add_index_string(return_value, i++, buf);
 	}
 	php_stream_close(stream);
@@ -931,6 +942,22 @@ PHP_FUNCTION(inflate_init)
 }
 /* }}} */

+/* Releases *out and returns false on failure. */
+static bool php_zlib_inflate_grow(zend_string **out, size_t buffer_used, size_t chunk_size, z_stream *Z)
+{
+	zend_string *grown = zend_string_frealloc(*out, ZSTR_LEN(*out) + chunk_size, 0, /* fallible */ true);
+
+	if (UNEXPECTED(grown == NULL)) {
+		zend_string_release_ex(*out, 0);
+		return false;
+	}
+
+	*out = grown;
+	Z->avail_out = chunk_size;
+	Z->next_out = (Bytef *) ZSTR_VAL(grown) + buffer_used;
+	return true;
+}
+
 /* {{{ Incrementally inflate encoded data in the specified context */
 PHP_FUNCTION(inflate_add)
 {
@@ -989,9 +1016,9 @@ PHP_FUNCTION(inflate_add)
 			case Z_OK:
 				if (ctx->Z.avail_out == 0) {
 					/* more output buffer space needed; realloc and try again */
-					out = zend_string_realloc(out, ZSTR_LEN(out) + CHUNK_SIZE, 0);
-					ctx->Z.avail_out = CHUNK_SIZE;
-					ctx->Z.next_out = (Bytef *) ZSTR_VAL(out) + buffer_used;
+					if (UNEXPECTED(!php_zlib_inflate_grow(&out, buffer_used, CHUNK_SIZE, &ctx->Z))) {
+						RETURN_OOM();
+					}
 					break;
 				} else {
 					goto complete;
@@ -1001,9 +1028,9 @@ PHP_FUNCTION(inflate_add)
 			case Z_BUF_ERROR:
 				if (flush_type == Z_FINISH && ctx->Z.avail_out == 0) {
 					/* more output buffer space needed; realloc and try again */
-					out = zend_string_realloc(out, ZSTR_LEN(out) + CHUNK_SIZE, 0);
-					ctx->Z.avail_out = CHUNK_SIZE;
-					ctx->Z.next_out = (Bytef *) ZSTR_VAL(out) + buffer_used;
+					if (UNEXPECTED(!php_zlib_inflate_grow(&out, buffer_used, CHUNK_SIZE, &ctx->Z))) {
+						RETURN_OOM();
+					}
 					break;
 				} else {
 					/* No more input data; we're finished */
@@ -1209,7 +1236,10 @@ PHP_FUNCTION(deflate_add)

 	out_size = PHP_ZLIB_BUFFER_SIZE_GUESS(in_len);
 	out_size = (out_size < 64) ? 64 : out_size;
-	out = zend_string_alloc(out_size, 0);
+	out = zend_string_falloc(out_size, 0, /* fallible */ true);
+	if (UNEXPECTED(!out)) {
+		RETURN_OOM();
+	}

 	ctx->Z.next_in = (Bytef *) in_buf;
 	ctx->Z.next_out = (Bytef *) ZSTR_VAL(out);
diff --git a/ext/zlib/zlib_filter.c b/ext/zlib/zlib_filter.c
index 24d418ae04c..abf8176c313 100644
--- a/ext/zlib/zlib_filter.c
+++ b/ext/zlib/zlib_filter.c
@@ -75,6 +75,10 @@ static php_stream_filter_status_t php_zlib_inflate_filter(
 		bucket = php_stream_bucket_make_writeable(buckets_in->head);

 		while (bin < (unsigned int) bucket->buflen && !data->finished) {
+			/* Output is unbounded relative to the input. */
+			if (ZEND_HAS_OOM()) {
+				break;
+			}

 			desired = bucket->buflen - bin;
 			if (desired > data->inbuf_len) {
@@ -121,7 +125,7 @@ static php_stream_filter_status_t php_zlib_inflate_filter(
 	if (!data->finished && flags & PSFS_FLAG_FLUSH_CLOSE) {
 		/* Spit it out! */
 		status = Z_OK;
-		while (status == Z_OK) {
+		while (status == Z_OK && !ZEND_HAS_OOM()) {
 			status = inflate(&(data->strm), Z_FINISH);
 			if (data->strm.avail_out < data->outbuf_len) {
 				size_t bucketlen = data->outbuf_len - data->strm.avail_out;
diff --git a/main/SAPI.c b/main/SAPI.c
index 79014cf3e27..2b78d8bb7d1 100644
--- a/main/SAPI.c
+++ b/main/SAPI.c
@@ -166,10 +166,14 @@ SAPI_API void sapi_handle_post(void *arg)
 		SG(request_info).post_entry->post_handler(SG(request_info).content_type_dup, arg);
 		efree(SG(request_info).content_type_dup);
 		SG(request_info).content_type_dup = NULL;
+
+		if (ZEND_HAS_OOM()) {
+			zend_handle_delayed_oom();
+		}
 	}
 }

-static void sapi_read_post_data(void)
+SAPI_API void sapi_read_post_data(void)
 {
 	sapi_post_entry *post_entry;
 	uint32_t content_type_length = (uint32_t)strlen(SG(request_info).content_type);
@@ -1021,7 +1025,7 @@ SAPI_API zend_stat_t *sapi_get_stat(void)
 SAPI_API char *sapi_getenv(const char *name, size_t name_len)
 {
 	char *value, *tmp;
-
+
 	if (!sapi_module.getenv) {
 		return NULL;
 	}
diff --git a/main/SAPI.h b/main/SAPI.h
index 098b7c8c94c..14acf1a276b 100644
--- a/main/SAPI.h
+++ b/main/SAPI.h
@@ -186,6 +186,7 @@ SAPI_API int sapi_add_header_ex(const char *header_line, size_t header_line_len,
 SAPI_API int sapi_send_headers(void);
 SAPI_API void sapi_free_header(sapi_header_struct *sapi_header);
 SAPI_API void sapi_handle_post(void *arg);
+SAPI_API void sapi_read_post_data(void);
 SAPI_API size_t sapi_read_post_block(char *buffer, size_t buflen);
 SAPI_API int sapi_register_post_entries(const sapi_post_entry *post_entry);
 SAPI_API int sapi_register_post_entry(const sapi_post_entry *post_entry);
diff --git a/main/main.c b/main/main.c
index b38ef8d2ecf..4ff0c034f1b 100644
--- a/main/main.c
+++ b/main/main.c
@@ -282,6 +282,30 @@ static PHP_INI_MH(OnChangeMemoryLimit)
 }
 /* }}} */

+static PHP_INI_MH(OnChangeMemoryHardLimitPercent)
+{
+	zend_long value;
+	if (new_value) {
+		value = zend_ini_parse_quantity_warn(new_value, entry->name);
+	} else {
+		value = -1; /* default: 200% if memory limit < 1G, fixed 1G buffer otherwise */
+	}
+
+	if (value < 100 && value != -1) {
+		zend_error(E_WARNING, "memory_hard_limit_percent must be >= 100, or -1");
+		return FAILURE;
+	}
+
+	if (zend_set_memory_hard_limit_percent(value) == FAILURE) {
+		if (stage != ZEND_INI_STAGE_DEACTIVATE) {
+			zend_error(E_WARNING, "Failed to set memory_hard_limit_percent to " ZEND_LONG_FMT, value);
+			return FAILURE;
+		}
+	}
+
+	return SUCCESS;
+}
+
 /* {{{ PHP_INI_MH */
 static PHP_INI_MH(OnSetLogFilter)
 {
@@ -741,6 +765,7 @@ PHP_INI_BEGIN()
 	STD_PHP_INI_ENTRY("mail.log",					NULL,		PHP_INI_SYSTEM|PHP_INI_PERDIR,		OnUpdateMailLog,			mail_log,			php_core_globals,	core_globals)
 	PHP_INI_ENTRY("browscap",					NULL,		PHP_INI_SYSTEM,		OnChangeBrowscap)
 	PHP_INI_ENTRY("memory_limit",				"128M",		PHP_INI_ALL,		OnChangeMemoryLimit)
+	PHP_INI_ENTRY("memory_hard_limit_percent",	NULL,		PHP_INI_ALL,		OnChangeMemoryHardLimitPercent)
 	PHP_INI_ENTRY("precision",					"14",		PHP_INI_ALL,		OnSetPrecision)
 	PHP_INI_ENTRY("sendmail_from",				NULL,		PHP_INI_ALL,		NULL)
 	PHP_INI_ENTRY("sendmail_path",	DEFAULT_SENDMAIL_PATH,	PHP_INI_SYSTEM,		NULL)
@@ -946,6 +971,10 @@ PHPAPI ZEND_COLD void php_verror(const char *docref, const char *params, int typ
 	zend_string *message;
 	int is_function = 0;

+	if (ZEND_HAS_OOM()) {
+		return;
+	}
+
 	/* get error text into buffer and escape for html if necessary */
 	buffer_len = (int)vspprintf(&buffer, 0, format, args);

@@ -1272,7 +1301,10 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c
 		PG(last_error_lineno) = error_lineno;
 	}

-	if (zend_alloc_in_memory_limit_error_reporting()) {
+	/* Discarding the output buffers frees every handler on the stack, which is
+	 * not safe while one of them is running */
+	if (zend_alloc_in_memory_limit_error_reporting()
+			&& !(php_output_get_status() & PHP_OUTPUT_LOCKED)) {
 		php_output_discard_all();
 	}

@@ -1388,7 +1420,7 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c
 		case E_PARSE:
 		case E_COMPILE_ERROR:
 		case E_USER_ERROR:
-			EG(exit_status) = 255;
+			EG(exit_status) = zend_mm_max_allocs_hit() ? ZEND_MM_MAX_ALLOCS_EXIT_STATUS : 255;
 			if (module_initialized) {
 				if (!PG(display_errors) &&
 				    !SG(headers_sent) &&
@@ -1781,6 +1813,7 @@ zend_result php_request_startup(void)
 		PG(in_user_include) = 0;

 		zend_activate();
+		php_basic_early_rinit();
 		sapi_activate();

 #ifdef ZEND_SIGNALS
@@ -1826,6 +1859,11 @@ zend_result php_request_startup(void)

 	SG(sapi_started) = 1;

+	char *max = getenv("ZEND_MM_MAX_ALLOCS");
+	if (max) {
+		zend_mm_set_max_allocs(ZEND_ATOL(max));
+	}
+
 	return retval;
 }
 /* }}} */
@@ -1836,6 +1874,7 @@ void php_request_shutdown(void *dummy)
 	bool report_memleaks;

 	EG(flags) |= EG_FLAGS_IN_SHUTDOWN;
+	EG(delayed_oom).state = ZEND_OOM_STATE_NONE;

 	report_memleaks = PG(report_memleaks);

@@ -1892,6 +1931,8 @@ void php_request_shutdown(void *dummy)

 		for (i=0; i<NUM_TRACK_VARS; i++) {
 			zval_ptr_dtor(&PG(http_globals)[i]);
+			/* May be accessed before php_request_startup() */
+			ZVAL_UNDEF(&PG(http_globals)[i]);
 		}
 	} zend_end_try();

@@ -1924,9 +1965,11 @@ void php_request_shutdown(void *dummy)
 	/* 15. Free Willy (here be crashes) */
 	zend_arena_destroy(CG(arena));
 	zend_interned_strings_deactivate();
+	clear_last_error();
 	zend_try {
 		shutdown_memory_manager(CG(unclean_shutdown) || !report_memleaks, 0);
 	} zend_end_try();
+	EG(delayed_oom).state = ZEND_OOM_STATE_NONE;

 	/* Reset memory limit, as the reset during INI_STAGE_DEACTIVATE may have failed.
 	 * At this point, no memory beyond a single chunk should be in use. */
@@ -1947,6 +1990,9 @@ void php_request_shutdown(void *dummy)
 #ifdef HAVE_DTRACE
 	DTRACE_REQUEST_SHUTDOWN(SAFE_FILENAME(SG(request_info).path_translated), SAFE_FILENAME(SG(request_info).request_uri), (char *)SAFE_FILENAME(SG(request_info).request_method));
 #endif /* HAVE_DTRACE */
+
+	/* May be accessed before php_request_startup() */
+	PG(modules_activated) = 0;
 }
 /* }}} */

diff --git a/main/php_variables.c b/main/php_variables.c
index 8f74d6cf284..7d8766fdc74 100644
--- a/main/php_variables.c
+++ b/main/php_variables.c
@@ -218,6 +218,13 @@ PHPAPI void php_register_variable_ex(const char *var_name, zval *val, zval *trac
 				return;
 			}

+			/* Each level retains an array, so stop once over the limit. */
+			if (ZEND_HAS_OOM()) {
+				zval_ptr_dtor_nogc(val);
+				free_alloca(var_orig, use_heap);
+				return;
+			}
+
 			ip++;
 			index_s = ip;
 			if (isspace((unsigned char)*ip)) {
@@ -249,7 +256,7 @@ PHPAPI void php_register_variable_ex(const char *var_name, zval *val, zval *trac
 			}

 			if (!index) {
-				array_init(&gpc_element);
+				array_init_singleton(&gpc_element);
 				if ((gpc_element_p = zend_hash_next_index_insert(symtable1, &gpc_element)) == NULL) {
 					zend_array_destroy(Z_ARR(gpc_element));
 					zval_ptr_dtor_nogc(val);
@@ -266,7 +273,7 @@ PHPAPI void php_register_variable_ex(const char *var_name, zval *val, zval *trac
 				gpc_element_p = zend_symtable_str_find(symtable1, index, index_len);
 				if (!gpc_element_p) {
 					zval tmp;
-					array_init(&tmp);
+					array_init_singleton(&tmp);
 					gpc_element_p = zend_symtable_str_update_ind(symtable1, index, index_len, &tmp);
 				} else {
 					if (Z_TYPE_P(gpc_element_p) == IS_INDIRECT) {
@@ -274,7 +281,7 @@ PHPAPI void php_register_variable_ex(const char *var_name, zval *val, zval *trac
 					}
 					if (Z_TYPE_P(gpc_element_p) != IS_ARRAY) {
 						zval_ptr_dtor_nogc(gpc_element_p);
-						array_init(gpc_element_p);
+						array_init_singleton(gpc_element_p);
 					} else {
 						SEPARATE_ARRAY(gpc_element_p);
 					}
@@ -375,6 +382,10 @@ static bool add_post_var(zval *arr, post_var_data_t *var, bool eof)
 	php_url_decode(var->ptr, klen);

 	val = estrndup(ksep, vlen);
+	if (ZEND_HAS_OOM()) {
+		efree(val);
+		return 0;
+	}
 	if (vlen) {
 		vlen = php_url_decode(val, vlen);
 	}
@@ -382,6 +393,10 @@ static bool add_post_var(zval *arr, post_var_data_t *var, bool eof)
 	if (sapi_module.input_filter(PARSE_POST, var->ptr, &val, vlen, &new_vlen)) {
 		php_register_variable_safe(var->ptr, val, new_vlen, arr);
 	}
+	if (ZEND_HAS_OOM()) {
+		efree(val);
+		return 0;
+	}
 	efree(val);

 	var->ptr = vsep + (vsep != var->end);
@@ -403,6 +418,12 @@ static inline int add_post_vars(zval *arr, post_var_data_t *vars, bool eof)
 					max_vars);
 			return FAILURE;
 		}
+		if (ZEND_HAS_OOM()) {
+			return FAILURE;
+		}
+	}
+	if (ZEND_HAS_OOM()) {
+		return FAILURE;
 	}

 	if (!eof && ZSTR_VAL(vars->str.s) != vars->ptr) {
@@ -432,6 +453,11 @@ SAPI_API SAPI_POST_HANDLER_FUNC(php_std_post_handler)
 			if (len > 0) {
 				smart_str_appendl(&post_data.str, buf, len);

+				if (ZEND_HAS_OOM()) {
+					smart_str_free(&post_data.str);
+					return;
+				}
+
 				if (SUCCESS != add_post_vars(arr, &post_data, 0)) {
 					smart_str_free(&post_data.str);
 					return;
@@ -472,7 +498,7 @@ SAPI_API SAPI_TREAT_DATA_FUNC(php_default_treat_data)
 		case PARSE_POST:
 		case PARSE_GET:
 		case PARSE_COOKIE:
-			array_init(&array);
+			array_init_singleton(&array);
 			switch (arg) {
 				case PARSE_POST:
 					zval_ptr_dtor_nogc(&PG(http_globals)[TRACK_VARS_POST]);
diff --git a/main/rfc1867.c b/main/rfc1867.c
index 82f006db961..83591e8466e 100644
--- a/main/rfc1867.c
+++ b/main/rfc1867.c
@@ -51,7 +51,7 @@ static php_rfc1867_basename_t php_rfc1867_basename = NULL;

 PHPAPI int (*php_rfc1867_callback)(unsigned int event, void *event_data, void **extra) = NULL;

-static void safe_php_register_variable(char *var, char *strval, size_t val_len, zval *track_vars_array, bool override_protection);
+static bool safe_php_register_variable(char *var, char *strval, size_t val_len, zval *track_vars_array, bool override_protection);

 /* The longest property name we use in an uploaded file array */
 #define MAX_SIZE_OF_INDEX sizeof("[full_path]")
@@ -140,10 +140,11 @@ static void normalize_protected_variable(char *varname) /* {{{ */
 }
 /* }}} */

-static void add_protected_variable(char *varname) /* {{{ */
+static bool add_protected_variable(char *varname) /* {{{ */
 {
 	normalize_protected_variable(varname);
 	zend_hash_str_add_empty_element(&PG(rfc1867_protected_variables), varname, strlen(varname));
+	return !ZEND_HAS_OOM();
 }
 /* }}} */

@@ -154,31 +155,33 @@ static bool is_protected_variable(char *varname) /* {{{ */
 }
 /* }}} */

-static void safe_php_register_variable(char *var, char *strval, size_t val_len, zval *track_vars_array, bool override_protection) /* {{{ */
+static bool safe_php_register_variable(char *var, char *strval, size_t val_len, zval *track_vars_array, bool override_protection) /* {{{ */
 {
 	if (override_protection || !is_protected_variable(var)) {
 		php_register_variable_safe(var, strval, val_len, track_vars_array);
 	}
+	return !ZEND_HAS_OOM();
 }
 /* }}} */

-static void safe_php_register_variable_ex(char *var, zval *val, zval *track_vars_array, bool override_protection) /* {{{ */
+static bool safe_php_register_variable_ex(char *var, zval *val, zval *track_vars_array, bool override_protection) /* {{{ */
 {
 	if (override_protection || !is_protected_variable(var)) {
 		php_register_variable_ex(var, val, track_vars_array);
 	}
+	return !ZEND_HAS_OOM();
 }
 /* }}} */

-static void register_http_post_files_variable(char *strvar, char *val, zval *http_post_files, bool override_protection) /* {{{ */
+static bool register_http_post_files_variable(char *strvar, char *val, zval *http_post_files, bool override_protection) /* {{{ */
 {
-	safe_php_register_variable(strvar, val, strlen(val), http_post_files, override_protection);
+	return safe_php_register_variable(strvar, val, strlen(val), http_post_files, override_protection);
 }
 /* }}} */

-static void register_http_post_files_variable_ex(char *var, zval *val, zval *http_post_files, bool override_protection) /* {{{ */
+static bool register_http_post_files_variable_ex(char *var, zval *val, zval *http_post_files, bool override_protection) /* {{{ */
 {
-	safe_php_register_variable_ex(var, val, http_post_files, override_protection);
+	return safe_php_register_variable_ex(var, val, http_post_files, override_protection);
 }
 /* }}} */

@@ -243,6 +246,10 @@ static int fill_buffer(multipart_buffer *self)

 	self->buf_begin = self->buffer;

+	if (SG(post_max_size) > 0 && SG(read_post_bytes) > SG(post_max_size)) {
+		return 0;
+	}
+
 	/* calculate the free space in the buffer */
 	bytes_to_read = self->bufsize - self->bytes_in_buffer;

@@ -390,6 +397,14 @@ static int find_boundary(multipart_buffer *self, char *boundary)
 	return 0;
 }

+static bool php_mime_should_save_hdr(const char *name)
+{
+	return !strcasecmp(name, "Content-Disposition")
+		|| !strcasecmp(name, "Content-Type");
+}
+
+static char *php_mime_get_hdr_value(zend_llist header, char *key);
+
 /* parse headers */
 static int multipart_buffer_headers(multipart_buffer *self, zend_llist *header)
 {
@@ -430,15 +445,25 @@ static int multipart_buffer_headers(multipart_buffer *self, zend_llist *header)
 			}

 			*value = '\0';
-			do { value++; } while (isspace((unsigned char)*value));

-			key = estrdup(line);
-			smart_string_appends(&buf_value, value);
+			/* Keep only headers we use, and ignore duplicates */
+			if (!php_mime_should_save_hdr(line)
+					|| php_mime_get_hdr_value(*header, line)) {
+				key = NULL;
+			} else {
+				key = estrdup(line);
+				do { value++; } while (isspace((unsigned char)*value));
+				smart_string_appends(&buf_value, value);
+			}
 		} else if (buf_value.c) { /* If no ':' on the line, add to previous line */
 			smart_string_appends(&buf_value, line);
 		} else {
 			continue;
 		}
+
+		if (ZEND_HAS_OOM()) {
+			goto oom;
+		}
 	}

 	if (buf_value.c && key) {
@@ -450,6 +475,13 @@ static int multipart_buffer_headers(multipart_buffer *self, zend_llist *header)
 	}

 	return 1;
+
+oom:
+	if (key) {
+		efree(key);
+	}
+	smart_string_free(&buf_value);
+	return 0;
 }

 static char *php_mime_get_hdr_value(zend_llist header, char *key)
@@ -507,11 +539,11 @@ static char *php_ap_getword(const zend_encoding *encoding, char **line, char sto
 	return res;
 }

-static char *substring_conf(char *start, int len, char quote)
+static char *substring_conf(char *start, size_t len, char quote)
 {
 	char *result = emalloc(len + 1);
 	char *resp = result;
-	int i;
+	size_t i;

 	for (i = 0; i < len && start[i] != quote; ++i) {
 		if (start[i] == '\\' && (start[i + 1] == '\\' || (quote && start[i + 1] == quote))) {
@@ -539,14 +571,14 @@ static char *php_ap_getword_conf(const zend_encoding *encoding, char *str)
 		char quote = *str;

 		str++;
-		return substring_conf(str, (int)strlen(str), quote);
+		return substring_conf(str, strlen(str), quote);
 	} else {
 		char *strend = str;

 		while (*strend && !isspace((unsigned char)*strend)) {
 			++strend;
 		}
-		return substring_conf(str, strend - str, 0);
+		return substring_conf(str, (size_t)(strend - str), 0);
 	}
 }

@@ -651,7 +683,11 @@ static char *multipart_buffer_read_body(multipart_buffer *self, size_t *len)
 	size_t total_bytes=0, read_bytes=0;

 	while((read_bytes = multipart_buffer_read(self, buf, sizeof(buf), NULL))) {
-		out = erealloc(out, total_bytes + read_bytes + 1);
+		char *new_out = safe_ferealloc(out, 1, total_bytes, read_bytes + 1, /* fallible */ true);
+		if (!new_out) {
+			break;
+		}
+		out = new_out;
 		memcpy(out + total_bytes, buf, read_bytes);
 		total_bytes += read_bytes;
 	}
@@ -786,6 +822,9 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
 		if (php_rfc1867_callback(MULTIPART_EVENT_START, &event_start, &event_extra_data) == FAILURE) {
 			goto fileupload_done;
 		}
+		if (ZEND_HAS_OOM()) {
+			goto fileupload_done;
+		}
 	}

 	while (!multipart_buffer_eof(mbuff))
@@ -797,6 +836,11 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */

 		zend_llist_clean(&header);

+		if (ZEND_HAS_OOM()) {
+			/* A previous body part has reached the soft memory limit: Stop here */
+			goto fileupload_done;
+		}
+
 		if (!multipart_buffer_headers(mbuff, &header)) {
 			goto fileupload_done;
 		}
@@ -818,6 +862,11 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
 			{
 				char *key = NULL, *word = pair;

+				if (ZEND_HAS_OOM()) {
+					efree(word);
+					break;
+				}
+
 				while (isspace((unsigned char)*cd)) {
 					++cd;
 				}
@@ -825,6 +874,12 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
 				if (strchr(pair, '=')) {
 					key = getword(mbuff->input_encoding, &pair, '=');

+					if (ZEND_HAS_OOM()) {
+						efree(word);
+						efree(key);
+						break;
+					}
+
 					if (!strcasecmp(key, "name")) {
 						if (param) {
 							efree(param);
@@ -852,6 +907,12 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
 							}
 						}
 					}
+
+					if (ZEND_HAS_OOM()) {
+						efree(word);
+						efree(key);
+						break;
+					}
 				}
 				if (key) {
 					efree(key);
@@ -859,12 +920,30 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
 				efree(word);
 			}

+			if (ZEND_HAS_OOM()) {
+				if (filename) {
+					efree(filename);
+				}
+				if (param) {
+					efree(param);
+				}
+				goto fileupload_done;
+			}
+
 			/* Normal form variable, safe to read all data into memory */
 			if (!filename && param) {
 				size_t value_len;
 				char *value = multipart_buffer_read_body(mbuff, &value_len);
 				size_t new_val_len; /* Dummy variable */

+				if (ZEND_HAS_OOM()) {
+					if (value) {
+						efree(value);
+					}
+					efree(param);
+					goto fileupload_done;
+				}
+
 				if (!value) {
 					value = estrdup("");
 					value_len = 0;
@@ -881,6 +960,12 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
 				}

 				if (++count <= PG(max_input_vars) && sapi_module.input_filter(PARSE_POST, param, &value, value_len, &new_val_len)) {
+					if (ZEND_HAS_OOM()) {
+						efree(param);
+						efree(value);
+						goto fileupload_done;
+					}
+
 					if (php_rfc1867_callback != NULL) {
 						multipart_event_formdata event_formdata;
 						size_t newlength = new_val_len;
@@ -895,9 +980,18 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
 							efree(value);
 							continue;
 						}
+						if (ZEND_HAS_OOM()) {
+							efree(param);
+							efree(value);
+							goto fileupload_done;
+						}
 						new_val_len = newlength;
 					}
-					safe_php_register_variable(param, value, new_val_len, array_ptr, 0);
+					if (!safe_php_register_variable(param, value, new_val_len, array_ptr, 0)) {
+						efree(param);
+						efree(value);
+						goto fileupload_done;
+					}
 				} else {
 					if (count == PG(max_input_vars) + 1) {
 						php_error_docref(NULL, E_WARNING, "Input variables exceeded " ZEND_LONG_FMT ". To increase the limit change max_input_vars in php.ini.", PG(max_input_vars));
@@ -912,6 +1006,11 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
 						event_formdata.length = value_len;
 						event_formdata.newlength = NULL;
 						php_rfc1867_callback(MULTIPART_EVENT_FORMDATA, &event_formdata, &event_extra_data);
+						if (ZEND_HAS_OOM()) {
+							efree(param);
+							efree(value);
+							goto fileupload_done;
+						}
 					}
 				}

@@ -989,6 +1088,11 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
 					efree(filename);
 					continue;
 				}
+				if (ZEND_HAS_OOM()) {
+					efree(param);
+					efree(filename);
+					goto fileupload_done;
+				}
 			}

 			if (skip_upload) {
@@ -1039,6 +1143,10 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
 						cancel_upload = UPLOAD_ERROR_X;
 						continue;
 					}
+					if (ZEND_HAS_OOM()) {
+						cancel_upload = UPLOAD_ERROR_X;
+						continue;
+					}
 				}

 				if (PG(upload_max_filesize) > 0 && (zend_long)(total_bytes+blen) > PG(upload_max_filesize)) {
@@ -1104,6 +1212,9 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
 				if (php_rfc1867_callback(MULTIPART_EVENT_FILE_END, &event_file_end, &event_extra_data) == FAILURE) {
 					cancel_upload = UPLOAD_ERROR_X;
 				}
+				if (ZEND_HAS_OOM()) {
+					cancel_upload = UPLOAD_ERROR_X;
+				}
 			}

 			if (cancel_upload) {
@@ -1118,6 +1229,12 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
 				zend_hash_add_ptr(SG(rfc1867_uploaded_files), temp_filename, temp_filename);
 			}

+			if (ZEND_HAS_OOM()) {
+				efree(filename);
+				efree(param);
+				goto fileupload_done;
+			}
+
 			/* is_arr_upload is true when name of file upload field
 			 * ends in [.*]
 			 * start_arr is set to point to 1st [ */
@@ -1129,18 +1246,36 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
 					efree(array_index);
 				}
 				array_index = estrndup(start_arr + 1, array_len - 2);
+
+				if (ZEND_HAS_OOM()) {
+					efree(filename);
+					efree(param);
+					goto fileupload_done;
+				}
 			}

 			/* Add $foo_name */
 			if (llen < strlen(param) + MAX_SIZE_OF_INDEX + 1) {
-				llen = (int)strlen(param);
-				lbuf = (char *) safe_erealloc(lbuf, llen, 1, MAX_SIZE_OF_INDEX + 1);
+				unsigned int new_llen = (int)strlen(param);
+				char *new_lbuf = (char *) safe_ferealloc(lbuf, new_llen, 1, MAX_SIZE_OF_INDEX + 1, /* fallible */ true);
+				if (!new_lbuf) {
+					efree(filename);
+					efree(param);
+					goto fileupload_done;
+				}
+				llen = new_llen;
+				lbuf = new_lbuf;
 				llen += MAX_SIZE_OF_INDEX + 1;
 			}

 			if (is_arr_upload) {
 				if (abuf) efree(abuf);
 				abuf = estrndup(param, strlen(param)-array_len);
+				if (ZEND_HAS_OOM()) {
+					efree(filename);
+					efree(param);
+					goto fileupload_done;
+				}
 				snprintf(lbuf, llen, "%s_name[%s]", abuf, array_index);
 			} else {
 				snprintf(lbuf, llen, "%s_name", param);
@@ -1162,7 +1297,9 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
 			} else {
 				snprintf(lbuf, llen, "%s[name]", param);
 			}
-			register_http_post_files_variable(lbuf, s, &PG(http_globals)[TRACK_VARS_FILES], 0);
+			if (!register_http_post_files_variable(lbuf, s, &PG(http_globals)[TRACK_VARS_FILES], 0)) {
+				goto oom_with_filename;
+			}
 			s = NULL;

 			/* Add full path of supplied file for folder uploads via
@@ -1174,7 +1311,9 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
 			} else {
 				snprintf(lbuf, llen, "%s[full_path]", param);
 			}
-			register_http_post_files_variable(lbuf, filename, &PG(http_globals)[TRACK_VARS_FILES], 0);
+			if (!register_http_post_files_variable(lbuf, filename, &PG(http_globals)[TRACK_VARS_FILES], 0)) {
+				goto oom_with_filename;
+			}
 			efree(filename);

 			/* Possible Content-Type: */
@@ -1194,7 +1333,9 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
 			} else {
 				snprintf(lbuf, llen, "%s[type]", param);
 			}
-			register_http_post_files_variable(lbuf, cd, &PG(http_globals)[TRACK_VARS_FILES], 0);
+			if (!register_http_post_files_variable(lbuf, cd, &PG(http_globals)[TRACK_VARS_FILES], 0)) {
+				goto oom_with_param;
+			}

 			/* Restore Content-Type Header */
 			if (s != NULL) {
@@ -1208,7 +1349,9 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
 				zval zfilename;

 				/* Initialize variables */
-				add_protected_variable(param);
+				if (!add_protected_variable(param)) {
+					goto oom_with_param;
+				}

 				/* Add $foo[tmp_name] */
 				if (is_arr_upload) {
@@ -1216,13 +1359,17 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
 				} else {
 					snprintf(lbuf, llen, "%s[tmp_name]", param);
 				}
-				add_protected_variable(lbuf);
+				if (!add_protected_variable(lbuf)) {
+					goto oom_with_param;
+				}
 				if (temp_filename) {
 					ZVAL_STR_COPY(&zfilename, temp_filename);
 				} else {
 					ZVAL_EMPTY_STRING(&zfilename);
 				}
-				register_http_post_files_variable_ex(lbuf, &zfilename, &PG(http_globals)[TRACK_VARS_FILES], 1);
+				if (!register_http_post_files_variable_ex(lbuf, &zfilename, &PG(http_globals)[TRACK_VARS_FILES], 1)) {
+					goto oom_with_param;
+				}
 			}

 			{
@@ -1260,7 +1407,9 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
 				} else {
 					snprintf(lbuf, llen, "%s[error]", param);
 				}
-				register_http_post_files_variable_ex(lbuf, &error_type, &PG(http_globals)[TRACK_VARS_FILES], 0);
+				if (!register_http_post_files_variable_ex(lbuf, &error_type, &PG(http_globals)[TRACK_VARS_FILES], 0)) {
+					goto oom_with_param;
+				}

 				/* Add $foo[size] */
 				if (is_arr_upload) {
@@ -1271,13 +1420,25 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
 				if (size_overflow) {
 					ZVAL_STRING(&file_size, file_size_buf);
 				}
-				register_http_post_files_variable_ex(lbuf, &file_size, &PG(http_globals)[TRACK_VARS_FILES], size_overflow);
+				if (!register_http_post_files_variable_ex(lbuf, &file_size, &PG(http_globals)[TRACK_VARS_FILES], size_overflow)) {
+					goto oom_with_param;
+				}
 			}
 			efree(param);
+			continue;
+
+oom_with_filename:
+			efree(filename);
+oom_with_param:
+			efree(param);
+			goto fileupload_done;
 		}
 	}

 fileupload_done:
+	if (SG(post_max_size) > 0 && SG(read_post_bytes) > SG(post_max_size)) {
+		sapi_module.sapi_error(E_WARNING, "Actual POST length does not match Content-Length, and exceeds " ZEND_LONG_FMT " bytes", SG(post_max_size));
+	}
 	if (php_rfc1867_callback != NULL) {
 		multipart_event_end event_end;

diff --git a/main/streams/memory.c b/main/streams/memory.c
index 9e5952eaad6..15b31154c9e 100644
--- a/main/streams/memory.c
+++ b/main/streams/memory.c
@@ -53,7 +53,11 @@ static ssize_t php_stream_memory_write(php_stream *stream, const char *buf, size
 		ms->fpos = ZSTR_LEN(ms->data);
 	}
 	if (ms->fpos + count > ZSTR_LEN(ms->data)) {
-		ms->data = zend_string_realloc(ms->data, ms->fpos + count, 0);
+		zend_string *grown = zend_string_frealloc(ms->data, ms->fpos + count, 0, /* fallible */ true);
+		if (UNEXPECTED(!grown)) {
+			return (ssize_t) -1;
+		}
+		ms->data = grown;
 	} else {
 		ms->data = zend_string_separate(ms->data, 0);
 	}
@@ -233,13 +237,21 @@ static int php_stream_memory_set_option(php_stream *stream, int option, int valu
 					}
 					newsize = *(size_t*)ptrparam;
 					if (newsize <= ZSTR_LEN(ms->data)) {
-						ms->data = zend_string_truncate(ms->data, newsize, 0);
+						zend_string *new_data = zend_string_ftruncate(ms->data, newsize, 0, /* fallible */ true);
+						if (UNEXPECTED(!new_data)) {
+							return PHP_STREAM_OPTION_RETURN_ERR;
+						}
+						ms->data = new_data;
 						if (newsize < ms->fpos) {
 							ms->fpos = newsize;
 						}
 					} else {
 						size_t old_size = ZSTR_LEN(ms->data);
-						ms->data = zend_string_realloc(ms->data, newsize, 0);
+						zend_string *new_data = zend_string_frealloc(ms->data, newsize, 0, /* fallible */ true);
+						if (UNEXPECTED(!new_data)) {
+							return PHP_STREAM_OPTION_RETURN_ERR;
+						}
+						ms->data = new_data;
 						memset(ZSTR_VAL(ms->data) + old_size, 0, newsize - old_size);
 						ZSTR_VAL(ms->data)[ZSTR_LEN(ms->data)] = '\0';
 					}
diff --git a/main/streams/streams.c b/main/streams/streams.c
index 06117a521f5..1186f449080 100644
--- a/main/streams/streams.c
+++ b/main/streams/streams.c
@@ -549,7 +549,10 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size)
 		php_stream_bucket_brigade *brig_inp = &brig_in, *brig_outp = &brig_out, *brig_swap;

 		/* allocate a buffer for reading chunks */
-		chunk_buf = emalloc(stream->chunk_size);
+		chunk_buf = femalloc(stream->chunk_size, /* fallible */ true);
+		if (UNEXPECTED(!chunk_buf)) {
+			return FAILURE;
+		}

 		while (!stream->eof && (stream->writepos - stream->readpos < (zend_off_t)to_read_now)) {
 			ssize_t justread = 0;
@@ -608,9 +611,20 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size)
 						}
 						/* grow buffer to hold this bucket */
 						if (stream->readbuflen - stream->writepos < bucket->buflen) {
-							stream->readbuflen += bucket->buflen;
-							stream->readbuf = perealloc(stream->readbuf, stream->readbuflen,
-									stream->is_persistent);
+							/* A filter's output is unbounded relative to its input. */
+							size_t newlen = stream->readbuflen + bucket->buflen;
+							unsigned char *newbuf = fperealloc(stream->readbuf, newlen,
+									stream->is_persistent, /* fallible */ true);
+							if (UNEXPECTED(!newbuf)) {
+								while (brig_inp->head) {
+									bucket = brig_inp->head;
+									php_stream_bucket_unlink(bucket);
+									php_stream_bucket_delref(bucket);
+								}
+								break;
+							}
+							stream->readbuf = newbuf;
+							stream->readbuflen = newlen;
 						}
 						if (bucket->buflen) {
 							memcpy(stream->readbuf + stream->writepos, bucket->buf, bucket->buflen);
@@ -672,9 +686,14 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size)
 			/* grow the buffer if required
 			 * TODO: this can fail for persistent streams */
 			if (stream->readbuflen - stream->writepos < stream->chunk_size) {
-				stream->readbuflen += stream->chunk_size;
-				stream->readbuf = perealloc(stream->readbuf, stream->readbuflen,
-						stream->is_persistent);
+				size_t newlen = stream->readbuflen + stream->chunk_size;
+				unsigned char *newbuf = fperealloc(stream->readbuf, newlen,
+						stream->is_persistent, /* fallible */ true);
+				if (UNEXPECTED(!newbuf)) {
+					return FAILURE;
+				}
+				stream->readbuf = newbuf;
+				stream->readbuflen = newlen;
 			}

 			justread = stream->ops->read(stream, (char*)stream->readbuf + stream->writepos,
@@ -774,7 +793,10 @@ PHPAPI ssize_t _php_stream_read(php_stream *stream, char *buf, size_t size)
  * to the copy_to_mem() operation, but only performs a single direct read. */
 PHPAPI zend_string *php_stream_read_to_str(php_stream *stream, size_t len)
 {
-	zend_string *str = zend_string_alloc(len, 0);
+	zend_string *str = zend_string_falloc(len, 0, /* fallible */ true);
+	if (UNEXPECTED(!str)) {
+		return NULL;
+	}
 	ssize_t read = php_stream_read(stream, ZSTR_VAL(str), len);
 	if (read < 0) {
 		zend_string_efree(str);
@@ -949,6 +971,9 @@ PHPAPI char *_php_stream_get_line(php_stream *stream, char *buf, size_t maxlen,
 			}

 			if (grow_mode) {
+				if (ZEND_HAS_OOM()) {
+					break;
+				}
 				/* allow room for a NUL. If this realloc is really a realloc
 				 * (ie: second time around), we get an extra byte. In most
 				 * cases, with the default chunk size of 8K, we will only
@@ -1117,7 +1142,10 @@ PHPAPI zend_string *php_stream_get_record(php_stream *stream, size_t maxlen, con
 		}
 	}

-	ret_buf = zend_string_alloc(tent_ret_len, 0);
+	ret_buf = zend_string_falloc(tent_ret_len, 0, /* fallible */ true);
+	if (UNEXPECTED(!ret_buf)) {
+		return NULL;
+	}
 	/* php_stream_read will not call ops->read here because the necessary
 	 * data is guaranteed to be buffered */
 	ZSTR_LEN(ret_buf) = php_stream_read(stream, ZSTR_VAL(ret_buf), tent_ret_len);
@@ -1460,6 +1488,9 @@ PHPAPI ssize_t _php_stream_passthru(php_stream * stream STREAMS_DC)

 		if (p) {
 			do {
+				if (ZEND_HAS_OOM()) {
+					break;
+				}
 				/* output functions return int, so pass in int max */
 				if (0 < (b = PHPWRITE(p + bcount, MIN(mapped - bcount, INT_MAX)))) {
 					bcount += b;
@@ -1473,6 +1504,9 @@ PHPAPI ssize_t _php_stream_passthru(php_stream * stream STREAMS_DC)
 	}

 	while ((b = php_stream_read(stream, buf, sizeof(buf))) > 0) {
+		if (ZEND_HAS_OOM()) {
+			break;
+		}
 		PHPWRITE(buf, b);
 		bcount += b;
 	}
@@ -1504,7 +1538,10 @@ PHPAPI zend_string *_php_stream_copy_to_mem(php_stream *src, size_t maxlen, int
 	}

 	if (maxlen > 0) {
-		result = zend_string_alloc(maxlen, persistent);
+		result = zend_string_falloc(maxlen, persistent, /* fallible */ true);
+		if (UNEXPECTED(!result)) {
+			return NULL;
+		}
 		ptr = ZSTR_VAL(result);
 		while ((len < maxlen) && !php_stream_eof(src)) {
 			ret = php_stream_read(src, ptr, maxlen - len);
@@ -1542,14 +1579,22 @@ PHPAPI zend_string *_php_stream_copy_to_mem(php_stream *src, size_t maxlen, int
 		max_len = step;
 	}

-	result = zend_string_alloc(max_len, persistent);
+	result = zend_string_falloc(max_len, persistent, /* fallible */ true);
+	if (UNEXPECTED(!result)) {
+		return NULL;
+	}
 	ptr = ZSTR_VAL(result);

 	// TODO: Propagate error?
 	while ((ret = php_stream_read(src, ptr, max_len - len)) > 0){
 		len += ret;
 		if (len + min_room >= max_len) {
-			result = zend_string_extend(result, max_len + step, persistent);
+			zend_string *grown = zend_string_fextend(result, max_len + step, persistent, /* fallible */ true);
+			if (UNEXPECTED(!grown)) {
+				zend_string_free(result);
+				return NULL;
+			}
+			result = grown;
 			max_len += step;
 			ptr = ZSTR_VAL(result) + len;
 		} else {
diff --git a/run-tests.php b/run-tests.php
index 8dacaf59895..00a49f9d559 100755
--- a/run-tests.php
+++ b/run-tests.php
@@ -1244,12 +1244,16 @@ function system_with_timeout(
         }
     }

-    $stat = proc_get_status($proc);
+    do {
+        $stat = proc_get_status($proc);
+        if ($stat['running']) {
+            usleep(1000);
+        }
+    } while ($stat['running']);

     if ($stat['signaled']) {
-        $data .= "\nTermsig=" . $stat['stopsig'] . "\n";
-    }
-    if ($stat["exitcode"] > 128 && $stat["exitcode"] < 160) {
+        $data .= "\nTermsig=" . $stat['termsig'] . "\n";
+    } else if ($stat["exitcode"] > 128 && $stat["exitcode"] < 160) {
         $data .= "\nTermsig=" . ($stat["exitcode"] - 128) . "\n";
     } else if (defined('PHP_WINDOWS_VERSION_MAJOR') && (($stat["exitcode"] >> 28) & 0b1111) === 0b1100) {
         // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/87fba13e-bf06-450e-83b1-9241dc81e781
diff --git a/sapi/cli/tests/oom_request_parsing_next_request.phpt b/sapi/cli/tests/oom_request_parsing_next_request.phpt
new file mode 100644
index 00000000000..edf1f4e9f29
--- /dev/null
+++ b/sapi/cli/tests/oom_request_parsing_next_request.phpt
@@ -0,0 +1,36 @@
+--TEST--
+OOM during request parsing does not leak into the next request
+--FILE--
+<?php
+include "php_cli_server.inc";
+php_cli_server_start(<<<'PHP'
+spl_autoload_register(function ($class) {
+    eval("class $class {}");
+});
+ob_start();
+echo get_class(new Foo), "\n";
+trigger_error("Bar", E_USER_WARNING);
+ob_end_flush();
+PHP, 'index.php', ['-d', 'memory_limit=8M', '-d', 'post_max_size=64M', '-d', 'html_errors=0']);
+
+function send(string $request): string {
+    $fp = php_cli_server_connect();
+    fwrite($fp, $request);
+    $response = stream_get_contents($fp);
+    fclose($fp);
+
+    return $response;
+}
+
+$body = 'a=' . str_repeat('a', 6 * 1024 * 1024);
+$response = send("POST / HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: " . strlen($body) . "\r\n\r\n" . $body);
+var_dump(str_contains($response, 'Allowed memory size of 8388608 bytes exhausted'));
+
+$response = send("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
+echo substr($response, strpos($response, "\r\n\r\n") + 4);
+?>
+--EXPECTF--
+bool(true)
+Foo
+
+Warning: Bar in %s on line %d
diff --git a/sapi/cli/tests/oom_shutdown_function_next_request.phpt b/sapi/cli/tests/oom_shutdown_function_next_request.phpt
new file mode 100644
index 00000000000..8a47311a48e
--- /dev/null
+++ b/sapi/cli/tests/oom_shutdown_function_next_request.phpt
@@ -0,0 +1,37 @@
+--TEST--
+OOM raised after a shutdown function does not leak into the next request
+--FILE--
+<?php
+include "php_cli_server.inc";
+php_cli_server_start(<<<'PHP'
+if (isset($_GET['oom'])) {
+    register_shutdown_function(function () {
+        $a = str_repeat('a', 3 * 1024 * 1024);
+        $GLOBALS['b'] = $a . $a;
+    });
+    return;
+}
+spl_autoload_register(function ($class) {
+    eval("class $class {}");
+});
+echo get_class(new Foo), "\n";
+PHP, 'index.php', ['-d', 'memory_limit=8M', '-d', 'html_errors=0']);
+
+function send(string $request): string {
+    $fp = php_cli_server_connect();
+    fwrite($fp, $request);
+    $response = stream_get_contents($fp);
+    fclose($fp);
+
+    return $response;
+}
+
+$response = send("GET /?oom=1 HTTP/1.1\r\nHost: localhost\r\n\r\n");
+var_dump(str_contains($response, 'Allowed memory size of 8388608 bytes exhausted'));
+
+$response = send("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
+echo substr($response, strpos($response, "\r\n\r\n") + 4);
+?>
+--EXPECT--
+bool(true)
+Foo
diff --git a/sapi/fpm/tests/bug77023-pm-dynamic-blocking-sigquit.phpt b/sapi/fpm/tests/bug77023-pm-dynamic-blocking-sigquit.phpt
index a571fe0bb36..b20d5587503 100644
--- a/sapi/fpm/tests/bug77023-pm-dynamic-blocking-sigquit.phpt
+++ b/sapi/fpm/tests/bug77023-pm-dynamic-blocking-sigquit.phpt
@@ -2,6 +2,7 @@
 FPM: Blocked SIGQUIT prevents idle process to be killed
 --EXTENSIONS--
 pcntl
+--FLAKY--
 --SKIPIF--
 <?php
 include "skipif.inc";
diff --git a/sapi/fuzzer/fuzzer-sapi.c b/sapi/fuzzer/fuzzer-sapi.c
index e55099a5d05..887c1b2d44a 100644
--- a/sapi/fuzzer/fuzzer-sapi.c
+++ b/sapi/fuzzer/fuzzer-sapi.c
@@ -196,6 +196,8 @@ void fuzzer_request_shutdown()
 			EG(exception) = NULL;
 		}

+		zend_mm_oom_info_dtor(&EG(delayed_oom));
+
 		/* Some fuzzers (like unserialize) may create circular structures. Make sure we free them.
 		 * Two calls are performed to handle objects with destructors. */
 		zend_gc_collect_cycles();
@@ -272,6 +274,12 @@ int fuzzer_do_request_from_buffer(
 			if (execute) {
 				zend_execute(op_array, NULL);
 			}
+			if (ZEND_HAS_OOM()) {
+				zend_handle_delayed_oom();
+			}
+			if (EG(exception)) {
+				zend_exception_error(EG(exception), E_WARNING);
+			}
 			zend_destroy_static_vars(op_array);
 			destroy_op_array(op_array);
 			efree(op_array);
diff --git a/sapi/phpdbg/phpdbg.c b/sapi/phpdbg/phpdbg.c
index 4e685d0894a..882d4985738 100644
--- a/sapi/phpdbg/phpdbg.c
+++ b/sapi/phpdbg/phpdbg.c
@@ -1460,7 +1460,7 @@ int main(int argc, char **argv) /* {{{ */

 		if (php_request_startup() == FAILURE) {
 			PUTS("Could not startup");
-			return 1;
+			goto phpdbg_out;
 		}

 #ifndef _WIN32
diff --git a/tests/basic/GHSA-2cv3-554h-jpmh_multipart.phpt b/tests/basic/GHSA-2cv3-554h-jpmh_multipart.phpt
new file mode 100644
index 00000000000..bafe774b511
--- /dev/null
+++ b/tests/basic/GHSA-2cv3-554h-jpmh_multipart.phpt
@@ -0,0 +1,37 @@
+--TEST--
+GHSA-2cv3-554h-jpmh post_max_size bypass - multipart
+--EXTENSIONS--
+zend_test
+--INI--
+post_max_size=100K
+--FILE--
+<?php
+
+function make_post_data($value) {
+    $data = ""
+        . "--b\r\n"
+        . "Content-Disposition: form-data; name=\"a\"\r\n"
+        . "Content-Type: application/octet-stream\r\n"
+        . "\r\n"
+        . "$value\r\n"
+        . "\r\n"
+        . "--b--\r\n"
+        ;
+    $stream = fopen('data:text/plain;base64,' . base64_encode($data), 'r');
+    return $stream;
+}
+
+const FILLUNIT = 1024 * 5;
+
+zend_test_handle_post_data(make_post_data('hello'), content_type: 'multipart/form-data; boundary=b', content_length: 0);
+var_dump(strlen($_POST['a']));
+
+zend_test_handle_post_data(make_post_data(str_repeat('a', 1*1024*1024)), content_type: 'multipart/form-data; boundary=b', content_length: 0);
+var_dump(strlen($_POST['a'] ?? '') < 100*1024);
+
+?>
+--EXPECTF--
+int(7)
+
+Warning: Actual POST length does not match Content-Length, and exceeds 102400 bytes in %s on line %d
+bool(true)
diff --git a/tests/basic/GHSA-2cv3-554h-jpmh_urlencoded.phpt b/tests/basic/GHSA-2cv3-554h-jpmh_urlencoded.phpt
new file mode 100644
index 00000000000..d743a51b339
--- /dev/null
+++ b/tests/basic/GHSA-2cv3-554h-jpmh_urlencoded.phpt
@@ -0,0 +1,29 @@
+--TEST--
+GHSA-2cv3-554h-jpmh post_max_size bypass - application/x-www-form-urlencoded
+--EXTENSIONS--
+zend_test
+--INI--
+post_max_size=100K
+--FILE--
+<?php
+
+function make_post_data($value) {
+    $data = "a=" . urlencode($value);
+    $stream = fopen('data:text/plain;base64,' . base64_encode($data), 'r');
+    return $stream;
+}
+
+const SAPI_POST_BLOCK_SIZE = 0x40000;
+
+zend_test_handle_post_data(make_post_data('hello'), content_type: 'application/x-www-form-urlencoded', content_length: 0);
+var_dump(strlen($_POST['a']));
+
+zend_test_handle_post_data(make_post_data(str_repeat('a', 1*1024*1024)), content_type: 'application/x-www-form-urlencoded', content_length: 0);
+var_dump(strlen($_POST['a'] ?? '') < 100*1024+SAPI_POST_BLOCK_SIZE);
+
+?>
+--EXPECTF--
+int(5)
+
+Warning: zend_test_handle_post_data(): Actual POST length does not match Content-Length, and exceeds 102400 bytes in %s on line %d
+bool(true)
diff --git a/tests/basic/GHSA-f883-x959-3vr9_001.phpt b/tests/basic/GHSA-f883-x959-3vr9_001.phpt
new file mode 100644
index 00000000000..dcc793a58bb
--- /dev/null
+++ b/tests/basic/GHSA-f883-x959-3vr9_001.phpt
@@ -0,0 +1,20 @@
+--TEST--
+GHSA-f883-x959-3vr9 001
+--POST_RAW--
+Content-Type: multipart/form-data; boundary=b
+--b
+Content-Disposition: form-data; name="d1"
+
+test
+--b--
+--FILE--
+<?php
+
+var_dump($_POST);
+
+?>
+--EXPECT--
+array(1) {
+  ["d1"]=>
+  string(4) "test"
+}
diff --git a/tests/basic/GHSA-f883-x959-3vr9_002.phpt b/tests/basic/GHSA-f883-x959-3vr9_002.phpt
new file mode 100644
index 00000000000..461960af12a
--- /dev/null
+++ b/tests/basic/GHSA-f883-x959-3vr9_002.phpt
@@ -0,0 +1,21 @@
+--TEST--
+GHSA-f883-x959-3vr9 002
+--POST_RAW--
+Content-Type: multipart/form-data; boundary=b
+--b
+Ignored: value
+Content-Disposition: form-data; name="d1"
+
+test
+--b--
+--FILE--
+<?php
+
+var_dump($_POST);
+
+?>
+--EXPECT--
+array(1) {
+  ["d1"]=>
+  string(4) "test"
+}
diff --git a/tests/basic/GHSA-f883-x959-3vr9_003.phpt b/tests/basic/GHSA-f883-x959-3vr9_003.phpt
new file mode 100644
index 00000000000..a16ad8b9493
--- /dev/null
+++ b/tests/basic/GHSA-f883-x959-3vr9_003.phpt
@@ -0,0 +1,21 @@
+--TEST--
+GHSA-f883-x959-3vr9 003
+--POST_RAW--
+Content-Type: multipart/form-data; boundary=b
+--b
+Content-Disposition: form-data; name="d1"
+Ignored: value
+
+test
+--b--
+--FILE--
+<?php
+
+var_dump($_POST);
+
+?>
+--EXPECT--
+array(1) {
+  ["d1"]=>
+  string(4) "test"
+}
diff --git a/tests/basic/GHSA-f883-x959-3vr9_004.phpt b/tests/basic/GHSA-f883-x959-3vr9_004.phpt
new file mode 100644
index 00000000000..4da18c9c4d8
--- /dev/null
+++ b/tests/basic/GHSA-f883-x959-3vr9_004.phpt
@@ -0,0 +1,23 @@
+--TEST--
+GHSA-f883-x959-3vr9 004
+--POST_RAW--
+Content-Type: multipart/form-data; boundary=b
+--b
+Content-Disposition: form-data;
+ name="d1"
+Ignored: value
+ value
+
+test
+--b--
+--FILE--
+<?php
+
+var_dump($_POST);
+
+?>
+--EXPECT--
+array(1) {
+  ["d1"]=>
+  string(4) "test"
+}
diff --git a/tests/basic/GHSA-f883-x959-3vr9_005.phpt b/tests/basic/GHSA-f883-x959-3vr9_005.phpt
new file mode 100644
index 00000000000..4d8223bf700
--- /dev/null
+++ b/tests/basic/GHSA-f883-x959-3vr9_005.phpt
@@ -0,0 +1,23 @@
+--TEST--
+GHSA-f883-x959-3vr9 005
+--POST_RAW--
+Content-Type: multipart/form-data; boundary=b
+--b
+Ignored: value
+ value
+Content-Disposition: form-data;
+ name="d1"
+
+test
+--b--
+--FILE--
+<?php
+
+var_dump($_POST);
+
+?>
+--EXPECT--
+array(1) {
+  ["d1"]=>
+  string(4) "test"
+}
diff --git a/tests/basic/GHSA-f883-x959-3vr9_006.phpt b/tests/basic/GHSA-f883-x959-3vr9_006.phpt
new file mode 100644
index 00000000000..88d2deae8a8
--- /dev/null
+++ b/tests/basic/GHSA-f883-x959-3vr9_006.phpt
@@ -0,0 +1,43 @@
+--TEST--
+GHSA-f883-x959-3vr9 006
+--POST_RAW--
+Content-Type: multipart/form-data; boundary=b
+--b
+Content-Disposition: form-data; name="f1"; filename="f1"
+Content-Type: text/plain
+
+test
+--b
+Content-Disposition: form-data; name="d1"
+
+test
+--b--
+--FILE--
+<?php
+
+var_dump($_POST);
+var_dump($_FILES);
+
+?>
+--EXPECTF--
+array(1) {
+  ["d1"]=>
+  string(4) "test"
+}
+array(1) {
+  ["f1"]=>
+  array(6) {
+    ["name"]=>
+    string(2) "f1"
+    ["full_path"]=>
+    string(2) "f1"
+    ["type"]=>
+    string(10) "text/plain"
+    ["tmp_name"]=>
+    string(%d) "%s"
+    ["error"]=>
+    int(0)
+    ["size"]=>
+    int(4)
+  }
+}
diff --git a/tests/basic/GHSA-f883-x959-3vr9_007.inc b/tests/basic/GHSA-f883-x959-3vr9_007.inc
new file mode 100644
index 00000000000..26d1cde739f
--- /dev/null
+++ b/tests/basic/GHSA-f883-x959-3vr9_007.inc
@@ -0,0 +1,3 @@
+<?php
+var_dump($_POST);
+?>
diff --git a/tests/basic/GHSA-f883-x959-3vr9_007.phpt b/tests/basic/GHSA-f883-x959-3vr9_007.phpt
new file mode 100644
index 00000000000..da2acc8ac09
--- /dev/null
+++ b/tests/basic/GHSA-f883-x959-3vr9_007.phpt
@@ -0,0 +1,63 @@
+--TEST--
+GHSA-f883-x959-3vr9 007
+--SKIPIF--
+<?php
+if (!getenv('TEST_PHP_CGI_EXECUTABLE')) {
+    die("skip php-cgi not available");
+}
+?>
+--FILE--
+<?php
+
+$cmd = [
+    getenv('TEST_PHP_CGI_EXECUTABLE'),
+    '-C',
+    '-n',
+    __DIR__ . '/GHSA-f883-x959-3vr9_007.inc',
+];
+
+$body = ""
+    . "--b\r\n"
+    . "Content-Disposition: form-data; name=\"d1\"\r\n"
+    . str_repeat("A:b\r\n", 600*1000)
+    . "\r\n"
+    . "test\r\n"
+    . "--b--\r\n"
+    ;
+
+$env = array_merge($_ENV, [
+    'REDIRECT_STATUS' => '1',
+    'CONTENT_TYPE' => "multipart/form-data; boundary=b",
+    'CONTENT_LENGTH' => strlen($body),
+    'REQUEST_METHOD' => 'POST',
+    'SCRIPT_FILENAME' => __DIR__ . '/GHSA-f883-x959-3vr9_007.inc',
+]);
+
+$spec = [
+    0 => ['pipe', 'r'],
+    1 => STDOUT,
+    2 => STDOUT,
+];
+
+$pipes = [];
+
+print "Starting...\n";
+
+$handle = proc_open($cmd, $spec, $pipes, getcwd(), $env);
+
+fwrite($pipes[0], $body);
+
+$status = proc_close($handle);
+
+print "\n";
+
+?>
+--EXPECTF--
+Starting...
+X-Powered-By: %s
+Content-type: text/html; charset=UTF-8
+
+array(1) {
+  ["d1"]=>
+  string(4) "test"
+}
diff --git a/tests/basic/GHSA-f883-x959-3vr9_008.phpt b/tests/basic/GHSA-f883-x959-3vr9_008.phpt
new file mode 100644
index 00000000000..f1b5049e9a6
--- /dev/null
+++ b/tests/basic/GHSA-f883-x959-3vr9_008.phpt
@@ -0,0 +1,44 @@
+--TEST--
+GHSA-f883-x959-3vr9 008
+--POST_RAW--
+Content-Type: multipart/form-data; boundary=b
+--b
+Content-Disposition: form-data;
+ name="f1"; filename="f1"
+Content-Type: text/plain
+
+test
+--b
+Content-Disposition: form-data; name="d1"
+
+test
+--b--
+--FILE--
+<?php
+
+var_dump($_POST);
+var_dump($_FILES);
+
+?>
+--EXPECTF--
+array(1) {
+  ["d1"]=>
+  string(4) "test"
+}
+array(1) {
+  ["f1"]=>
+  array(6) {
+    ["name"]=>
+    string(2) "f1"
+    ["full_path"]=>
+    string(2) "f1"
+    ["type"]=>
+    string(10) "text/plain"
+    ["tmp_name"]=>
+    string(%d) "%s"
+    ["error"]=>
+    int(0)
+    ["size"]=>
+    int(4)
+  }
+}
diff --git a/tests/basic/GHSA-f883-x959-3vr9_009.phpt b/tests/basic/GHSA-f883-x959-3vr9_009.phpt
new file mode 100644
index 00000000000..f2697b86fdd
--- /dev/null
+++ b/tests/basic/GHSA-f883-x959-3vr9_009.phpt
@@ -0,0 +1,44 @@
+--TEST--
+GHSA-f883-x959-3vr9 009
+--POST_RAW--
+Content-Type: multipart/form-data; boundary=b
+--b
+Content-Type: text/plain
+Content-Disposition: form-data;
+ name="f1"; filename="f1"
+
+test
+--b
+Content-Disposition: form-data; name="d1"
+
+test
+--b--
+--FILE--
+<?php
+
+var_dump($_POST);
+var_dump($_FILES);
+
+?>
+--EXPECTF--
+array(1) {
+  ["d1"]=>
+  string(4) "test"
+}
+array(1) {
+  ["f1"]=>
+  array(6) {
+    ["name"]=>
+    string(2) "f1"
+    ["full_path"]=>
+    string(2) "f1"
+    ["type"]=>
+    string(10) "text/plain"
+    ["tmp_name"]=>
+    string(%d) "%s"
+    ["error"]=>
+    int(0)
+    ["size"]=>
+    int(4)
+  }
+}
diff --git a/tests/basic/GHSA-f883-x959-3vr9_010.phpt b/tests/basic/GHSA-f883-x959-3vr9_010.phpt
new file mode 100644
index 00000000000..7fc5b96f351
--- /dev/null
+++ b/tests/basic/GHSA-f883-x959-3vr9_010.phpt
@@ -0,0 +1,45 @@
+--TEST--
+GHSA-f883-x959-3vr9 010
+--POST_RAW--
+Content-Type: multipart/form-data; boundary=b
+--b
+Content-Disposition: form-data; name="f1"; filename="f1"
+Content-Disposition: form-data; name="f2"; filename="f2"
+Content-Type: text/plain
+
+test
+--b
+Content-Disposition: form-data; name="d1"
+Content-Disposition: form-data; name="d2"
+
+test
+--b--
+--FILE--
+<?php
+
+var_dump($_POST);
+var_dump($_FILES);
+
+?>
+--EXPECTF--
+array(1) {
+  ["d1"]=>
+  string(4) "test"
+}
+array(1) {
+  ["f1"]=>
+  array(6) {
+    ["name"]=>
+    string(2) "f1"
+    ["full_path"]=>
+    string(2) "f1"
+    ["type"]=>
+    string(10) "text/plain"
+    ["tmp_name"]=>
+    string(%d) "%s"
+    ["error"]=>
+    int(0)
+    ["size"]=>
+    int(4)
+  }
+}
diff --git a/tests/basic/GHSA-f883-x959-3vr9_011.phpt b/tests/basic/GHSA-f883-x959-3vr9_011.phpt
new file mode 100644
index 00000000000..384c8f2f2b6
--- /dev/null
+++ b/tests/basic/GHSA-f883-x959-3vr9_011.phpt
@@ -0,0 +1,20 @@
+--TEST--
+GHSA-f883-x959-3vr9 011
+--POST_RAW--
+Content-Type: multipart/form-data; boundary=b
+--b
+content-disposition: form-data; name="d1"
+
+test
+--b--
+--FILE--
+<?php
+
+var_dump($_POST);
+
+?>
+--EXPECT--
+array(1) {
+  ["d1"]=>
+  string(4) "test"
+}
diff --git a/tests/basic/oom_rfc1867.inc b/tests/basic/oom_rfc1867.inc
new file mode 100644
index 00000000000..5750745975c
--- /dev/null
+++ b/tests/basic/oom_rfc1867.inc
@@ -0,0 +1,88 @@
+<?php
+
+/* Helpers for the oom_rfc1867_*.phpt tests.
+ *
+ * Each test posts a multipart/form-data body to php-cgi with a small
+ * memory_limit and every input limit raised, so that only the memory limit
+ * bounds what the parser accumulates. */
+
+const OOM_RFC1867_BOUNDARY = '---------------------------12345';
+
+function oom_rfc1867_part($fh, string $headers, string $body): void
+{
+    fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n" . $headers . "\r\n\r\n" . $body . "\r\n");
+}
+
+/* Writes $bytes bytes of $char, a chunk at a time, so the test itself doesn't
+ * need a multi-megabyte string in memory. */
+function oom_rfc1867_fill($fh, int $bytes, string $char = 'A'): void
+{
+    while ($bytes > 0) {
+        $chunk = min($bytes, 1024 * 1024);
+        fwrite($fh, str_repeat($char, $chunk));
+        $bytes -= $chunk;
+    }
+}
+
+/* Calls $writeBody() to write the body parts, appends the closing boundary,
+ * posts the result and prints what php-cgi reported.
+ *
+ * $extraIni is passed as additional -d options, for the paths that only run
+ * with a given extension configured (mbstring translation, upload progress). */
+function oom_rfc1867_request(callable $writeBody, string $memoryLimit = '8M', array $extraIni = [], ?string $target = null): void
+{
+    $bodyFile = tempnam(sys_get_temp_dir(), 'oom_rfc1867');
+    $fh = fopen($bodyFile, 'wb');
+    $writeBody($fh);
+    fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "--\r\n");
+    fclose($fh);
+
+    $cmd = [
+        getenv('TEST_PHP_CGI_EXECUTABLE'),
+        '-C',
+        '-n',
+        '-d', 'memory_limit=' . $memoryLimit,
+        '-d', 'post_max_size=1G',
+        '-d', 'upload_max_filesize=1G',
+        '-d', 'max_input_vars=10000000',
+        '-d', 'max_file_uploads=1000000',
+        '-d', 'max_multipart_body_parts=10000000',
+        '-d', 'max_input_time=-1',
+        '-d', 'html_errors=0',
+    ];
+
+    foreach ($extraIni as $ini) {
+        $cmd[] = '-d';
+        $cmd[] = $ini;
+    }
+
+    $env = array_merge($_ENV, [
+        'REDIRECT_STATUS' => '1',
+        'CONTENT_TYPE' => 'multipart/form-data; boundary=' . OOM_RFC1867_BOUNDARY,
+        'CONTENT_LENGTH' => filesize($bodyFile),
+        'REQUEST_METHOD' => 'POST',
+        'SCRIPT_FILENAME' => $target ?? __DIR__ . '/oom_rfc1867_target.inc',
+    ]);
+
+    $spec = [
+        0 => ['file', $bodyFile, 'r'],
+        1 => ['pipe', 'w'],
+        2 => ['pipe', 'w'],
+    ];
+
+    $pipes = [];
+    $handle = proc_open($cmd, $spec, $pipes, getcwd(), $env);
+    $out = stream_get_contents($pipes[1]);
+    $err = stream_get_contents($pipes[2]);
+    fclose($pipes[1]);
+    fclose($pipes[2]);
+    proc_close($handle);
+    unlink($bodyFile);
+
+    /* Drop the CGI headers, keep the body. */
+    $out = preg_replace("/^.*?\r\n\r\n/s", '', $out);
+    echo trim(str_replace("\r\n", "\n", $out)), "\n";
+    if ($err !== '') {
+        echo 'stderr: ', trim($err), "\n";
+    }
+}
diff --git a/tests/basic/oom_rfc1867_array_upload_name.phpt b/tests/basic/oom_rfc1867_array_upload_name.phpt
new file mode 100644
index 00000000000..6a790c93d5f
--- /dev/null
+++ b/tests/basic/oom_rfc1867_array_upload_name.phpt
@@ -0,0 +1,45 @@
+--TEST--
+rfc1867: a huge array upload field name doesn't reach the hard limit
+--SKIPIF--
+<?php require __DIR__ . "/oom_rfc1867_skipif.inc"; ?>
+--FILE--
+<?php
+require __DIR__ . '/oom_rfc1867.inc';
+
+oom_rfc1867_request(function ($fh) {
+    fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+        . 'Content-Disposition: form-data; name="');
+    for ($i = 0; $i < 5; $i++) {
+        fwrite($fh, str_repeat('A', 1024 * 1024));
+    }
+    fwrite($fh, "[k]\"; filename=\"f.txt\"\r\nContent-Type: text/plain\r\n\r\nx\r\n");
+}, '16M');
+
+// Same, with the field name in the array index instead.
+oom_rfc1867_request(function ($fh) {
+    fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+        . 'Content-Disposition: form-data; name="u[');
+    for ($i = 0; $i < 5; $i++) {
+        fwrite($fh, str_repeat('A', 1024 * 1024));
+    }
+    fwrite($fh, "]\"; filename=\"f.txt\"\r\nContent-Type: text/plain\r\n\r\nx\r\n");
+}, '16M');
+
+// Base name and array index both huge: abuf and array_index are each a copy
+// of param, retained together with param and the header value.
+foreach ([1, 2] as $mb) {
+    oom_rfc1867_request(function ($fh) use ($mb) {
+        fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+            . 'Content-Disposition: form-data; name="');
+        oom_rfc1867_fill($fh, $mb * 1024 * 1024);
+        fwrite($fh, '[');
+        oom_rfc1867_fill($fh, $mb * 1024 * 1024);
+        fwrite($fh, "]\"; filename=\"f.txt\"\r\nContent-Type: text/plain\r\n\r\nx\r\n");
+    }, '8M');
+}
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 16777216 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 16777216 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
diff --git a/tests/basic/oom_rfc1867_early_report.phpt b/tests/basic/oom_rfc1867_early_report.phpt
new file mode 100644
index 00000000000..544482321b0
--- /dev/null
+++ b/tests/basic/oom_rfc1867_early_report.phpt
@@ -0,0 +1,34 @@
+--TEST--
+rfc1867: the soft limit is reported immediately after input parsing
+--SKIPIF--
+<?php require __DIR__ . "/oom_rfc1867_skipif.inc"; ?>
+--FILE--
+<?php
+require __DIR__ . '/oom_rfc1867.inc';
+
+$dir = sys_get_temp_dir();
+$scripts = [];
+foreach ([1, 60000] as $stmts) {
+    $file = $dir . "/oom_rfc1867_early_report_$stmts.php";
+    file_put_contents($file, "<?php\n"
+        . str_repeat("\$x = 1 + 2;\n", $stmts)
+        . "printf(\"post=%d files=%d\\n\", count(\$_POST), count(\$_FILES));\n");
+    $scripts[] = $file;
+}
+
+foreach ($scripts as $script) {
+    oom_rfc1867_request(function ($fh) {
+        fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+            . "Content-Disposition: form-data; name=\"f\"\r\n\r\n");
+        oom_rfc1867_fill($fh, 6 * 1024 * 1024, 'x');
+        fwrite($fh, "\r\n");
+    }, '8M', [], $script);
+}
+
+foreach ($scripts as $script) {
+    @unlink($script);
+}
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
diff --git a/tests/basic/oom_rfc1867_field_name.phpt b/tests/basic/oom_rfc1867_field_name.phpt
new file mode 100644
index 00000000000..954257e9483
--- /dev/null
+++ b/tests/basic/oom_rfc1867_field_name.phpt
@@ -0,0 +1,25 @@
+--TEST--
+rfc1867: a huge form field name doesn't reach the hard limit
+--SKIPIF--
+<?php require __DIR__ . "/oom_rfc1867_skipif.inc"; ?>
+--FILE--
+<?php
+require __DIR__ . '/oom_rfc1867.inc';
+
+for ($kb = 5 * 1024; $kb <= 6 * 1024 + 512; $kb += 256) {
+    oom_rfc1867_request(function ($fh) use ($kb) {
+        fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+            . 'Content-Disposition: form-data; name="'
+            . str_repeat('A', $kb * 1024)
+            . "\"\r\n\r\nv\r\n");
+    }, '10M');
+}
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 10485760 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
diff --git a/tests/basic/oom_rfc1867_files.phpt b/tests/basic/oom_rfc1867_files.phpt
new file mode 100644
index 00000000000..d951017803e
--- /dev/null
+++ b/tests/basic/oom_rfc1867_files.phpt
@@ -0,0 +1,29 @@
+--TEST--
+rfc1867: a huge number of file parts doesn't reach the hard limit
+--SKIPIF--
+<?php require __DIR__ . "/oom_rfc1867_skipif.inc"; ?>
+--FILE--
+<?php
+require __DIR__ . '/oom_rfc1867.inc';
+
+// Many tiny uploads, with a memory_limit of 8 MB (table and $_FILES)
+oom_rfc1867_request(function ($fh) {
+    for ($i = 0; $i < 30000; $i++) {
+        oom_rfc1867_part($fh,
+            "Content-Disposition: form-data; name=\"u$i\"; filename=\"f$i.txt\"\r\nContent-Type: text/plain",
+            'x');
+    }
+});
+
+// Same, as an array upload (lbuf/abuf)
+oom_rfc1867_request(function ($fh) {
+    for ($i = 0; $i < 30000; $i++) {
+        oom_rfc1867_part($fh,
+            "Content-Disposition: form-data; name=\"u[k$i]\"; filename=\"f$i.txt\"\r\nContent-Type: text/plain",
+            'x');
+    }
+});
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
diff --git a/tests/basic/oom_rfc1867_files_registration.phpt b/tests/basic/oom_rfc1867_files_registration.phpt
new file mode 100644
index 00000000000..0dc523666ba
--- /dev/null
+++ b/tests/basic/oom_rfc1867_files_registration.phpt
@@ -0,0 +1,58 @@
+--TEST--
+rfc1867: reaching the soft limit at each step of the $_FILES registration
+--SKIPIF--
+<?php
+require __DIR__ . "/oom_rfc1867_skipif.inc";
+if (getenv('SKIP_ASAN')) die("skip difficult to produce on ASAN");
+?>
+--FILE--
+<?php
+require __DIR__ . '/oom_rfc1867.inc';
+
+function oom_rfc1867_pad($fh, int $bytes): void
+{
+    if ($bytes <= 0) {
+        return;
+    }
+    fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+        . "Content-Disposition: form-data; name=\"pad\"\r\n\r\n");
+    oom_rfc1867_fill($fh, $bytes);
+    fwrite($fh, "\r\n");
+}
+
+/* How much padding it takes to leave a given amount of room depends on how the
+ * allocator accounts memory: Zend MM counts whole chunks, while the tracked
+ * allocator used under ASan counts exact sizes. */
+$padsKb = getenv('USE_ZEND_ALLOC') === '0'
+    ? [1400, 1700, 2000, 2300]
+    : [200, 292, 636, 1340];
+
+/* name="<700K>[k]" - the field name carries the weight. */
+foreach ($padsKb as $padKb) {
+    oom_rfc1867_request(function ($fh) use ($padKb) {
+        oom_rfc1867_pad($fh, $padKb * 1024);
+        fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+            . 'Content-Disposition: form-data; name="');
+        oom_rfc1867_fill($fh, 700 * 1024);
+        fwrite($fh, "[k]\"; filename=\"f.txt\"\r\nContent-Type: text/plain\r\n\r\nx\r\n");
+    });
+}
+
+/* name="u[<700K>]" - the weight is in the array index instead. */
+oom_rfc1867_request(function ($fh) {
+    oom_rfc1867_pad($fh, 1344 * 1024);
+    fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+        . 'Content-Disposition: form-data; name="u[');
+    oom_rfc1867_fill($fh, 700 * 1024);
+    fwrite($fh, "]\"; filename=\"f.txt\"\r\nContent-Type: text/plain\r\n\r\nx\r\n");
+});
+
+/* The [full_path] entry is in oom_rfc1867_files_registration_full_path.phpt:
+ * it needs a padding that only Zend MM's accounting can produce. */
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
diff --git a/tests/basic/oom_rfc1867_files_registration_full_path.phpt b/tests/basic/oom_rfc1867_files_registration_full_path.phpt
new file mode 100644
index 00000000000..c56c6fb3e44
--- /dev/null
+++ b/tests/basic/oom_rfc1867_files_registration_full_path.phpt
@@ -0,0 +1,26 @@
+--TEST--
+rfc1867: reaching the soft limit on the $_FILES [full_path] entry
+--SKIPIF--
+<?php
+require __DIR__ . "/oom_rfc1867_skipif.inc";
+if (getenv('SKIP_ASAN')) die("skip difficult to produce on ASAN");
+?>
+--FILE--
+<?php
+require __DIR__ . '/oom_rfc1867.inc';
+
+/* Huge file name: the weight lands on the [full_path] entry. */
+oom_rfc1867_request(function ($fh) {
+    fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+        . "Content-Disposition: form-data; name=\"pad\"\r\n\r\n");
+    oom_rfc1867_fill($fh, 1344 * 1024);
+    fwrite($fh, "\r\n");
+
+    fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+        . 'Content-Disposition: form-data; name="u"; filename="');
+    oom_rfc1867_fill($fh, 1200 * 1024);
+    fwrite($fh, "\"\r\nContent-Type: text/plain\r\n\r\nx\r\n");
+});
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
diff --git a/tests/basic/oom_rfc1867_form_value.phpt b/tests/basic/oom_rfc1867_form_value.phpt
new file mode 100644
index 00000000000..27f0c7464f9
--- /dev/null
+++ b/tests/basic/oom_rfc1867_form_value.phpt
@@ -0,0 +1,19 @@
+--TEST--
+rfc1867: a huge form variable value doesn't reach the hard limit
+--SKIPIF--
+<?php require __DIR__ . "/oom_rfc1867_skipif.inc"; ?>
+--FILE--
+<?php
+require __DIR__ . '/oom_rfc1867.inc';
+
+oom_rfc1867_request(function ($fh) {
+    fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+        . "Content-Disposition: form-data; name=\"f\"\r\n\r\n");
+    for ($i = 0; $i < 6; $i++) {
+        fwrite($fh, str_repeat('x', 1024 * 1024));
+    }
+    fwrite($fh, "\r\n");
+});
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
diff --git a/tests/basic/oom_rfc1867_header_continuation.phpt b/tests/basic/oom_rfc1867_header_continuation.phpt
new file mode 100644
index 00000000000..8eeba3ab9dc
--- /dev/null
+++ b/tests/basic/oom_rfc1867_header_continuation.phpt
@@ -0,0 +1,18 @@
+--TEST--
+rfc1867: a huge header value built from continuation lines doesn't reach the hard limit
+--SKIPIF--
+<?php require __DIR__ . "/oom_rfc1867_skipif.inc"; ?>
+--FILE--
+<?php
+require __DIR__ . '/oom_rfc1867.inc';
+
+oom_rfc1867_request(function ($fh) {
+    fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\nContent-Type: pad\r\n");
+    for ($i = 0; $i < 2500; $i++) {
+        fwrite($fh, ' ' . str_repeat('a', 4000) . "\r\n");
+    }
+    fwrite($fh, "Content-Disposition: form-data; name=\"f\"\r\n\r\nv\r\n");
+});
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
diff --git a/tests/basic/oom_rfc1867_header_count.phpt b/tests/basic/oom_rfc1867_header_count.phpt
new file mode 100644
index 00000000000..aa54e7425e9
--- /dev/null
+++ b/tests/basic/oom_rfc1867_header_count.phpt
@@ -0,0 +1,18 @@
+--TEST--
+rfc1867: a part with a huge number of headers doesn't reach the hard limit
+--SKIPIF--
+<?php require __DIR__ . "/oom_rfc1867_skipif.inc"; ?>
+--FILE--
+<?php
+require __DIR__ . '/oom_rfc1867.inc';
+
+oom_rfc1867_request(function ($fh) {
+    fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n");
+    for ($i = 0; $i < 150000; $i++) {
+        fwrite($fh, "X-$i: v\r\n");
+    }
+    fwrite($fh, "Content-Disposition: form-data; name=\"f\"\r\n\r\nv\r\n");
+});
+?>
+--EXPECT--
+post=1 files=0
diff --git a/tests/basic/oom_rfc1867_long_header_line.phpt b/tests/basic/oom_rfc1867_long_header_line.phpt
new file mode 100644
index 00000000000..073ee7bc76b
--- /dev/null
+++ b/tests/basic/oom_rfc1867_long_header_line.phpt
@@ -0,0 +1,45 @@
+--TEST--
+rfc1867: a huge Content-Disposition line doesn't reach the hard limit
+--SKIPIF--
+<?php require __DIR__ . "/oom_rfc1867_skipif.inc"; ?>
+--FILE--
+<?php
+require __DIR__ . '/oom_rfc1867.inc';
+
+oom_rfc1867_request(function ($fh) {
+    fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+        . 'Content-Disposition: form-data; name="');
+    for ($i = 0; $i < 5; $i++) {
+        fwrite($fh, str_repeat('A', 1024 * 1024));
+    }
+    fwrite($fh, "\"\r\n\r\nv\r\n");
+});
+
+oom_rfc1867_request(function ($fh) {
+    fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+        . 'Content-Disposition: form-data; name="u"; filename="');
+    for ($i = 0; $i < 6; $i++) {
+        fwrite($fh, str_repeat('A', 1024 * 1024));
+    }
+    fwrite($fh, ".txt\"\r\nContent-Type: text/plain\r\n\r\nx\r\n");
+});
+
+/* Both the field name and the file name huge in the same Content-Disposition:
+ * getword_conf() keeps a copy of each, so param and filename are retained at
+ * the same time on top of the header value they were parsed out of. */
+foreach ([2, 3] as $mb) {
+    oom_rfc1867_request(function ($fh) use ($mb) {
+        fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+            . 'Content-Disposition: form-data; name="');
+        oom_rfc1867_fill($fh, $mb * 1024 * 1024);
+        fwrite($fh, '"; filename="');
+        oom_rfc1867_fill($fh, $mb * 1024 * 1024);
+        fwrite($fh, "\"\r\nContent-Type: text/plain\r\n\r\nx\r\n");
+    });
+}
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
diff --git a/tests/basic/oom_rfc1867_mb_encoding.phpt b/tests/basic/oom_rfc1867_mb_encoding.phpt
new file mode 100644
index 00000000000..93f5f48d192
--- /dev/null
+++ b/tests/basic/oom_rfc1867_mb_encoding.phpt
@@ -0,0 +1,114 @@
+--TEST--
+rfc1867: mbstring input conversion of a body part doesn't reach the hard limit
+--SKIPIF--
+<?php require __DIR__ . "/oom_rfc1867_mb_skipif.inc"; ?>
+--FILE--
+<?php
+require __DIR__ . '/oom_rfc1867.inc';
+
+const MB_INI = [
+    'mbstring.encoding_translation=1',
+    'internal_encoding=UTF-8',
+    'mbstring.http_input=CP1252',
+    /* The conversion is what is under test, not the deprecation notice. */
+    'error_reporting=' . (E_ALL & ~E_DEPRECATED),
+];
+
+/* Sweep the size: the interesting case is the one where the part still fits
+ * under the soft limit and the conversion is what pushes past it, so the
+ * window matters. */
+for ($kb = 1216; $kb <= 1664; $kb += 32) {
+    oom_rfc1867_request(function ($fh) use ($kb) {
+        fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+            . "Content-Disposition: form-data; name=\"f\"\r\n\r\n");
+        oom_rfc1867_fill($fh, $kb * 1024, "\x80");
+        fwrite($fh, "\r\n");
+    }, '8M', MB_INI);
+}
+
+/* Same conversion, on the field name (getword_conf result) ... */
+foreach ([1664, 1792] as $kb) {
+    oom_rfc1867_request(function ($fh) use ($kb) {
+        fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+            . 'Content-Disposition: form-data; name="');
+        oom_rfc1867_fill($fh, $kb * 1024, "\x80");
+        fwrite($fh, "\"\r\n\r\nv\r\n");
+    }, '8M', MB_INI);
+}
+
+/* ... and on the file name. */
+foreach ([1664, 1792] as $kb) {
+    oom_rfc1867_request(function ($fh) use ($kb) {
+        fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+            . 'Content-Disposition: form-data; name="u"; filename="');
+        oom_rfc1867_fill($fh, $kb * 1024, "\x80");
+        fwrite($fh, "\"\r\nContent-Type: text/plain\r\n\r\nx\r\n");
+    }, '8M', MB_INI);
+}
+
+/* Bigger expansion factors, through a different mbfl encoder. */
+foreach ([704, 736, 960, 1248, 1472, 1632] as $kb) {
+    oom_rfc1867_request(function ($fh) use ($kb) {
+        fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+            . "Content-Disposition: form-data; name=\"f\"\r\n\r\n");
+        /* ISO-8859-8 0xE0 -> "&#1488;": one byte in, seven bytes out. */
+        oom_rfc1867_fill($fh, $kb * 1024, "\xE0");
+        fwrite($fh, "\r\n");
+    }, '8M', [
+        'mbstring.encoding_translation=1',
+        'internal_encoding=HTML-ENTITIES',
+        'mbstring.http_input=ISO-8859-8',
+        'error_reporting=' . (E_ALL & ~E_DEPRECATED),
+    ]);
+}
+
+/* UTF-32: a flat 4x on every input byte. The window where the conversion is
+ * what reaches the limit - rather than the body read before it - depends on how
+ * the allocator accounts memory, and Zend MM's chunks and the tracked
+ * allocator's exact sizes only overlap in a single point. */
+$utf32Kb = getenv('USE_ZEND_ALLOC') === '0'
+    ? [1152, 1280]
+    : [896, 1280];
+
+foreach ($utf32Kb as $kb) {
+    oom_rfc1867_request(function ($fh) use ($kb) {
+        fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+            . "Content-Disposition: form-data; name=\"f\"\r\n\r\n");
+        oom_rfc1867_fill($fh, $kb * 1024, "\x80");
+        fwrite($fh, "\r\n");
+    }, '8M', [
+        'mbstring.encoding_translation=1',
+        'internal_encoding=UTF-32',
+        'mbstring.http_input=CP1252',
+        'error_reporting=' . (E_ALL & ~E_DEPRECATED),
+    ]);
+}
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
diff --git a/tests/basic/oom_rfc1867_mb_skipif.inc b/tests/basic/oom_rfc1867_mb_skipif.inc
new file mode 100644
index 00000000000..77062e8e83d
--- /dev/null
+++ b/tests/basic/oom_rfc1867_mb_skipif.inc
@@ -0,0 +1,5 @@
+<?php
+require __DIR__ . "/oom_rfc1867_skipif.inc";
+if (!extension_loaded('mbstring')) {
+    die("skip mbstring not available");
+}
diff --git a/tests/basic/oom_rfc1867_post_vars.phpt b/tests/basic/oom_rfc1867_post_vars.phpt
new file mode 100644
index 00000000000..9cdd812e08d
--- /dev/null
+++ b/tests/basic/oom_rfc1867_post_vars.phpt
@@ -0,0 +1,16 @@
+--TEST--
+rfc1867: a huge number of form variables doesn't reach the hard limit
+--SKIPIF--
+<?php require __DIR__ . "/oom_rfc1867_skipif.inc"; ?>
+--FILE--
+<?php
+require __DIR__ . '/oom_rfc1867.inc';
+
+oom_rfc1867_request(function ($fh) {
+    for ($i = 0; $i < 70000; $i++) {
+        oom_rfc1867_part($fh, "Content-Disposition: form-data; name=\"f$i\"", "v$i");
+    }
+});
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
diff --git a/tests/basic/oom_rfc1867_session_skipif.inc b/tests/basic/oom_rfc1867_session_skipif.inc
new file mode 100644
index 00000000000..94fb6ee5d2d
--- /dev/null
+++ b/tests/basic/oom_rfc1867_session_skipif.inc
@@ -0,0 +1,5 @@
+<?php
+require __DIR__ . "/oom_rfc1867_skipif.inc";
+if (!extension_loaded('session')) {
+    die("skip session not available");
+}
diff --git a/tests/basic/oom_rfc1867_skipif.inc b/tests/basic/oom_rfc1867_skipif.inc
new file mode 100644
index 00000000000..9aa518d142a
--- /dev/null
+++ b/tests/basic/oom_rfc1867_skipif.inc
@@ -0,0 +1,7 @@
+<?php
+if (!getenv('TEST_PHP_CGI_EXECUTABLE')) {
+    die("skip php-cgi not available");
+}
+if (substr(PHP_OS, 0, 3) == 'WIN') {
+    die("skip not for Windows in CI - probably resource issue");
+}
diff --git a/tests/basic/oom_rfc1867_target.inc b/tests/basic/oom_rfc1867_target.inc
new file mode 100644
index 00000000000..695095bebbc
--- /dev/null
+++ b/tests/basic/oom_rfc1867_target.inc
@@ -0,0 +1,2 @@
+<?php
+printf("post=%d files=%d\n", count($_POST), count($_FILES));
diff --git a/tests/basic/oom_rfc1867_upload_progress.phpt b/tests/basic/oom_rfc1867_upload_progress.phpt
new file mode 100644
index 00000000000..77512e33719
--- /dev/null
+++ b/tests/basic/oom_rfc1867_upload_progress.phpt
@@ -0,0 +1,98 @@
+--TEST--
+rfc1867: the session upload progress handler doesn't reach the hard limit
+--SKIPIF--
+<?php
+require __DIR__ . "/oom_rfc1867_session_skipif.inc";
+if (getenv('SKIP_ASAN')) die("skip slow under ASan");
+?>
+--FILE--
+<?php
+require __DIR__ . '/oom_rfc1867.inc';
+
+$savePath = sys_get_temp_dir() . '/oom_rfc1867_sess';
+@mkdir($savePath);
+
+$sessionIni = [
+    'session.upload_progress.enabled=1',
+    'session.save_path=' . $savePath,
+    /* accept the session id we make up below */
+    'session.use_strict_mode=0',
+    /* update on every chunk, so FILE_DATA re-encodes as often as possible */
+    'session.upload_progress.freq=1',
+    'session.upload_progress.min_freq=0',
+];
+
+/* The progress handler only engages once it has seen both the session id and
+ * the progress key, so those two parts come first. */
+function oom_rfc1867_progress_preamble($fh): void
+{
+    oom_rfc1867_part($fh, 'Content-Disposition: form-data; name="PHPSESSID"',
+        'abcdef0123456789abcdef0123456789');
+    oom_rfc1867_part($fh, 'Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"', 'k');
+}
+
+/* Many small file parts: $_SESSION[...]['files'] grows per part, and the whole
+ * session is re-encoded each time. */
+foreach ([4000, 8000] as $parts) {
+    oom_rfc1867_request(function ($fh) use ($parts) {
+        oom_rfc1867_progress_preamble($fh);
+        for ($i = 0; $i < $parts; $i++) {
+            oom_rfc1867_part($fh,
+                "Content-Disposition: form-data; name=\"u$i\"; filename=\"f$i.txt\"\r\nContent-Type: text/plain",
+                'xxxxxxxx');
+        }
+    }, '4M', $sessionIni);
+}
+
+/* Fewer parts, but each one makes the encoded session much bigger. */
+foreach ([[1000, 1024], [2000, 512]] as [$parts, $nameLen]) {
+    oom_rfc1867_request(function ($fh) use ($parts, $nameLen) {
+        oom_rfc1867_progress_preamble($fh);
+        for ($i = 0; $i < $parts; $i++) {
+            $name = str_pad("u$i", $nameLen, 'A');
+            oom_rfc1867_part($fh,
+                "Content-Disposition: form-data; name=\"$name\"; filename=\"$name.txt\"\r\nContent-Type: text/plain",
+                'xxxxxxxx');
+        }
+    }, '8M', $sessionIni);
+}
+
+/* A single part whose field name and file name are both huge: the handler
+ * copies both into the progress array before the parser's own checks run. */
+oom_rfc1867_request(function ($fh) {
+    oom_rfc1867_progress_preamble($fh);
+    fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+        . 'Content-Disposition: form-data; name="');
+    oom_rfc1867_fill($fh, 1024 * 1024);
+    fwrite($fh, '"; filename="');
+    oom_rfc1867_fill($fh, 1024 * 1024);
+    fwrite($fh, "\"\r\nContent-Type: text/plain\r\n\r\nxxxx\r\n");
+}, '8M', $sessionIni);
+
+/* A huge PHP_SESSION_UPLOAD_PROGRESS value: the handler copies it into the
+ * progress key on MULTIPART_EVENT_FORMDATA, so the soft limit is reached
+ * inside the handler rather than in the parser. */
+oom_rfc1867_request(function ($fh) {
+    oom_rfc1867_part($fh, 'Content-Disposition: form-data; name="PHPSESSID"',
+        'abcdef0123456789abcdef0123456789');
+    fwrite($fh, '--' . OOM_RFC1867_BOUNDARY . "\r\n"
+        . "Content-Disposition: form-data; name=\"PHP_SESSION_UPLOAD_PROGRESS\"\r\n\r\n");
+    oom_rfc1867_fill($fh, 1024 * 1024);
+    fwrite($fh, "\r\n");
+    oom_rfc1867_part($fh,
+        "Content-Disposition: form-data; name=\"u\"; filename=\"f.txt\"\r\nContent-Type: text/plain",
+        'x');
+}, '4M', $sessionIni);
+
+foreach (glob($savePath . '/*') as $f) {
+    @unlink($f);
+}
+@rmdir($savePath);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 4194304 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 4194304 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 8388608 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
+Fatal error: Allowed memory size of 4194304 bytes exhausted %S(tried to allocate %d bytes) in Unknown on line 0
diff --git a/tests/output/gh15179.phpt b/tests/output/gh15179.phpt
index 207728446df..11942e268b6 100644
--- a/tests/output/gh15179.phpt
+++ b/tests/output/gh15179.phpt
@@ -15,4 +15,4 @@
 output_add_rewrite_var($var, $var);
 ?>
 --EXPECTF--
-Fatal error: Allowed memory size of %d bytes exhausted %s
+%rHard memory limit|Fatal error: Allowed memory size%r of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d%r(\n\nTermsig=9)?%r
diff --git a/tests/output/oom_in_internal_output_handler.phpt b/tests/output/oom_in_internal_output_handler.phpt
new file mode 100644
index 00000000000..2ad58e5566f
--- /dev/null
+++ b/tests/output/oom_in_internal_output_handler.phpt
@@ -0,0 +1,20 @@
+--TEST--
+OOM inside an internal output handler must not free the running handler
+--EXTENSIONS--
+mbstring
+--INI--
+memory_limit=64M
+output_encoding=HTML-ENTITIES
+mbstring.encoding_translation=1
+--FILE--
+<?php
+/* mb_output_handler() is an internal function, so a delayed OOM raised while it
+ * runs is only noticed after it returns, in zend_call_function(). The fatal
+ * error raised there must not discard the output buffers: doing so frees every
+ * handler on the stack, including the one php_output_stack_pop() is still
+ * holding. */
+ob_start('mb_output_handler');
+echo str_repeat("\u{00e9}", 200000);
+ini_set('memory_limit', memory_get_usage(true) + 1024 * 1024);
+?>
+--EXPECT--