Commit 358d9c54a66 for php.net

commit 358d9c54a66de3ed03708abbdb2caa7e75e8c607
Author: Jakub Zelenka <bukka@php.net>
Date:   Mon Mar 30 10:27:34 2026 +0200

    Fix bug #49874: ftell() and fseek() inconsistency when using stream filters (#19981)

    Currently filter seeking does not work correctly for most streams. The
    idea is to extend API to allow seeking for some streams. There are
    couple of cases:

    - filter is always seekable - e.g. string.rot13, string.toupper,
      string.tolower
    - filter is seekable only when sought to start or when it re-run the
      data from the start (it means when there is some data buffered) -
      this is for pretty much all other filters that keep some state.
    - user filters are seekable by default for BC reason but if new
      seek method is implemented, then it is called and based on the
      bool result the seeking either fails or succeed.

diff --git a/NEWS b/NEWS
index 54a45028c6a..e75b26e7d14 100644
--- a/NEWS
+++ b/NEWS
@@ -153,6 +153,8 @@ PHP                                                                        NEWS
   . Fixed bug GH-21221 (Prevent closing of innerstream of php://temp stream).
     (ilutov)
   . Improved stream_socket_server() bind failure error reporting. (ilutov)
+  . Fixed bug #49874 (ftell() and fseek() inconsistency when using stream
+    filters). (Jakub Zelenka)

 - Zip:
   . Fixed ZipArchive callback being called after executor has shut down.
diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS
index 57bef65c25e..656e1d556a5 100644
--- a/UPGRADING.INTERNALS
+++ b/UPGRADING.INTERNALS
@@ -74,6 +74,7 @@ PHP 8.6 INTERNALS UPGRADE NOTES
     longer is a pointer, but a directly embedded HashTable struct.
   . Added a C23_ENUM() helper macro to define forward-compatible fixed-size
     enums.
+  . Extended php_stream_filter_ops with seek method.

 ========================
 2. Build system changes
diff --git a/ext/bz2/bz2_filter.c b/ext/bz2/bz2_filter.c
index 27059391dac..69ee483d21f 100644
--- a/ext/bz2/bz2_filter.c
+++ b/ext/bz2/bz2_filter.c
@@ -42,6 +42,10 @@ typedef struct _php_bz2_filter_data {
 	unsigned int is_flushed : 1;          /* only for compression */

 	int persistent;
+
+	/* Configuration for reset - immutable */
+	int blockSize100k;  /* compress only */
+	int workFactor;     /* compress only */
 } php_bz2_filter_data;

 /* }}} */
@@ -178,6 +182,36 @@ static php_stream_filter_status_t php_bz2_decompress_filter(
 	return exit_status;
 }

