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, ¶m);
- 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 -> "א": 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--