Commit 235b9b5cc16 for php.net
commit 235b9b5cc16b346c6fb0cb423e22e12214bef661
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Tue Jun 16 18:17:08 2026 -0400
Fix leak of preserved input string with FILTER_THROW_ON_FAILURE
php_zval_filter() copies the filtered value so it can be quoted in the
FilterFailedException message, then released the copy with
zend_string_delref(), which only decrements the refcount. When the input
is a non-string scalar that convert_to_string() turns into a fresh heap
string, the copy was the sole owner and leaked one string per call on
both the failure and the success path. Use zend_string_release() so it is
freed at refcount zero.
Closes GH-22339
diff --git a/ext/filter/filter.c b/ext/filter/filter.c
index 4a928379877..a169ecf987d 100644
--- a/ext/filter/filter.c
+++ b/ext/filter/filter.c
@@ -298,10 +298,10 @@ static void php_zval_filter(zval *value, zend_long filter, zend_long flags, zval
filter_func.name,
ZSTR_VAL(copy_for_throwing)
);
- zend_string_delref(copy_for_throwing);
+ zend_string_release(copy_for_throwing);
return;
}
- zend_string_delref(copy_for_throwing);
+ zend_string_release(copy_for_throwing);
copy_for_throwing = NULL;
}
diff --git a/ext/filter/tests/filter_throw_on_failure_leak.phpt b/ext/filter/tests/filter_throw_on_failure_leak.phpt
new file mode 100644
index 00000000000..d42896a94e8
--- /dev/null
+++ b/ext/filter/tests/filter_throw_on_failure_leak.phpt
@@ -0,0 +1,37 @@
+--TEST--
+filter: FILTER_THROW_ON_FAILURE does not leak the preserved input string
+--EXTENSIONS--
+filter
+--FILE--
+<?php
+// php_zval_filter() copies the input string so it can be quoted in the
+// exception message. A non-string scalar input (here a float / a large int)
+// is turned into a fresh heap string by convert_to_string(), so the copy is
+// the sole extra owner. Releasing it with zend_string_delref() decremented
+// without freeing, leaking one string per call on both the failure and the
+// success path. Loop and assert memory stays flat.
+function leakcheck(callable $fn): bool {
+ $fn();
+ $before = memory_get_usage();
+ for ($i = 0; $i < 2000; $i++) {
+ $fn();
+ }
+ return memory_get_usage() - $before === 0;
+}
+
+// Validation fails -> exception thrown.
+var_dump(leakcheck(function () {
+ try {
+ filter_var(1.5, FILTER_VALIDATE_INT, ['flags' => FILTER_THROW_ON_FAILURE]);
+ } catch (\Filter\FilterFailedException $e) {
+ }
+}));
+
+// Validation succeeds.
+var_dump(leakcheck(function () {
+ filter_var(15, FILTER_VALIDATE_INT, ['flags' => FILTER_THROW_ON_FAILURE]);
+}));
+?>
+--EXPECT--
+bool(true)
+bool(true)