+static zend_result php_bz2_decompress_seek(
+	php_stream *stream,
+	php_stream_filter *thisfilter,
+	zend_off_t offset,
+	int whence
+	)
+{
+	if (!Z_PTR(thisfilter->abstract)) {
+		return FAILURE;
+	}
+
+	php_bz2_filter_data *data = Z_PTR(thisfilter->abstract);
+
+	/* End current decompression if running */
+	if (data->status == PHP_BZ2_RUNNING) {
+		BZ2_bzDecompressEnd(&(data->strm));
+	}
+
+	/* Reset stream state */
+	data->strm.next_in = data->inbuf;
+	data->strm.avail_in = 0;
+	data->strm.next_out = data->outbuf;
+	data->strm.avail_out = data->outbuf_len;
+	data->status = PHP_BZ2_UNINITIALIZED;
+
+	/* Note: We don't reinitialize here - it will be done on first use in the filter function */
+
+	return SUCCESS;
+}
+
 static void php_bz2_decompress_dtor(php_stream_filter *thisfilter)
 {
 	if (thisfilter && Z_PTR(thisfilter->abstract)) {
@@ -193,6 +227,7 @@ static void php_bz2_decompress_dtor(php_stream_filter *thisfilter)

 static const php_stream_filter_ops php_bz2_decompress_ops = {
 	php_bz2_decompress_filter,
+	php_bz2_decompress_seek,
 	php_bz2_decompress_dtor,
 	"bzip2.decompress"
 };
@@ -288,6 +323,41 @@ static php_stream_filter_status_t php_bz2_compress_filter(
 	return exit_status;
 }

+static zend_result php_bz2_compress_seek(
+	php_stream *stream,
+	php_stream_filter *thisfilter,
+	zend_off_t offset,
+	int whence
+	)
+{
+	int status;
+
+	if (!Z_PTR(thisfilter->abstract)) {
+		return FAILURE;
+	}
+
+	php_bz2_filter_data *data = Z_PTR(thisfilter->abstract);
+
+	/* End current compression */
+	BZ2_bzCompressEnd(&(data->strm));
+
+	/* Reset stream state */
+	data->strm.next_in = data->inbuf;
+	data->strm.avail_in = 0;
+	data->strm.next_out = data->outbuf;
+	data->strm.avail_out = data->outbuf_len;
+	data->is_flushed = 1;
+
+	/* Reinitialize compression with saved configuration */
+	status = BZ2_bzCompressInit(&(data->strm), data->blockSize100k, 0, data->workFactor);
+	if (status != BZ_OK) {
+		php_error_docref(NULL, E_WARNING, "bzip2.compress: failed to reset compression state");
+		return FAILURE;
+	}
+
+	return SUCCESS;
+}
+
 static void php_bz2_compress_dtor(php_stream_filter *thisfilter)
 {
 	if (Z_PTR(thisfilter->abstract)) {
@@ -301,6 +371,7 @@ static void php_bz2_compress_dtor(php_stream_filter *thisfilter)

 static const php_stream_filter_ops php_bz2_compress_ops = {
 	php_bz2_compress_filter,
+	php_bz2_compress_seek,
 	php_bz2_compress_dtor,
 	"bzip2.compress"
 };
@@ -309,7 +380,7 @@ static const php_stream_filter_ops php_bz2_compress_ops = {

 /* {{{ bzip2.* common factory */

-static php_stream_filter *php_bz2_filter_create(const char *filtername, zval *filterparams, uint8_t persistent)
+static php_stream_filter *php_bz2_filter_create(const char *filtername, zval *filterparams, bool persistent)
 {
 	const php_stream_filter_ops *fops = NULL;
 	php_bz2_filter_data *data;
@@ -388,6 +459,10 @@ static php_stream_filter *php_bz2_filter_create(const char *filtername, zval *fi
 			}
 		}

+		/* Save configuration for reset */
+		data->blockSize100k = blockSize100k;
+		data->workFactor = workFactor;
+
 		status = BZ2_bzCompressInit(&(data->strm), blockSize100k, 0, workFactor);
 		data->is_flushed = 1;
 		fops = &php_bz2_compress_ops;
@@ -403,7 +478,7 @@ static php_stream_filter *php_bz2_filter_create(const char *filtername, zval *fi
 		return NULL;
 	}

-	return php_stream_filter_alloc(fops, data, persistent);
+	return php_stream_filter_alloc(fops, data, persistent, PSFS_SEEKABLE_START);
 }

 const php_stream_filter_factory php_bz2_filter_factory = {
diff --git a/ext/bz2/tests/bz2_filter_seek_compress.phpt b/ext/bz2/tests/bz2_filter_seek_compress.phpt
new file mode 100644
index 00000000000..0656b244484
--- /dev/null
+++ b/ext/bz2/tests/bz2_filter_seek_compress.phpt
@@ -0,0 +1,55 @@
+--TEST--
+bzip2.compress filter with seek to start
+--EXTENSIONS--
+bz2
+--FILE--
+<?php
+$file = __DIR__ . '/bz2_filter_seek_compress.bz2';
+
+$text1 = 'Short text.';
+$text2 = 'This is a much longer text that will completely overwrite the previous compressed data in the file.';
+
+$fp = fopen($file, 'w+');
+stream_filter_append($fp, 'bzip2.compress', STREAM_FILTER_WRITE);
+
+fwrite($fp, $text1);
+fflush($fp);
+
+$size1 = ftell($fp);
+echo "Size after first write: $size1\n";
+
+$result = fseek($fp, 0, SEEK_SET);
+echo "Seek to start: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
+
+fwrite($fp, $text2);
+fflush($fp);
+
+$size2 = ftell($fp);
+echo "Size after second write: $size2\n";
+echo "Second write is larger: " . ($size2 > $size1 ? "YES" : "NO") . "\n";
+
+$result = fseek($fp, 50, SEEK_SET);
+echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
+
+fclose($fp);
+
+$fp = fopen($file, 'r');
+stream_filter_append($fp, 'bzip2.decompress', STREAM_FILTER_READ);
+$content = stream_get_contents($fp);
+fclose($fp);
+
+echo "Decompressed content matches text2: " . ($content === $text2 ? "YES" : "NO") . "\n";
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/bz2_filter_seek_compress.bz2');
+?>
+--EXPECTF--
+Size after first write: 40
+Seek to start: SUCCESS
+Size after second write: 98
+Second write is larger: YES
+
+Warning: fseek(): Stream filter bzip2.compress is seekable only to start position in %s on line %d
+Seek to middle: FAILURE
+Decompressed content matches text2: YES
diff --git a/ext/bz2/tests/bz2_filter_seek_decompress.phpt b/ext/bz2/tests/bz2_filter_seek_decompress.phpt
new file mode 100644
index 00000000000..8ccd224b1a4
--- /dev/null
+++ b/ext/bz2/tests/bz2_filter_seek_decompress.phpt
@@ -0,0 +1,43 @@
+--TEST--
+bzip2.decompress filter with seek to start
+--EXTENSIONS--
+bz2
+--FILE--
+<?php
+$file = __DIR__ . '/bz2_filter_seek_decompress.bz2';
+
+$text = 'I am the very model of a modern major general, I\'ve information vegetable, animal, and mineral.';
+
+$fp = fopen($file, 'w');
+stream_filter_append($fp, 'bzip2.compress', STREAM_FILTER_WRITE);
+fwrite($fp, $text);
+fclose($fp);
+
+$fp = fopen($file, 'r');
+stream_filter_append($fp, 'bzip2.decompress', STREAM_FILTER_READ);
+
+$partial = fread($fp, 20);
+echo "First read (20 bytes): " . $partial . "\n";
+
+$result = fseek($fp, 0, SEEK_SET);
+echo "Seek to start: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
+
+$full = stream_get_contents($fp);
+echo "Content after seek matches: " . ($full === $text ? "YES" : "NO") . "\n";
+
+$result = fseek($fp, 50, SEEK_SET);
+echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
+
+fclose($fp);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/bz2_filter_seek_decompress.bz2');
+?>
+--EXPECTF--
+First read (20 bytes): I am the very model
+Seek to start: SUCCESS
+Content after seek matches: YES
+
+Warning: fseek(): Stream filter bzip2.decompress is seekable only to start position in %s on line %d
+Seek to middle: FAILURE
diff --git a/ext/gd/tests/bug79945.phpt b/ext/gd/tests/bug79945.phpt
index 5db958e36d3..46dac358d8e 100644
--- a/ext/gd/tests/bug79945.phpt
+++ b/ext/gd/tests/bug79945.phpt
@@ -9,17 +9,17 @@
 }
 set_error_handler(function($errno, $errstr) {
     if (str_contains($errstr, 'Cannot cast a filtered stream on this system')) {
-        die('skip: fopencookie not support on this system');
+        die('skip: fopencookie not supported on this system');
     }
 });
-imagecreatefrompng('php://filter/read=convert.base64-encode/resource=' . __DIR__ . '/test.png');
+imagecreatefrompng('php://filter/read=string.rot13/resource=' . __DIR__ . '/test.png');
 restore_error_handler();
 ?>
 --FILE--
 <?php
-imagecreatefrompng('php://filter/read=convert.base64-encode/resource=' . __DIR__ . '/test.png');
+imagecreatefrompng('php://filter/read=string.rot13/resource=' . __DIR__ . '/test.png');
 ?>
 --CLEAN--
 --EXPECTF--

-Warning: imagecreatefrompng(): "php://filter/read=convert.base64-encode/resource=%s" is not a valid PNG file in %s on line %d
+Warning: imagecreatefrompng(): "php://filter/read=string.rot13/resource=%s" is not a valid PNG file in %s on line %d
diff --git a/ext/iconv/iconv.c b/ext/iconv/iconv.c
index ed7d332397d..02cffcb4b85 100644
--- a/ext/iconv/iconv.c
+++ b/ext/iconv/iconv.c
@@ -2585,6 +2585,33 @@ static php_stream_filter_status_t php_iconv_stream_filter_do_filter(
 }
 /* }}} */

+/* {{{ php_iconv_stream_filter_seek */
+static zend_result php_iconv_stream_filter_seek(
+		php_stream *stream,
+		php_stream_filter *filter,
+		zend_off_t offset,
+		int whence)
+{
+	php_iconv_stream_filter *self = (php_iconv_stream_filter *)Z_PTR(filter->abstract);
+
+	/* Reset stub buffer */
+	self->stub_len = 0;
+
+	/* Reset iconv conversion state by closing and reopening the converter */
+	iconv_close(self->cd);
+
+	self->cd = iconv_open(self->to_charset, self->from_charset);
+	if ((iconv_t)-1 == self->cd) {
+		php_error_docref(NULL, E_WARNING,
+				"iconv stream filter (\"%s\"=>\"%s\"): failed to reset conversion state",
+				self->from_charset, self->to_charset);
+		return FAILURE;
+	}
+
+	return SUCCESS;
+}
+/* }}} */
+
 /* {{{ php_iconv_stream_filter_cleanup */
 static void php_iconv_stream_filter_cleanup(php_stream_filter *filter)
 {
@@ -2595,12 +2622,13 @@ static void php_iconv_stream_filter_cleanup(php_stream_filter *filter)

 static const php_stream_filter_ops php_iconv_stream_filter_ops = {
 	php_iconv_stream_filter_do_filter,
+	php_iconv_stream_filter_seek,
 	php_iconv_stream_filter_cleanup,
 	"convert.iconv.*"
 };

 /* {{{ php_iconv_stream_filter_create */
-static php_stream_filter *php_iconv_stream_filter_factory_create(const char *name, zval *params, uint8_t persistent)
+static php_stream_filter *php_iconv_stream_filter_factory_create(const char *name, zval *params, bool persistent)
 {
 	php_iconv_stream_filter *inst;
 	const char *from_charset = NULL, *to_charset = NULL;
@@ -2632,7 +2660,8 @@ static php_stream_filter *php_iconv_stream_filter_factory_create(const char *nam
 		return NULL;
 	}

-	return php_stream_filter_alloc(&php_iconv_stream_filter_ops, inst, persistent);
+	return php_stream_filter_alloc(&php_iconv_stream_filter_ops, inst, persistent,
+			PSFS_SEEKABLE_START);
 }
 /* }}} */

diff --git a/ext/iconv/tests/iconv_stream_filter_seek.phpt b/ext/iconv/tests/iconv_stream_filter_seek.phpt
new file mode 100644
index 00000000000..97519ec7853
--- /dev/null
+++ b/ext/iconv/tests/iconv_stream_filter_seek.phpt
@@ -0,0 +1,43 @@
+--TEST--
+iconv stream filter with seek to start
+--EXTENSIONS--
+iconv
+--FILE--
+<?php
+$file = __DIR__ . '/iconv_stream_filter_seek.txt';
+
+$text = 'Hello, this is a test for iconv stream filter seeking functionality.';
+
+$fp = fopen($file, 'w');
+stream_filter_append($fp, 'convert.iconv.ISO-2022-JP/UTF-8');
+fwrite($fp, $text);
+fclose($fp);
+
+$fp = fopen($file, 'r');
+stream_filter_append($fp, 'convert.iconv.UTF-8/ISO-2022-JP');
+
+$partial = fread($fp, 20);
+echo "First read (20 bytes): " . $partial . "\n";
+
+$result = fseek($fp, 0, SEEK_SET);
+echo "Seek to start: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
+
+$full = fread($fp, strlen($text));
+echo "Content after seek matches: " . ($full === $text ? "YES" : "NO") . "\n";
+
+$result = fseek($fp, 50, SEEK_SET);
+echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
+
+fclose($fp);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/iconv_stream_filter_seek.txt');
+?>
+--EXPECTF--
+First read (20 bytes): Hello, this is a tes
+Seek to start: SUCCESS
+Content after seek matches: YES
+
+Warning: fseek(): Stream filter convert.iconv.* is seekable only to start position in %s on line %d
+Seek to middle: FAILURE
diff --git a/ext/standard/filters.c b/ext/standard/filters.c
index 0faa0eda686..c909211788b 100644
--- a/ext/standard/filters.c
+++ b/ext/standard/filters.c
@@ -59,12 +59,13 @@ static php_stream_filter_status_t strfilter_rot13_filter(
 static const php_stream_filter_ops strfilter_rot13_ops = {
 	strfilter_rot13_filter,
 	NULL,
+	NULL,
 	"string.rot13"
 };

-static php_stream_filter *strfilter_rot13_create(const char *filtername, zval *filterparams, uint8_t persistent)
+static php_stream_filter *strfilter_rot13_create(const char *filtername, zval *filterparams, bool persistent)
 {
-	return php_stream_filter_alloc(&strfilter_rot13_ops, NULL, persistent);
+	return php_stream_filter_alloc(&strfilter_rot13_ops, NULL, persistent, PSFS_SEEKABLE_ALWAYS);
 }

 static const php_stream_filter_factory strfilter_rot13_factory = {
@@ -135,23 +136,25 @@ static php_stream_filter_status_t strfilter_tolower_filter(
 static const php_stream_filter_ops strfilter_toupper_ops = {
 	strfilter_toupper_filter,
 	NULL,
+	NULL,
 	"string.toupper"
 };

 static const php_stream_filter_ops strfilter_tolower_ops = {
 	strfilter_tolower_filter,
 	NULL,
+	NULL,
 	"string.tolower"
 };

-static php_stream_filter *strfilter_toupper_create(const char *filtername, zval *filterparams, uint8_t persistent)
+static php_stream_filter *strfilter_toupper_create(const char *filtername, zval *filterparams, bool persistent)
 {
-	return php_stream_filter_alloc(&strfilter_toupper_ops, NULL, persistent);
+	return php_stream_filter_alloc(&strfilter_toupper_ops, NULL, persistent, PSFS_SEEKABLE_ALWAYS);
 }

-static php_stream_filter *strfilter_tolower_create(const char *filtername, zval *filterparams, uint8_t persistent)
+static php_stream_filter *strfilter_tolower_create(const char *filtername, zval *filterparams, bool persistent)
 {
-	return php_stream_filter_alloc(&strfilter_tolower_ops, NULL, persistent);
+	return php_stream_filter_alloc(&strfilter_tolower_ops, NULL, persistent, PSFS_SEEKABLE_ALWAYS);
 }

 static const php_stream_filter_factory strfilter_toupper_factory = {
@@ -181,14 +184,17 @@ typedef struct _php_conv php_conv;

 typedef php_conv_err_t (*php_conv_convert_func)(php_conv *, const char **, size_t *, char **, size_t *);
 typedef void (*php_conv_dtor_func)(php_conv *);
+typedef php_conv_err_t (*php_conv_reset_func)(php_conv *);

 struct _php_conv {
 	php_conv_convert_func convert_op;
 	php_conv_dtor_func dtor;
+	php_conv_reset_func reset_op;
 };

 #define php_conv_convert(a, b, c, d, e) ((php_conv *)(a))->convert_op((php_conv *)(a), (b), (c), (d), (e))
 #define php_conv_dtor(a) ((php_conv *)a)->dtor((a))
+#define php_conv_reset(a) (((php_conv *)(a))->reset_op ? ((php_conv *)(a))->reset_op((php_conv *)(a)) : PHP_CONV_ERR_SUCCESS)

 /* {{{ php_conv_base64_encode */
 typedef struct _php_conv_base64_encode {
@@ -206,6 +212,7 @@ typedef struct _php_conv_base64_encode {

 static php_conv_err_t php_conv_base64_encode_convert(php_conv_base64_encode *inst, const char **in_p, size_t *in_left, char **out_p, size_t *out_left);
 static void php_conv_base64_encode_dtor(php_conv_base64_encode *inst);
+static php_conv_err_t php_conv_base64_encode_reset(php_conv_base64_encode *inst);

 static const unsigned char b64_tbl_enc[256] = {
 	'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
@@ -226,10 +233,19 @@ static const unsigned char b64_tbl_enc[256] = {
 	'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/'
 };

+static php_conv_err_t php_conv_base64_encode_reset(php_conv_base64_encode *inst)
+{
+	/* Reset only mutable state, preserve configuration (lbchars, line_len, etc.) */
+	inst->erem_len = 0;
+	inst->line_ccnt = inst->line_len;
+	return PHP_CONV_ERR_SUCCESS;
+}
+
 static php_conv_err_t php_conv_base64_encode_ctor(php_conv_base64_encode *inst, unsigned int line_len, const char *lbchars, size_t lbchars_len, int lbchars_dup, bool persistent)
 {
 	inst->_super.convert_op = (php_conv_convert_func) php_conv_base64_encode_convert;
 	inst->_super.dtor = (php_conv_dtor_func) php_conv_base64_encode_dtor;
+	inst->_super.reset_op = (php_conv_reset_func) php_conv_base64_encode_reset;
 	inst->erem_len = 0;
 	inst->line_ccnt = line_len;
 	inst->line_len = line_len;
@@ -449,6 +465,7 @@ typedef struct _php_conv_base64_decode {

 static php_conv_err_t php_conv_base64_decode_convert(php_conv_base64_decode *inst, const char **in_p, size_t *in_left, char **out_p, size_t *out_left);
 static void php_conv_base64_decode_dtor(php_conv_base64_decode *inst);
+static php_conv_err_t php_conv_base64_decode_reset(php_conv_base64_decode *inst);

 static unsigned int b64_tbl_dec[256] = {
 	64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
@@ -469,10 +486,21 @@ static unsigned int b64_tbl_dec[256] = {
 	64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64
 };

+static php_conv_err_t php_conv_base64_decode_reset(php_conv_base64_decode *inst)
+{
+	/* Reset only mutable state */
+	inst->urem = 0;
+	inst->urem_nbits = 0;
+	inst->ustat = 0;
+	inst->eos = 0;
+	return PHP_CONV_ERR_SUCCESS;
+}
+
 static void php_conv_base64_decode_ctor(php_conv_base64_decode *inst)
 {
 	inst->_super.convert_op = (php_conv_convert_func) php_conv_base64_decode_convert;
 	inst->_super.dtor = (php_conv_dtor_func) php_conv_base64_decode_dtor;
+	inst->_super.reset_op = (php_conv_reset_func) php_conv_base64_decode_reset;

 	inst->urem = 0;
 	inst->urem_nbits = 0;
@@ -618,6 +646,7 @@ typedef struct _php_conv_qprint_encode {

 static void php_conv_qprint_encode_dtor(php_conv_qprint_encode *inst);
 static php_conv_err_t php_conv_qprint_encode_convert(php_conv_qprint_encode *inst, const char **in_pp, size_t *in_left_p, char **out_pp, size_t *out_left_p);
+static php_conv_err_t php_conv_qprint_encode_reset(php_conv_qprint_encode *inst);

 static void php_conv_qprint_encode_dtor(php_conv_qprint_encode *inst)
 {
@@ -627,6 +656,15 @@ static void php_conv_qprint_encode_dtor(php_conv_qprint_encode *inst)
 	}
 }

+static php_conv_err_t php_conv_qprint_encode_reset(php_conv_qprint_encode *inst)
+{
+	/* Reset only mutable state, preserve configuration */
+	inst->line_ccnt = inst->line_len;
+	inst->lb_ptr = 0;
+	inst->lb_cnt = 0;
+	return PHP_CONV_ERR_SUCCESS;
+}
+
 #define NEXT_CHAR(ps, icnt, lb_ptr, lb_cnt, lbchars) \
 	((lb_ptr) < (lb_cnt) ? (lbchars)[(lb_ptr)] : *(ps))

@@ -832,6 +870,7 @@ static php_conv_err_t php_conv_qprint_encode_ctor(php_conv_qprint_encode *inst,
 	}
 	inst->_super.convert_op = (php_conv_convert_func) php_conv_qprint_encode_convert;
 	inst->_super.dtor = (php_conv_dtor_func) php_conv_qprint_encode_dtor;
+	inst->_super.reset_op = (php_conv_reset_func) php_conv_qprint_encode_reset;
 	inst->line_ccnt = line_len;
 	inst->line_len = line_len;
 	if (lbchars != NULL) {
@@ -862,6 +901,10 @@ typedef struct _php_conv_qprint_decode {
 	unsigned int lb_cnt;
 } php_conv_qprint_decode;

+static void php_conv_qprint_decode_dtor(php_conv_qprint_decode *inst);
+static php_conv_err_t php_conv_qprint_decode_convert(php_conv_qprint_decode *inst, const char **in_pp, size_t *in_left_p, char **out_pp, size_t *out_left_p);
+static php_conv_err_t php_conv_qprint_decode_reset(php_conv_qprint_decode *inst);
+
 static void php_conv_qprint_decode_dtor(php_conv_qprint_decode *inst)
 {
 	assert(inst != NULL);
@@ -870,6 +913,16 @@ static void php_conv_qprint_decode_dtor(php_conv_qprint_decode *inst)
 	}
 }

+static php_conv_err_t php_conv_qprint_decode_reset(php_conv_qprint_decode *inst)
+{
+	/* Reset only mutable state, preserve configuration */
+	inst->scan_stat = 0;
+	inst->next_char = 0;
+	inst->lb_ptr = 0;
+	inst->lb_cnt = 0;
+	return PHP_CONV_ERR_SUCCESS;
+}
+
 static php_conv_err_t php_conv_qprint_decode_convert(php_conv_qprint_decode *inst, const char **in_pp, size_t *in_left_p, char **out_pp, size_t *out_left_p)
 {
 	php_conv_err_t err = PHP_CONV_ERR_SUCCESS;
@@ -1040,10 +1093,12 @@ static php_conv_err_t php_conv_qprint_decode_convert(php_conv_qprint_decode *ins

 	return err;
 }
+
 static php_conv_err_t php_conv_qprint_decode_ctor(php_conv_qprint_decode *inst, const char *lbchars, size_t lbchars_len, int lbchars_dup, bool persistent)
 {
 	inst->_super.convert_op = (php_conv_convert_func) php_conv_qprint_decode_convert;
 	inst->_super.dtor = (php_conv_dtor_func) php_conv_qprint_decode_dtor;
+	inst->_super.reset_op = (php_conv_reset_func) php_conv_qprint_decode_reset;
 	inst->scan_stat = 0;
 	inst->next_char = 0;
 	inst->lb_ptr = inst->lb_cnt = 0;
@@ -1540,6 +1595,29 @@ static php_stream_filter_status_t strfilter_convert_filter(
 	return PSFS_ERR_FATAL;
 }

+static zend_result strfilter_convert_seek(
+	php_stream *stream,
+	php_stream_filter *thisfilter,
+	zend_off_t offset,
+	int whence
+	)
+{
+	php_convert_filter *inst = (php_convert_filter *)Z_PTR(thisfilter->abstract);
+
+	/* Reset stub buffer */
+	inst->stub_len = 0;
+
+	/* Reset the converter state - preserves all configuration/options */
+	if (inst->cd != NULL) {
+		php_conv_err_t err = php_conv_reset(inst->cd);
+		if (err != PHP_CONV_ERR_SUCCESS) {
+			return FAILURE;
+		}
+	}
+
+	return SUCCESS;
+}
+
 static void strfilter_convert_dtor(php_stream_filter *thisfilter)
 {
 	assert(Z_PTR(thisfilter->abstract) != NULL);
@@ -1550,11 +1628,12 @@ static void strfilter_convert_dtor(php_stream_filter *thisfilter)

 static const php_stream_filter_ops strfilter_convert_ops = {
 	strfilter_convert_filter,
+	strfilter_convert_seek,
 	strfilter_convert_dtor,
 	"convert.*"
 };

-static php_stream_filter *strfilter_convert_create(const char *filtername, zval *filterparams, uint8_t persistent)
+static php_stream_filter *strfilter_convert_create(const char *filtername, zval *filterparams, bool persistent)
 {
 	php_convert_filter *inst;

@@ -1590,7 +1669,7 @@ static php_stream_filter *strfilter_convert_create(const char *filtername, zval
 		return NULL;
 	}

-	return php_stream_filter_alloc(&strfilter_convert_ops, inst, persistent);
+	return php_stream_filter_alloc(&strfilter_convert_ops, inst, persistent, PSFS_SEEKABLE_START);
 }

 static const php_stream_filter_factory strfilter_convert_factory = {
@@ -1637,6 +1716,22 @@ static php_stream_filter_status_t consumed_filter_filter(
 	return PSFS_PASS_ON;
 }

+static zend_result consumed_filter_seek(
+	php_stream *stream,
+	php_stream_filter *thisfilter,
+	zend_off_t offset,
+	int whence
+	)
+{
+	php_consumed_filter_data *data = (php_consumed_filter_data *)Z_PTR(thisfilter->abstract);
+
+	/* Reset consumed state */
+	data->consumed = 0;
+	data->offset = ~0;
+
+	return SUCCESS;
+}
+
 static void consumed_filter_dtor(php_stream_filter *thisfilter)
 {
 	if (thisfilter && Z_PTR(thisfilter->abstract)) {
@@ -1647,11 +1742,12 @@ static void consumed_filter_dtor(php_stream_filter *thisfilter)

 static const php_stream_filter_ops consumed_filter_ops = {
 	consumed_filter_filter,
+	consumed_filter_seek,
 	consumed_filter_dtor,
 	"consumed"
 };

-static php_stream_filter *consumed_filter_create(const char *filtername, zval *filterparams, uint8_t persistent)
+static php_stream_filter *consumed_filter_create(const char *filtername, zval *filterparams, bool persistent)
 {
 	const php_stream_filter_ops *fops = NULL;
 	php_consumed_filter_data *data;
@@ -1667,7 +1763,7 @@ static php_stream_filter *consumed_filter_create(const char *filtername, zval *f
 	data->offset = ~0;
 	fops = &consumed_filter_ops;

-	return php_stream_filter_alloc(fops, data, persistent);
+	return php_stream_filter_alloc(fops, data, persistent, PSFS_SEEKABLE_START);
 }

 static const php_stream_filter_factory consumed_filter_factory = {
@@ -1851,6 +1947,22 @@ static php_stream_filter_status_t php_chunked_filter(
 	return PSFS_PASS_ON;
 }

+static zend_result php_chunked_seek(
+	php_stream *stream,
+	php_stream_filter *thisfilter,
+	zend_off_t offset,
+	int whence
+	)
+{
+	php_chunked_filter_data *data = (php_chunked_filter_data *)Z_PTR(thisfilter->abstract);
+
+	/* Reset chunked decoder state */
+	data->state = CHUNK_SIZE_START;
+	data->chunk_size = 0;
+
+	return SUCCESS;
+}
+
 static void php_chunked_dtor(php_stream_filter *thisfilter)
 {
 	if (thisfilter && Z_PTR(thisfilter->abstract)) {
@@ -1861,11 +1973,12 @@ static void php_chunked_dtor(php_stream_filter *thisfilter)

 static const php_stream_filter_ops chunked_filter_ops = {
 	php_chunked_filter,
+	php_chunked_seek,
 	php_chunked_dtor,
 	"dechunk"
 };

-static php_stream_filter *chunked_filter_create(const char *filtername, zval *filterparams, uint8_t persistent)
+static php_stream_filter *chunked_filter_create(const char *filtername, zval *filterparams, bool persistent)
 {
 	const php_stream_filter_ops *fops = NULL;
 	php_chunked_filter_data *data;
@@ -1881,7 +1994,7 @@ static php_stream_filter *chunked_filter_create(const char *filtername, zval *fi
 	data->persistent = persistent;
 	fops = &chunked_filter_ops;

-	return php_stream_filter_alloc(fops, data, persistent);
+	return php_stream_filter_alloc(fops, data, persistent, PSFS_SEEKABLE_START);
 }

 static const php_stream_filter_factory chunked_filter_factory = {
diff --git a/ext/standard/tests/filters/convert_filter_seek.phpt b/ext/standard/tests/filters/convert_filter_seek.phpt
new file mode 100644
index 00000000000..6cbcd7b843f
--- /dev/null
+++ b/ext/standard/tests/filters/convert_filter_seek.phpt
@@ -0,0 +1,75 @@
+--TEST--
+convert filters (base64, quoted-printable) with seek to start only
+--FILE--
+<?php
+$file = __DIR__ . '/convert_filters_seek.txt';
+
+$text = 'Hello World! This is a test for convert filter seeking functionality.';
+
+echo "Testing convert.base64-encode/decode\n";
+$fp = fopen($file, 'w');
+stream_filter_append($fp, 'convert.base64-encode', STREAM_FILTER_WRITE);
+fwrite($fp, $text);
+fclose($fp);
+
+$fp = fopen($file, 'r');
+stream_filter_append($fp, 'convert.base64-decode', STREAM_FILTER_READ);
+
+$partial = fread($fp, 20);
+echo "First read (20 bytes): $partial\n";
+
+$result = fseek($fp, 0, SEEK_SET);
+echo "Seek to start: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
+
+$full = fread($fp, strlen($text));
+echo "Content matches: " . ($full === $text ? "YES" : "NO") . "\n";
+
+$result = fseek($fp, 50, SEEK_SET);
+echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
+
+fclose($fp);
+
+echo "\nTesting convert.quoted-printable-encode/decode\n";
+$text2 = "Line1\r\nLine2\r\nLine3";
+$fp = fopen($file, 'w');
+stream_filter_append($fp, 'convert.quoted-printable-encode', STREAM_FILTER_WRITE);
+fwrite($fp, $text2);
+fclose($fp);
+
+$fp = fopen($file, 'r');
+stream_filter_append($fp, 'convert.quoted-printable-decode', STREAM_FILTER_READ);
+
+$partial = fread($fp, 10);
+echo "First read (10 bytes): " . bin2hex($partial) . "\n";
+
+$result = fseek($fp, 0, SEEK_SET);
+echo "Seek to start: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
+
+$full = fread($fp, strlen($text2));
+echo "Content matches: " . ($full === $text2 ? "YES" : "NO") . "\n";
+
+$result = fseek($fp, 20, SEEK_SET);
+echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
+
+fclose($fp);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/convert_filters_seek.txt');
+?>
+--EXPECTF--
+Testing convert.base64-encode/decode
+First read (20 bytes): Hello World! This is
+Seek to start: SUCCESS
+Content matches: YES
+
+Warning: fseek(): Stream filter convert.* is seekable only to start position in %s on line %d
+Seek to middle: FAILURE
+
+Testing convert.quoted-printable-encode/decode
+First read (10 bytes): 4c696e65310d0a4c696e
+Seek to start: SUCCESS
+Content matches: YES
+
+Warning: fseek(): Stream filter convert.* is seekable only to start position in %s on line %d
+Seek to middle: FAILURE
diff --git a/ext/standard/tests/filters/php_user_filter_04.phpt b/ext/standard/tests/filters/php_user_filter_04.phpt
new file mode 100644
index 00000000000..72f874f21ad
--- /dev/null
+++ b/ext/standard/tests/filters/php_user_filter_04.phpt
@@ -0,0 +1,28 @@
+--TEST--
+php_user_filter with invalid seek signature
+--FILE--
+<?php
+
+class InvalidSeekFilter extends php_user_filter
+{
+    public function filter($in, $out, &$consumed, bool $closing): int
+    {
+        return PSFS_PASS_ON;
+    }
+
+    public function onCreate(): bool
+    {
+        return true;
+    }
+
+    public function onClose(): void {}
+
+    public function seek($offset): bool
+    {
+        return true;
+    }
+}
+
+?>
+--EXPECTF--
+Fatal error: Declaration of InvalidSeekFilter::seek($offset): bool must be compatible with php_user_filter::seek(int $offset, int $whence): bool in %s on line %d
diff --git a/ext/standard/tests/filters/string_filters_seek.phpt b/ext/standard/tests/filters/string_filters_seek.phpt
new file mode 100644
index 00000000000..60eab2ccb84
--- /dev/null
+++ b/ext/standard/tests/filters/string_filters_seek.phpt
@@ -0,0 +1,68 @@
+--TEST--
+string filters (rot13, toupper, tolower) with seek - fully seekable
+--FILE--
+<?php
+$file = __DIR__ . '/string_filters_seek.txt';
+
+$text = 'Hello World! This is a test for string filter seeking.';
+
+$filters = ['string.rot13', 'string.toupper', 'string.tolower'];
+$expected = [
+    'string.rot13' => 'Uryyb Jbeyq! Guvf vf n grfg sbe fgevat svygre frrxvat.',
+    'string.toupper' => 'HELLO WORLD! THIS IS A TEST FOR STRING FILTER SEEKING.',
+    'string.tolower' => 'hello world! this is a test for string filter seeking.'
+];
+
+foreach ($filters as $filter) {
+    echo "Testing filter: $filter\n";
+
+    file_put_contents($file, $text);
+
+    $fp = fopen($file, 'r');
+    stream_filter_append($fp, $filter, STREAM_FILTER_READ);
+
+    $partial = fread($fp, 20);
+    echo "First read (20 bytes): $partial\n";
+
+    $result = fseek($fp, 0, SEEK_SET);
+    echo "Seek to start: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
+
+    $full = fread($fp, strlen($text));
+    echo "Content matches: " . ($full === $expected[$filter] ? "YES" : "NO") . "\n";
+
+    $result = fseek($fp, 13, SEEK_SET);
+    echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
+
+    $from_middle = fread($fp, 10);
+    $expected_middle = substr($expected[$filter], 13, 10);
+    echo "Read from middle matches: " . ($from_middle === $expected_middle ? "YES" : "NO") . "\n";
+
+    fclose($fp);
+    echo "\n";
+}
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/string_filters_seek.txt');
+?>
+--EXPECT--
+Testing filter: string.rot13
+First read (20 bytes): Uryyb Jbeyq! Guvf vf
+Seek to start: SUCCESS
+Content matches: YES
+Seek to middle: SUCCESS
+Read from middle matches: YES
+
+Testing filter: string.toupper
+First read (20 bytes): HELLO WORLD! THIS IS
+Seek to start: SUCCESS
+Content matches: YES
+Seek to middle: SUCCESS
+Read from middle matches: YES
+
+Testing filter: string.tolower
+First read (20 bytes): hello world! this is
+Seek to start: SUCCESS
+Content matches: YES
+Seek to middle: SUCCESS
+Read from middle matches: YES
diff --git a/ext/standard/tests/filters/user_filter_seek_01.phpt b/ext/standard/tests/filters/user_filter_seek_01.phpt
new file mode 100644
index 00000000000..cb4e9fe7226
--- /dev/null
+++ b/ext/standard/tests/filters/user_filter_seek_01.phpt
@@ -0,0 +1,81 @@
+--TEST--
+php_user_filter with seek method - always seekable (stateless filter)
+--FILE--
+<?php
+
+class RotateFilter extends php_user_filter
+{
+    private $rotation = 0;
+
+    public function filter($in, $out, &$consumed, bool $closing): int
+    {
+        while ($bucket = stream_bucket_make_writeable($in)) {
+            $rotated = '';
+            for ($i = 0; $i < strlen($bucket->data); $i++) {
+                $char = $bucket->data[$i];
+                if (ctype_alpha($char)) {
+                    $base = ctype_upper($char) ? ord('A') : ord('a');
+                    $rotated .= chr($base + (ord($char) - $base + $this->rotation) % 26);
+                } else {
+                    $rotated .= $char;
+                }
+            }
+            $bucket->data = $rotated;
+            $consumed += $bucket->datalen;
+            stream_bucket_append($out, $bucket);
+        }
+        return PSFS_PASS_ON;
+    }
+
+    public function onCreate(): bool
+    {
+        if (isset($this->params['rotation'])) {
+            $this->rotation = (int)$this->params['rotation'];
+        }
+        return true;
+    }
+
+    public function onClose(): void {}
+
+    public function seek(int $offset, int $whence): bool
+    {
+        // Stateless filter - always seekable to any position
+        return true;
+    }
+}
+
+stream_filter_register('test.rotate', 'RotateFilter');
+
+$file = __DIR__ . '/user_filter_seek_001.txt';
+$text = 'Hello World';
+
+file_put_contents($file, $text);
+
+$fp = fopen($file, 'r');
+stream_filter_append($fp, 'test.rotate', STREAM_FILTER_READ, ['rotation' => 13]);
+
+$partial = fread($fp, 5);
+echo "First read: $partial\n";
+
+$result = fseek($fp, 0, SEEK_SET);
+echo "Seek to start: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
+
+$full = fread($fp, strlen($text));
+echo "Full content: $full\n";
+
+$result = fseek($fp, 6, SEEK_SET);
+echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
+
+$from_middle = fread($fp, 5);
+echo "Read from middle: $from_middle\n";
+
+fclose($fp);
+unlink($file);
+
+?>
+--EXPECT--
+First read: Uryyb
+Seek to start: SUCCESS
+Full content: Uryyb Jbeyq
+Seek to middle: SUCCESS
+Read from middle: Jbeyq
diff --git a/ext/standard/tests/filters/user_filter_seek_02.phpt b/ext/standard/tests/filters/user_filter_seek_02.phpt
new file mode 100644
index 00000000000..39f4c3c6624
--- /dev/null
+++ b/ext/standard/tests/filters/user_filter_seek_02.phpt
@@ -0,0 +1,75 @@
+--TEST--
+php_user_filter with seek method - stateful filter
+--FILE--
+<?php
+
+class CountingFilter extends php_user_filter
+{
+    private $count = 0;
+
+    public function filter($in, $out, &$consumed, bool $closing): int
+    {
+        while ($bucket = stream_bucket_make_writeable($in)) {
+            $modified = '';
+            for ($i = 0; $i < strlen($bucket->data); $i++) {
+                $modified .= $bucket->data[$i] . $this->count++;
+            }
+            $bucket->data = $modified;
+            $consumed += $bucket->datalen;
+            stream_bucket_append($out, $bucket);
+        }
+        return PSFS_PASS_ON;
+    }
+
+    public function onCreate(): bool
+    {
+        return true;
+    }
+
+    public function onClose(): void {}
+
+    public function seek(int $offset, int $whence): bool
+    {
+        if ($offset === 0 && $whence === SEEK_SET) {
+            $this->count = 0;
+            return true;
+        }
+        return false;
+    }
+}
+
+stream_filter_register('test.counting', 'CountingFilter');
+
+$file = __DIR__ . '/user_filter_seek_002.txt';
+$text = 'ABC';
+
+file_put_contents($file, $text);
+
+$fp = fopen($file, 'r');
+stream_filter_append($fp, 'test.counting', STREAM_FILTER_READ);
+
+$first = fread($fp, 10);
+echo "First read: $first\n";
+
+$result = fseek($fp, 0, SEEK_SET);
+echo "Seek to start: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
+
+$second = fread($fp, 10);
+echo "Second read after seek: $second\n";
+echo "Counts reset: " . ($first === $second ? "YES" : "NO") . "\n";
+
+$result = fseek($fp, 1, SEEK_SET);
+echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
+
+fclose($fp);
+unlink($file);
+
+?>
+--EXPECTF--
+First read: A0B1C2
+Seek to start: SUCCESS
+Second read after seek: A0B1C2
+Counts reset: YES
+
+Warning: fseek(): Stream filter seeking for user-filter failed in %s on line %d
+Seek to middle: FAILURE
diff --git a/ext/standard/user_filters.c b/ext/standard/user_filters.c
index 610784e547e..ac21e083efb 100644
--- a/ext/standard/user_filters.c
+++ b/ext/standard/user_filters.c
@@ -48,6 +48,16 @@ PHP_METHOD(php_user_filter, filter)
 	RETURN_LONG(PSFS_ERR_FATAL);
 }

+PHP_METHOD(php_user_filter, seek)
+{
+	zend_long offset, whence;
+	if (zend_parse_parameters(ZEND_NUM_ARGS(), "ll", &offset, &whence) == FAILURE) {
+		RETURN_THROWS();
+	}
+
+	RETURN_TRUE;
+}
+
 PHP_METHOD(php_user_filter, onCreate)
 {
 	ZEND_PARSE_PARAMETERS_NONE();
@@ -123,6 +133,36 @@ static void userfilter_dtor(php_stream_filter *thisfilter)
 	zval_ptr_dtor(obj);
 }

+static zend_result userfilter_assign_stream(php_stream *stream, zval *obj,
+		zend_string **stream_name_p, uint32_t orig_no_fclose)
+{
+	/* Give the userfilter class a hook back to the stream */
+	const zend_class_entry *old_scope = EG(fake_scope);
+	EG(fake_scope) = Z_OBJCE_P(obj);
+
+	zend_string *stream_name = ZSTR_INIT_LITERAL("stream", false);
+	bool stream_property_exists = Z_OBJ_HT_P(obj)->has_property(Z_OBJ_P(obj), stream_name, ZEND_PROPERTY_EXISTS, NULL);
+	if (stream_property_exists) {
+		zval stream_zval;
+		php_stream_to_zval(stream, &stream_zval);
+		zend_update_property_ex(Z_OBJCE_P(obj), Z_OBJ_P(obj), stream_name, &stream_zval);
+		/* If property update threw an exception, skip filter execution */
+		if (EG(exception)) {
+			EG(fake_scope) = old_scope;
+			zend_string_release(stream_name);
+			stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
+			stream->flags |= orig_no_fclose;
+			return FAILURE;
+		}
+		*stream_name_p = stream_name;
+	} else {
+		zend_string_release(stream_name);
+	}
+	EG(fake_scope) = old_scope;
+
+	return SUCCESS;
+}
+
 static php_stream_filter_status_t userfilter_filter(
 			php_stream *stream,
 			php_stream_filter *thisfilter,
@@ -148,31 +188,14 @@ static php_stream_filter_status_t userfilter_filter(
 	uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE;
 	stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE;

-	/* Give the userfilter class a hook back to the stream */
-	const zend_class_entry *old_scope = EG(fake_scope);
-	EG(fake_scope) = Z_OBJCE_P(obj);
-
-	zend_string *stream_name = ZSTR_INIT_LITERAL("stream", 0);
-	bool stream_property_exists = Z_OBJ_HT_P(obj)->has_property(Z_OBJ_P(obj), stream_name, ZEND_PROPERTY_EXISTS, NULL);
-	if (stream_property_exists) {
-		zval stream_zval;
-		php_stream_to_zval(stream, &stream_zval);
-		zend_update_property_ex(Z_OBJCE_P(obj), Z_OBJ_P(obj), stream_name, &stream_zval);
-		/* If property update threw an exception, skip filter execution */
-		if (EG(exception)) {
-			EG(fake_scope) = old_scope;
-			if (buckets_in->head) {
-				php_error_docref(NULL, E_WARNING, "Unprocessed filter buckets remaining on input brigade");
-			}
-			zend_string_release(stream_name);
-			stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
-			stream->flags |= orig_no_fclose;
-			return PSFS_ERR_FATAL;
+	zend_string *stream_name = NULL;
+	if (userfilter_assign_stream(stream, obj, &stream_name, orig_no_fclose) == FAILURE) {
+		if (buckets_in->head) {
+			php_error_docref(NULL, E_WARNING, "Unprocessed filter buckets remaining on input brigade");
 		}
+		return PSFS_ERR_FATAL;
 	}

-	EG(fake_scope) = old_scope;
-
 	ZVAL_STRINGL(&func_name, "filter", sizeof("filter")-1);

 	/* Setup calling arguments */
@@ -217,12 +240,11 @@ static php_stream_filter_status_t userfilter_filter(
 	 * Since the property accepted a resource assignment above, it must have
 	 * no type hint or be typed as mixed, so we can safely assign null.
 	 */
-	if (stream_property_exists) {
+	if (stream_name != NULL) {
 		zend_update_property_null(Z_OBJCE_P(obj), Z_OBJ_P(obj), ZSTR_VAL(stream_name), ZSTR_LEN(stream_name));
+		zend_string_release(stream_name);
 	}

-	zend_string_release(stream_name);
-
 	zval_ptr_dtor(&args[3]);
 	zval_ptr_dtor(&args[2]);
 	zval_ptr_dtor(&args[1]);
@@ -234,14 +256,73 @@ static php_stream_filter_status_t userfilter_filter(
 	return ret;
 }

+static zend_result userfilter_seek(
+			php_stream *stream,
+			php_stream_filter *thisfilter,
+			zend_off_t offset,
+			int whence
+			)
+{
+	zval *obj = &thisfilter->abstract;
+	zval retval;
+	zval args[2];
+
+	/* the userfilter object probably doesn't exist anymore */
+	if (CG(unclean_shutdown)) {
+		return FAILURE;
+	}
+
+	/* Check if the seek method exists */
+	zend_function *seek_method = zend_hash_str_find_ptr(&Z_OBJCE_P(obj)->function_table, ZEND_STRL("seek"));
+	if (seek_method == NULL) {
+		/* Method doesn't exist - consider this a successful seek for BC */
+		return SUCCESS;
+	}
+
+	/* Make sure the stream is not closed while the filter callback executes. */
+	uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE;
+	stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE;
+
+	zend_string *stream_name = NULL;
+	if (userfilter_assign_stream(stream, obj, &stream_name, orig_no_fclose) == FAILURE) {
+		return FAILURE;
+	}
+
+	/* Setup calling arguments */
+	ZVAL_LONG(&args[0], offset);
+	ZVAL_LONG(&args[1], whence);
+
+	zend_call_known_function(seek_method, Z_OBJ_P(obj), Z_OBJCE_P(obj), &retval, 2, args, NULL);
+
+	zend_result ret = FAILURE;
+	if (Z_TYPE(retval) != IS_UNDEF) {
+		ret = zend_is_true(&retval) ? SUCCESS : FAILURE;
+		zval_ptr_dtor(&retval);
+	}
+
+	/* filter resources are cleaned up by the stream destructor,
+	 * keeping a reference to the stream resource here would prevent it
+	 * from being destroyed properly */
+	if (stream_name != NULL) {
+		zend_update_property_null(Z_OBJCE_P(obj), Z_OBJ_P(obj), ZSTR_VAL(stream_name), ZSTR_LEN(stream_name));
+		zend_string_release(stream_name);
+	}
+
+	stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
+	stream->flags |= orig_no_fclose;
+
+	return ret;
+}
+
 static const php_stream_filter_ops userfilter_ops = {
 	userfilter_filter,
+	userfilter_seek,
 	userfilter_dtor,
 	"user-filter"
 };

 static php_stream_filter *user_filter_factory_create(const char *filtername,
-		zval *filterparams, uint8_t persistent)
+		zval *filterparams, bool persistent)
 {
 	struct php_user_filter_data *fdat = NULL;
 	php_stream_filter *filter;
@@ -304,7 +385,7 @@ static php_stream_filter *user_filter_factory_create(const char *filtername,
 		return NULL;
 	}

-	filter = php_stream_filter_alloc(&userfilter_ops, NULL, 0);
+	filter = php_stream_filter_alloc(&userfilter_ops, NULL, false, PSFS_SEEKABLE_CHECK);

 	/* filtername */
 	add_property_string(&obj, "filtername", filtername);
diff --git a/ext/standard/user_filters.stub.php b/ext/standard/user_filters.stub.php
index acaa42bb33c..475ec58e79e 100644
--- a/ext/standard/user_filters.stub.php
+++ b/ext/standard/user_filters.stub.php
@@ -49,6 +49,9 @@ class php_user_filter
      */
     public function filter($in, $out, &$consumed, bool $closing): int {}

+    /** @tentative-return-type */
+    public function seek(int $offset, int $whence): bool {}
+
     /** @tentative-return-type */
     public function onCreate(): bool {}

diff --git a/ext/standard/user_filters_arginfo.h b/ext/standard/user_filters_arginfo.h
index 1d3c7bb5358..d530a5c0006 100644
Binary files a/ext/standard/user_filters_arginfo.h and b/ext/standard/user_filters_arginfo.h differ
diff --git a/ext/zlib/tests/zlib_filter_seek_deflate.phpt b/ext/zlib/tests/zlib_filter_seek_deflate.phpt
new file mode 100644
index 00000000000..6acee8e4e8c
--- /dev/null
+++ b/ext/zlib/tests/zlib_filter_seek_deflate.phpt
@@ -0,0 +1,55 @@
+--TEST--
+zlib.deflate filter with seek to start
+--EXTENSIONS--
+zlib
+--FILE--
+<?php
+$file = __DIR__ . '/zlib_filter_seek_deflate.zlib';
+
+$text1 = 'Short text.';
+$text2 = 'This is a much longer text that will completely overwrite the previous compressed data in the file.';
+
+$fp = fopen($file, 'w+');
+stream_filter_append($fp, 'zlib.deflate', STREAM_FILTER_WRITE);
+
+fwrite($fp, $text1);
+fflush($fp);
+
+$size1 = ftell($fp);
+echo "Size after first write: $size1\n";
+
+$result = fseek($fp, 0, SEEK_SET);
+echo "Seek to start: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
+
+fwrite($fp, $text2);
+fflush($fp);
+
+$size2 = ftell($fp);
+echo "Size after second write: $size2\n";
+echo "Second write is larger: " . ($size2 > $size1 ? "YES" : "NO") . "\n";
+
+$result = fseek($fp, 50, SEEK_SET);
+echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
+
+fclose($fp);
+
+$fp = fopen($file, 'r');
+stream_filter_append($fp, 'zlib.inflate', STREAM_FILTER_READ);
+$content = stream_get_contents($fp);
+fclose($fp);
+
+echo "Decompressed content matches text2: " . ($content === $text2 ? "YES" : "NO") . "\n";
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/zlib_filter_seek_deflate.zlib');
+?>
+--EXPECTF--
+Size after first write: %d
+Seek to start: SUCCESS
+Size after second write: %d
+Second write is larger: YES
+
+Warning: fseek(): Stream filter zlib.deflate is seekable only to start position in %s on line %d
+Seek to middle: FAILURE
+Decompressed content matches text2: YES
diff --git a/ext/zlib/tests/zlib_filter_seek_inflate.phpt b/ext/zlib/tests/zlib_filter_seek_inflate.phpt
new file mode 100644
index 00000000000..2ca7508941c
--- /dev/null
+++ b/ext/zlib/tests/zlib_filter_seek_inflate.phpt
@@ -0,0 +1,45 @@
+--TEST--
+zlib.inflate filter with seek to start
+--EXTENSIONS--
+zlib
+--FILE--
+<?php
+$file = __DIR__ . '/zlib_filter_seek_inflate.zlib';
+
+$text = 'I am the very model of a modern major general, I\'ve information vegetable, animal, and mineral.';
+
+$fp = fopen($file, 'w');
+stream_filter_append($fp, 'zlib.deflate', STREAM_FILTER_WRITE);
+fwrite($fp, $text);
+fclose($fp);
+
+$fp = fopen($file, 'r');
+stream_filter_append($fp, 'zlib.inflate', STREAM_FILTER_READ);
+
+$partial = fread($fp, 20);
+echo "First read (20 bytes): " . $partial . "\n";
+
+$result = fseek($fp, 0, SEEK_SET);
+echo "Seek to start: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
+echo "Position after seek: " . ftell($fp) . "\n";
+
+$full = stream_get_contents($fp);
+echo "Content after seek matches: " . ($full === $text ? "YES" : "NO") . "\n";
+
+$result = fseek($fp, 50, SEEK_SET);
+echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
+
+fclose($fp);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/zlib_filter_seek_inflate.zlib');
+?>
+--EXPECTF--
+First read (20 bytes): I am the very model
+Seek to start: SUCCESS
+Position after seek: 0
+Content after seek matches: YES
+
+Warning: fseek(): Stream filter zlib.inflate is seekable only to start position in %s on line %d
+Seek to middle: FAILURE
diff --git a/ext/zlib/zlib_filter.c b/ext/zlib/zlib_filter.c
index e42132fd000..69c9a85465e 100644
--- a/ext/zlib/zlib_filter.c
+++ b/ext/zlib/zlib_filter.c
@@ -28,6 +28,7 @@ typedef struct _php_zlib_filter_data {
 	size_t outbuf_len;
 	int persistent;
 	bool finished; /* for zlib.deflate: signals that no flush is pending */
+	int windowBits;
 } php_zlib_filter_data;

 /* }}} */
@@ -143,6 +144,47 @@ static php_stream_filter_status_t php_zlib_inflate_filter(
 	return exit_status;
 }

+static zend_result php_zlib_inflate_seek(
+	php_stream *stream,
+	php_stream_filter *thisfilter,
+	zend_off_t offset,
+	int whence
+	)
+{
+	int status;
+
+	if (!thisfilter || !Z_PTR(thisfilter->abstract)) {
+		return FAILURE;
+	}
+
+	php_zlib_filter_data *data = Z_PTR(thisfilter->abstract);
+
+	if (data->finished) {
+		/* Stream was ended, need to reinitialize */
+		status = inflateInit2(&(data->strm), data->windowBits);
+		if (status != Z_OK) {
+			php_error_docref(NULL, E_WARNING, "zlib.inflate: failed to reinitialize inflation state");
+			return FAILURE;
+		}
+	} else {
+		/* Stream is active, just reset it */
+		status = inflateReset(&(data->strm));
+		if (status != Z_OK) {
+			php_error_docref(NULL, E_WARNING, "zlib.inflate: failed to reset inflation state");
+			return FAILURE;
+		}
+	}
+
+	/* Reset our own state */
+	data->strm.next_in = data->inbuf;
+	data->strm.avail_in = 0;
+	data->strm.next_out = data->outbuf;
+	data->strm.avail_out = data->outbuf_len;
+	data->finished = false;
+
+	return SUCCESS;
+}
+
 static void php_zlib_inflate_dtor(php_stream_filter *thisfilter)
 {
 	if (thisfilter && Z_PTR(thisfilter->abstract)) {
@@ -158,6 +200,7 @@ static void php_zlib_inflate_dtor(php_stream_filter *thisfilter)

 static const php_stream_filter_ops php_zlib_inflate_ops = {
 	php_zlib_inflate_filter,
+	php_zlib_inflate_seek,
 	php_zlib_inflate_dtor,
 	"zlib.inflate"
 };
@@ -259,6 +302,38 @@ static php_stream_filter_status_t php_zlib_deflate_filter(
 	return exit_status;
 }

+static zend_result php_zlib_deflate_seek(
+	php_stream *stream,
+	php_stream_filter *thisfilter,
+	zend_off_t offset,
+	int whence
+	)
+{
+	int status;
+
+	if (!thisfilter || !Z_PTR(thisfilter->abstract)) {
+		return FAILURE;
+	}
+
+	php_zlib_filter_data *data = Z_PTR(thisfilter->abstract);
+
+	/* Reset zlib deflation state */
+	status = deflateReset(&(data->strm));
+	if (status != Z_OK) {
+		php_error_docref(NULL, E_WARNING, "zlib.deflate: failed to reset deflation state");
+		return FAILURE;
+	}
+
+	/* Reset our own state */
+	data->strm.next_in = data->inbuf;
+	data->strm.avail_in = 0;
+	data->strm.next_out = data->outbuf;
+	data->strm.avail_out = data->outbuf_len;
+	data->finished = true;
+
+	return SUCCESS;
+}
+
 static void php_zlib_deflate_dtor(php_stream_filter *thisfilter)
 {
 	if (thisfilter && Z_PTR(thisfilter->abstract)) {
@@ -272,6 +347,7 @@ static void php_zlib_deflate_dtor(php_stream_filter *thisfilter)

 static const php_stream_filter_ops php_zlib_deflate_ops = {
 	php_zlib_deflate_filter,
+	php_zlib_deflate_seek,
 	php_zlib_deflate_dtor,
 	"zlib.deflate"
 };
@@ -280,7 +356,7 @@ static const php_stream_filter_ops php_zlib_deflate_ops = {

 /* {{{ zlib.* common factory */

-static php_stream_filter *php_zlib_filter_create(const char *filtername, zval *filterparams, uint8_t persistent)
+static php_stream_filter *php_zlib_filter_create(const char *filtername, zval *filterparams, bool persistent)
 {
 	const php_stream_filter_ops *fops = NULL;
 	php_zlib_filter_data *data;
@@ -315,6 +391,7 @@ static php_stream_filter *php_zlib_filter_create(const char *filtername, zval *f
 	}

 	data->strm.data_type = Z_ASCII;
+	data->persistent = persistent;

 	if (strcasecmp(filtername, "zlib.inflate") == 0) {
 		int windowBits = -MAX_WBITS;
@@ -334,6 +411,9 @@ static php_stream_filter *php_zlib_filter_create(const char *filtername, zval *f
 			}
 		}

+		/* Save configuration for reset */
+		data->windowBits = windowBits;
+
 		/* RFC 1951 Inflate */
 		data->finished = false;
 		status = inflateInit2(&(data->strm), windowBits);
@@ -401,6 +481,10 @@ static php_stream_filter *php_zlib_filter_create(const char *filtername, zval *f
 					php_error_docref(NULL, E_WARNING, "Invalid filter parameter, ignored");
 			}
 		}
+
+		/* Save configuration for reset */
+		data->windowBits = windowBits;
+
 		status = deflateInit2(&(data->strm), level, Z_DEFLATED, windowBits, memLevel, 0);
 		data->finished = true;
 		fops = &php_zlib_deflate_ops;
@@ -416,7 +500,7 @@ static php_stream_filter *php_zlib_filter_create(const char *filtername, zval *f
 		return NULL;
 	}

-	return php_stream_filter_alloc(fops, data, persistent);
+	return php_stream_filter_alloc(fops, data, persistent, PSFS_SEEKABLE_START);
 }

 const php_stream_filter_factory php_zlib_filter_factory = {
diff --git a/main/streams/filter.c b/main/streams/filter.c
index d0c1fdc8e78..2260bae8233 100644
--- a/main/streams/filter.c
+++ b/main/streams/filter.c
@@ -216,7 +216,7 @@ PHPAPI void php_stream_bucket_unlink(php_stream_bucket *bucket)
  * match. If that fails, we try "convert.charset.*", then "convert.*"
  * This means that we don't need to clog up the hashtable with a zillion
  * charsets (for example) but still be able to provide them all as filters */
-PHPAPI php_stream_filter *php_stream_filter_create(const char *filtername, zval *filterparams, uint8_t persistent)
+PHPAPI php_stream_filter *php_stream_filter_create(const char *filtername, zval *filterparams, bool persistent)
 {
 	HashTable *filter_hash = (FG(stream_filters) ? FG(stream_filters) : &stream_filters_hash);
 	const php_stream_filter_factory *factory = NULL;
@@ -260,7 +260,8 @@ PHPAPI php_stream_filter *php_stream_filter_create(const char *filtername, zval
 	return filter;
 }

-PHPAPI php_stream_filter *_php_stream_filter_alloc(const php_stream_filter_ops *fops, void *abstract, uint8_t persistent STREAMS_DC)
+PHPAPI php_stream_filter *_php_stream_filter_alloc(const php_stream_filter_ops *fops,
+		void *abstract, bool persistent, php_stream_filter_seekable_t seekable STREAMS_DC)
 {
 	php_stream_filter *filter;

@@ -268,6 +269,7 @@ PHPAPI php_stream_filter *_php_stream_filter_alloc(const php_stream_filter_ops *
 	memset(filter, 0, sizeof(php_stream_filter));

 	filter->fops = fops;
+	filter->seekable = seekable;
 	Z_PTR(filter->abstract) = abstract;
 	filter->is_persistent = persistent;

diff --git a/main/streams/php_stream_filter_api.h b/main/streams/php_stream_filter_api.h
index e224b85b2d9..cb4fdb5fd7f 100644
--- a/main/streams/php_stream_filter_api.h
+++ b/main/streams/php_stream_filter_api.h
@@ -61,6 +61,13 @@ typedef enum {
 	PSFS_PASS_ON	/* filter generated output buckets; pass them on to next in chain */
 } php_stream_filter_status_t;

+typedef enum {
+	PSFS_SEEKABLE_NEVER, /* seeking filter never possible */
+	PSFS_SEEKABLE_START, /* seeking possible only to start (position 0) */
+	PSFS_SEEKABLE_CHECK, /* seeking possible but it is always checked if callback function set */
+	PSFS_SEEKABLE_ALWAYS, /* seeking is always possible */
+} php_stream_filter_seekable_t;
+
 /* Buckets API. */
 BEGIN_EXTERN_C()
 PHPAPI php_stream_bucket *php_stream_bucket_new(const php_stream *stream, char *buf, size_t buflen, uint8_t own_buf, uint8_t buf_persistent);
@@ -88,6 +95,13 @@ typedef struct _php_stream_filter_ops {
 			int flags
 			);

+	zend_result (*seek)(
+			php_stream *stream,
+			php_stream_filter *thisfilter,
+			zend_off_t offset,
+			int whence
+	);
+
 	void (*dtor)(php_stream_filter *thisfilter);

 	const char *label;
@@ -106,7 +120,8 @@ struct _php_stream_filter {
 	zval abstract; /* for use by filter implementation */
 	php_stream_filter *next;
 	php_stream_filter *prev;
-	int is_persistent;
+	php_stream_filter_seekable_t seekable;
+	bool is_persistent;

 	/* link into stream and chain */
 	php_stream_filter_chain *chain;
@@ -127,10 +142,14 @@ PHPAPI zend_result php_stream_filter_append_ex(php_stream_filter_chain *chain, p
 PHPAPI zend_result _php_stream_filter_flush(php_stream_filter *filter, bool finish);
 PHPAPI php_stream_filter *php_stream_filter_remove(php_stream_filter *filter, bool call_dtor);
 PHPAPI void php_stream_filter_free(php_stream_filter *filter);
-PHPAPI php_stream_filter *_php_stream_filter_alloc(const php_stream_filter_ops *fops, void *abstract, uint8_t persistent STREAMS_DC);
+PHPAPI php_stream_filter *_php_stream_filter_alloc(const php_stream_filter_ops *fops,
+		void *abstract, bool persistent, php_stream_filter_seekable_t seekable STREAMS_DC);
+
 END_EXTERN_C()
-#define php_stream_filter_alloc(fops, thisptr, persistent) _php_stream_filter_alloc((fops), (thisptr), (persistent) STREAMS_CC)
-#define php_stream_filter_alloc_rel(fops, thisptr, persistent) _php_stream_filter_alloc((fops), (thisptr), (persistent) STREAMS_REL_CC)
+#define php_stream_filter_alloc(fops, thisptr, persistent, seekable) \
+		_php_stream_filter_alloc((fops), (thisptr), (persistent), (seekable) STREAMS_CC)
+#define php_stream_filter_alloc_rel(fops, thisptr, persistent, seekable) \
+	_php_stream_filter_alloc((fops), (thisptr), (persistent), (seekable) STREAMS_REL_CC)
 #define php_stream_filter_prepend(chain, filter) _php_stream_filter_prepend((chain), (filter))
 #define php_stream_filter_append(chain, filter) _php_stream_filter_append((chain), (filter))
 #define php_stream_filter_flush(filter, finish) _php_stream_filter_flush((filter), (finish))
@@ -138,12 +157,12 @@ END_EXTERN_C()
 #define php_stream_is_filtered(stream)	((stream)->readfilters.head || (stream)->writefilters.head)

 typedef struct _php_stream_filter_factory {
-	php_stream_filter *(*create_filter)(const char *filtername, zval *filterparams, uint8_t persistent);
+	php_stream_filter *(*create_filter)(const char *filtername, zval *filterparams, bool persistent);
 } php_stream_filter_factory;

 BEGIN_EXTERN_C()
 PHPAPI zend_result php_stream_filter_register_factory(const char *filterpattern, const php_stream_filter_factory *factory);
 PHPAPI zend_result php_stream_filter_unregister_factory(const char *filterpattern);
 PHPAPI zend_result php_stream_filter_register_factory_volatile(zend_string *filterpattern, const php_stream_filter_factory *factory);
-PHPAPI php_stream_filter *php_stream_filter_create(const char *filtername, zval *filterparams, uint8_t persistent);
+PHPAPI php_stream_filter *php_stream_filter_create(const char *filtername, zval *filterparams, bool persistent);
 END_EXTERN_C()
diff --git a/main/streams/streams.c b/main/streams/streams.c
index 32c7ba99f58..26f147632ce 100644
--- a/main/streams/streams.c
+++ b/main/streams/streams.c
@@ -1362,6 +1362,52 @@ PHPAPI zend_off_t _php_stream_tell(const php_stream *stream)
 	return stream->position;
 }

+static bool php_stream_are_filters_seekable(php_stream_filter *filter, bool is_start_seeking)
+{
+	while (filter) {
+		if (filter->seekable == PSFS_SEEKABLE_NEVER) {
+			php_error_docref(NULL, E_WARNING, "Stream filter %s is never seekable", filter->fops->label);
+			return false;
+		}
+		if (!is_start_seeking && filter->seekable == PSFS_SEEKABLE_START) {
+			php_error_docref(NULL, E_WARNING, "Stream filter %s is seekable only to start position", filter->fops->label);
+			return false;
+		}
+		filter = filter->next;
+	}
+	return true;
+}
+
+static zend_result php_stream_filters_seek(php_stream *stream, php_stream_filter *filter,
+		bool is_start_seeking, zend_off_t offset, int whence)
+{
+	while (filter) {
+		if (((filter->seekable == PSFS_SEEKABLE_START && is_start_seeking) ||
+				filter->seekable == PSFS_SEEKABLE_CHECK) &&
+				filter->fops->seek(stream, filter, offset, whence) == FAILURE) {
+			php_error_docref(NULL, E_WARNING, "Stream filter seeking for %s failed", filter->fops->label);
+			return FAILURE;
+		}
+		filter = filter->next;
+	}
+	return SUCCESS;
+}
+
+static zend_result php_stream_filters_seek_all(php_stream *stream, bool is_start_seeking,
+		zend_off_t offset, int whence)
+{
+	if (php_stream_filters_seek(stream, stream->writefilters.head, is_start_seeking, offset, whence) == FAILURE) {
+		return FAILURE;
+	}
+	if (php_stream_filters_seek(stream, stream->readfilters.head, is_start_seeking, offset, whence) == FAILURE) {
+		return FAILURE;
+	}
+
+	return SUCCESS;
+}
+
+
+
 PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence)
 {
 	if (stream->fclose_stdiocast == PHP_STREAM_FCLOSE_FOPENCOOKIE) {
@@ -1374,6 +1420,18 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence)
 		}
 	}

+	bool is_start_seeking = whence == SEEK_SET && offset == 0;
+
+	if (stream->writefilters.head) {
+		_php_stream_flush(stream, 0);
+		if (!php_stream_are_filters_seekable(stream->writefilters.head, is_start_seeking)) {
+			return -1;
+		}
+	}
+	if (stream->readfilters.head && !php_stream_are_filters_seekable(stream->readfilters.head, is_start_seeking)) {
+		return -1;
+	}
+
 	/* handle the case where we are in the buffer */
 	if ((stream->flags & PHP_STREAM_FLAG_NO_BUFFER) == 0) {
 		switch(whence) {
@@ -1383,7 +1441,7 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence)
 					stream->position += offset;
 					stream->eof = 0;
 					stream->fatal_error = 0;
-					return 0;
+					return php_stream_filters_seek_all(stream, is_start_seeking, offset, whence) == SUCCESS ? 0 : -1;
 				}
 				break;
 			case SEEK_SET:
@@ -1393,7 +1451,7 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence)
 					stream->position = offset;
 					stream->eof = 0;
 					stream->fatal_error = 0;
-					return 0;
+					return php_stream_filters_seek_all(stream, is_start_seeking, offset, whence) == SUCCESS ? 0 : -1;
 				}
 				break;
 		}
@@ -1402,11 +1460,6 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence)

 	if (stream->ops->seek && (stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0) {
 		int ret;
-
-		if (stream->writefilters.head) {
-			_php_stream_flush(stream, 0);
-		}
-
 		switch(whence) {
 			case SEEK_CUR:
 				ZEND_ASSERT(stream->position >= 0);
@@ -1433,7 +1486,7 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence)
 			/* invalidate the buffer contents */
 			stream->readpos = stream->writepos = 0;

-			return ret;
+			return php_stream_filters_seek_all(stream, is_start_seeking, offset, whence) == SUCCESS ? ret : -1;
 		}
 		/* else the stream has decided that it can't support seeking after all;
 		 * fall through to attempt emulation */