Skip to content

Crash in FILTER_VALIDATE_INT when __toString() mutates the referenced options array #24579

Open

Description

@Meecoy

Description

Crash in FILTER_VALIDATE_INT when __toString() mutates the referenced options array

Summary

Calling filter_var() with FILTER_VALIDATE_INT crashes when the input object implements __toString() and modifies the options array referenced by the filter arguments during string conversion.

Reproduced on PHP 8.5.12.

A non-debug build terminated with SIGSEGV. A debug build terminated with SIGABRT after Zend detected an inconsistent hash table state.

Minimal reproducer

<?php

$shared = ['min_range' => 0];
$args = ['options' => &$shared];

class Reenter
{
    public function __toString(): string
    {
        global $shared;
        $shared = 'changed';
        return '1';
    }
}

filter_var(new Reenter(), FILTER_VALIDATE_INT, $args);

Steps to reproduce

  1. Build PHP 8.5.12 from php-src with the Filter extension enabled.
  2. Run the script above using the built PHP binary.

Build configuration used on Arch Linux:

./buildconf --force
./configure --disable-all --enable-cli --enable-debug --enable-filter \
  CFLAGS="-O0 -g3 -fno-omit-frame-pointer"
make -j"$(nproc)"

Run the reproducer:

./sapi/cli/php repro.php

Observed behavior

In the debug build, execution terminates with an assertion failure similar to:

Zend/zend_hash.c(2919) : ht=0x7ffff78027c0 is being cleaned
php: Zend/zend_hash.c:73: _zend_is_inconsistent:
Assertion `0' failed.
Program received signal SIGABRT

The backtrace points to zend_hash_str_find() called from php_filter_int():

#5 _zend_is_inconsistent(...)
#6 zend_hash_str_find(ht=0x7ffff78027c0, str="min_range", len=9)
#7 php_filter_int(...)
   at ext/filter/logical_filters.c:213
#8 php_zval_filter(...)
   at ext/filter/filter.c:289
#9 php_filter_call(...)
   at ext/filter/filter.c:610
#10 zif_filter_var(...)
   at ext/filter/filter.c:787

At frame 7, the option_array zval was observed to have type IS_STRING, while the filter code attempted to access it as a hash table through Z_ARRVAL_P(option_array).

A non-debug build previously terminated with SIGSEGV for the same reproducer.

Expected behavior

The call should not crash the PHP process.

If the options argument becomes invalid during reentrant execution, PHP should handle the situation safely, for example by rejecting invalid options or returning a filter failure according to the applicable API semantics.

Initial investigation

In ext/filter/logical_filters.c, the FETCH_LONG_OPTION macro accesses the options argument as a hash table:

#define FETCH_LONG_OPTION(var_name, option_name) \
    var_name = 0; \
    var_name##_set = 0; \
    if (option_array) { \
        if ((option_val = zend_hash_str_find( \
            Z_ARRVAL_P(option_array), \
            option_name, \
            sizeof(option_name) - 1)) != NULL) { \
            var_name = zval_get_long(option_val); \
            var_name##_set = 1; \
        } \
    }

The macro checks whether option_array is non-null, but does not explicitly check that the zval is an array before passing its contents to zend_hash_str_find().

The filter call path performs string conversion on the input value before invoking the filter function. Since __toString() can execute userland code and mutate $shared, the options value may change during this conversion.

This appears to create a reentrancy-related invalid-state condition in the filter options handling. The precise underlying mechanism has not yet been confirmed.

Environment

  • PHP version: 8.5.12
  • Operating system: Arch Linux
  • Build: locally compiled from php-src, with debug symbols
  • Configure options: --disable-all --enable-cli --enable-debug --enable-filter
  • Compiler optimization: -O0

Suggested investigation

  • Verify whether the options zval can be changed through the referenced argument during input string conversion.
  • Check whether the filter implementation should validate the zval type before using Z_ARRVAL_P().
  • Review the relevant argument handling and ownership semantics in php_filter_call(), php_zval_filter(), and zif_filter_var().
  • Add a regression test ensuring that reentrant mutation of the options argument cannot crash PHP.

PHP Version

PHP 8.5.12 (cli) (built: Oct 21 2026 20:05:19) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.5.12, Copyright (c) Zend Technologies
    with Zend OPcache v8.5.12, Copyright (c), by Zend Technologies

Operating System

Linux 7.2.8-2-cachyos

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions