Skip to content

Commit 235b9b5

Browse files
committed
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
1 parent 6124206 commit 235b9b5

2 files changed

Lines changed: 39 additions & 2 deletions

File tree

ext/filter/filter.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,10 +298,10 @@ static void php_zval_filter(zval *value, zend_long filter, zend_long flags, zval
298298
filter_func.name,
299299
ZSTR_VAL(copy_for_throwing)
300300
);
301-
zend_string_delref(copy_for_throwing);
301+
zend_string_release(copy_for_throwing);
302302
return;
303303
}
304-
zend_string_delref(copy_for_throwing);
304+
zend_string_release(copy_for_throwing);
305305
copy_for_throwing = NULL;
306306
}
307307

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--TEST--
2+
filter: FILTER_THROW_ON_FAILURE does not leak the preserved input string
3+
--EXTENSIONS--
4+
filter
5+
--FILE--
6+
<?php
7+
// php_zval_filter() copies the input string so it can be quoted in the
8+
// exception message. A non-string scalar input (here a float / a large int)
9+
// is turned into a fresh heap string by convert_to_string(), so the copy is
10+
// the sole extra owner. Releasing it with zend_string_delref() decremented
11+
// without freeing, leaking one string per call on both the failure and the
12+
// success path. Loop and assert memory stays flat.
13+
function leakcheck(callable $fn): bool {
14+
$fn();
15+
$before = memory_get_usage();
16+
for ($i = 0; $i < 2000; $i++) {
17+
$fn();
18+
}
19+
return memory_get_usage() - $before === 0;
20+
}
21+
22+
// Validation fails -> exception thrown.
23+
var_dump(leakcheck(function () {
24+
try {
25+
filter_var(1.5, FILTER_VALIDATE_INT, ['flags' => FILTER_THROW_ON_FAILURE]);
26+
} catch (\Filter\FilterFailedException $e) {
27+
}
28+
}));
29+
30+
// Validation succeeds.
31+
var_dump(leakcheck(function () {
32+
filter_var(15, FILTER_VALIDATE_INT, ['flags' => FILTER_THROW_ON_FAILURE]);
33+
}));
34+
?>
35+
--EXPECT--
36+
bool(true)
37+
bool(true)

0 commit comments

Comments
 (0)