Skip to content

Fix memory leak in filter_var_array() when a nested element throws - #25365

Open
eyupcanakman wants to merge 1 commit into
php:PHP-8.5from
eyupcanakman:filter-var-array-throw-leak
Open

eyupcanakman wants to merge 1 commit into
php:PHP-8.5from
eyupcanakman:filter-var-array-throw-leak

Conversation

@eyupcanakman

Copy link
Copy Markdown
Contributor

php_filter_array_handler() filters a copy of each input value, nval, and returns as soon as an exception is pending. It never frees that copy, so an array value leaks.

filter_var_array(['a' => ['1', 'x']], ['a' => ['filter' => FILTER_VALIDATE_INT, 'flags' => FILTER_REQUIRE_ARRAY | FILTER_THROW_ON_FAILURE]]);

On a debug build the exception is followed by === Total 2 memory leaks detected ===.

A FILTER_CALLBACK callback that throws on a nested element leaks the same way, and so does filter_input_array(), which uses the same handler. The early return came in with FILTER_THROW_ON_FAILURE, so PHP-8.4 does not have it and does not leak. GH-25349 adds the return to 8.4 with the copy already freed.

php_filter_array_handler() returns on a pending exception without freeing nval, its copy of the input value, so an array value leaks.
Free it before returning.
filter
--FILE--
<?php
function leakcheck(callable $fn): bool {

@devnexen devnexen Nov 11, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might be possible to simplify a tad

--- a/ext/filter/tests/filter_var_array_exception_leak.phpt
+++ b/ext/filter/tests/filter_var_array_exception_leak.phpt
@@ -4,34 +4,23 @@
 filter
 --FILE--
 <?php
-function leakcheck(callable $fn): bool {
-    $fn();
-    $before = memory_get_usage();
-    for ($i = 0; $i < 2000; $i++) {
-        $fn();
-    }
-    return memory_get_usage() - $before === 0;
+try {
+    filter_var_array(['a' => ['1', 'x']], ['a' => ['filter' => FILTER_VALIDATE_INT, 'flags' => FILTER_REQUIRE_ARRAY | FILTER_THROW_ON_FAILURE]]);
+} catch (Filter\FilterFailedException $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
 }

-var_dump(leakcheck(function () {
-    try {
-        filter_var_array(['a' => ['1', 'x']], ['a' => ['filter' => FILTER_VALIDATE_INT, 'flags' => FILTER_REQUIRE_ARRAY | FILTER_THROW_ON_FAILURE]]);
-    } catch (Filter\FilterFailedException $e) {
-    }
-}));
-
-var_dump(leakcheck(function () {
-    try {
-        filter_var_array(['a' => ['1', 'x']], ['a' => ['filter' => FILTER_CALLBACK, 'options' => function ($value) {
-            if ($value === 'x') {
-                throw new Exception('callback failed');
-            }
-            return $value;
-        }]]);
-    } catch (Exception $e) {
-    }
-}));
+try {
+    filter_var_array(['a' => ['1', 'x']], ['a' => ['filter' => FILTER_CALLBACK, 'options' => function ($value) {
+        if ($value === 'x') {
+            throw new Exception('callback failed');
+        }
+        return $value;
+    }]]);
+} catch (Exception $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
 ?>
 --EXPECT--
-bool(true)
-bool(true)
+Filter\FilterFailedException: filter validation failed: filter int not satisfied by 'x'
+Exception: callback failed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants