From 358d9c54a66de3ed03708abbdb2caa7e75e8c607 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 30 Mar 2026 10:27:34 +0200 Subject: [PATCH 1/3] 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. --- NEWS | 2 + UPGRADING.INTERNALS | 1 + ext/bz2/bz2_filter.c | 79 +++++++++- ext/bz2/tests/bz2_filter_seek_compress.phpt | 55 +++++++ ext/bz2/tests/bz2_filter_seek_decompress.phpt | 43 ++++++ ext/gd/tests/bug79945.phpt | 8 +- ext/iconv/iconv.c | 33 ++++- ext/iconv/tests/iconv_stream_filter_seek.phpt | 43 ++++++ ext/standard/filters.c | 137 ++++++++++++++++-- .../tests/filters/convert_filter_seek.phpt | 75 ++++++++++ .../tests/filters/php_user_filter_04.phpt | 28 ++++ .../tests/filters/string_filters_seek.phpt | 68 +++++++++ .../tests/filters/user_filter_seek_01.phpt | 81 +++++++++++ .../tests/filters/user_filter_seek_02.phpt | 75 ++++++++++ ext/standard/user_filters.c | 135 +++++++++++++---- ext/standard/user_filters.stub.php | 3 + ext/standard/user_filters_arginfo.h | 9 +- ext/zlib/tests/zlib_filter_seek_deflate.phpt | 55 +++++++ ext/zlib/tests/zlib_filter_seek_inflate.phpt | 45 ++++++ ext/zlib/zlib_filter.c | 88 ++++++++++- main/streams/filter.c | 6 +- main/streams/php_stream_filter_api.h | 31 +++- main/streams/streams.c | 69 ++++++++- 23 files changed, 1103 insertions(+), 66 deletions(-) create mode 100644 ext/bz2/tests/bz2_filter_seek_compress.phpt create mode 100644 ext/bz2/tests/bz2_filter_seek_decompress.phpt create mode 100644 ext/iconv/tests/iconv_stream_filter_seek.phpt create mode 100644 ext/standard/tests/filters/convert_filter_seek.phpt create mode 100644 ext/standard/tests/filters/php_user_filter_04.phpt create mode 100644 ext/standard/tests/filters/string_filters_seek.phpt create mode 100644 ext/standard/tests/filters/user_filter_seek_01.phpt create mode 100644 ext/standard/tests/filters/user_filter_seek_02.phpt create mode 100644 ext/zlib/tests/zlib_filter_seek_deflate.phpt create mode 100644 ext/zlib/tests/zlib_filter_seek_inflate.phpt diff --git a/NEWS b/NEWS index 54a45028c6a14..e75b26e7d1436 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 57bef65c25ee9..656e1d556a506 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 27059391dacb9..69ee483d21f12 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 0000000000000..0656b244484d9 --- /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-- + $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-- + +--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 0000000000000..8ccd224b1a406 --- /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-- + +--CLEAN-- + +--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 5db958e36d358..46dac358d8e21 100644 --- a/ext/gd/tests/bug79945.phpt +++ b/ext/gd/tests/bug79945.phpt @@ -9,17 +9,17 @@ if (!(imagetypes() & IMG_PNG)) { } 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-- --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 ed7d332397dbc..02cffcb4b859b 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 0000000000000..97519ec7853d3 --- /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-- + +--CLEAN-- + +--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 0faa0eda6863a..c909211788b03 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 0000000000000..6cbcd7b843f35 --- /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-- + +--CLEAN-- + +--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 0000000000000..72f874f21ad40 --- /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-- + +--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 0000000000000..60eab2ccb84ae --- /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-- + '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-- + +--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 0000000000000..cb4e9fe72267f --- /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-- +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 0000000000000..39f4c3c66243e --- /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-- +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 610784e547e45..ac21e083efbb0 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 acaa42bb33c84..475ec58e79e78 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 1d3c7bb53586b..d530a5c000662 100644 --- a/ext/standard/user_filters_arginfo.h +++ b/ext/standard/user_filters_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit user_filters.stub.php instead. - * Stub hash: 33264435fe01a2cc9aa21a4a087dbbf3c4007206 */ + * Stub hash: 593afbcb51bb35207b93ac7556b92ac845043116 */ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_php_user_filter_filter, 0, 4, IS_LONG, 0) ZEND_ARG_INFO(0, in) @@ -8,6 +8,11 @@ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_php_user_filter_ ZEND_ARG_TYPE_INFO(0, closing, _IS_BOOL, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_php_user_filter_seek, 0, 2, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, offset, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, whence, IS_LONG, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_php_user_filter_onCreate, 0, 0, _IS_BOOL, 0) ZEND_END_ARG_INFO() @@ -15,11 +20,13 @@ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_php_user_filter_ ZEND_END_ARG_INFO() ZEND_METHOD(php_user_filter, filter); +ZEND_METHOD(php_user_filter, seek); ZEND_METHOD(php_user_filter, onCreate); ZEND_METHOD(php_user_filter, onClose); static const zend_function_entry class_php_user_filter_methods[] = { ZEND_ME(php_user_filter, filter, arginfo_class_php_user_filter_filter, ZEND_ACC_PUBLIC) + ZEND_ME(php_user_filter, seek, arginfo_class_php_user_filter_seek, ZEND_ACC_PUBLIC) ZEND_ME(php_user_filter, onCreate, arginfo_class_php_user_filter_onCreate, ZEND_ACC_PUBLIC) ZEND_ME(php_user_filter, onClose, arginfo_class_php_user_filter_onClose, ZEND_ACC_PUBLIC) ZEND_FE_END 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 0000000000000..6acee8e4e8c81 --- /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-- + $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-- + +--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 0000000000000..2ca7508941ccc --- /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-- + +--CLEAN-- + +--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 e42132fd0008c..69c9a85465e8e 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 d0c1fdc8e788c..2260bae82335e 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 e224b85b2d9a2..cb4fdb5fd7f79 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 32c7ba99f58c2..26f147632cef7 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 */ From c3a1214ef9975b537339196da6645ee9c5dc4667 Mon Sep 17 00:00:00 2001 From: Calvin Buckley Date: Mon, 30 Mar 2026 09:27:54 -0400 Subject: [PATCH 2/3] ext/standard: Use posix_spawn_file_actions_addchdir when available (#21553) posix_spawn_file_actions_addchdir is part of POSIX now, so some OSes (macOS at least) have started to deprecated the _np variant. Some support both names (Solaris, NetBSD), others don't yet (FreeBSD). Use the non-np variant when possible to avoid the deprecation warning on macOS and other platforms in the future. Fixes GH-21552 --- ext/standard/config.m4 | 2 +- ext/standard/proc_open.c | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/ext/standard/config.m4 b/ext/standard/config.m4 index ef6b3c5a01018..1d64c7ff69681 100644 --- a/ext/standard/config.m4 +++ b/ext/standard/config.m4 @@ -328,7 +328,7 @@ dnl PHP_CHECK_FUNC(res_search, resolv, socket) -AC_CHECK_FUNCS([posix_spawn_file_actions_addchdir_np elf_aux_info]) +AC_CHECK_FUNCS([posix_spawn_file_actions_addchdir posix_spawn_file_actions_addchdir_np elf_aux_info]) dnl dnl Obsolete check for strptime() declaration. The strptime, where available, diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c index 55f4dd4849587..a393a2a1f8848 100644 --- a/ext/standard/proc_open.c +++ b/ext/standard/proc_open.c @@ -35,7 +35,7 @@ #include #endif -#ifdef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP +#if defined(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP) || defined(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR) /* Only defined on glibc >= 2.29, FreeBSD CURRENT, musl >= 1.1.24, * MacOS Catalina or later.. * It should be possible to modify this so it is also @@ -45,6 +45,13 @@ */ #include #define USE_POSIX_SPAWN + +/* The non-_np variant is in macOS 26 (and _np deprecated) */ +#ifdef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR +#define POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR posix_spawn_file_actions_addchdir +#else +#define POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR posix_spawn_file_actions_addchdir_np +#endif #endif /* This symbol is defined in ext/standard/config.m4. @@ -1394,9 +1401,9 @@ PHP_FUNCTION(proc_open) } if (cwd) { - r = posix_spawn_file_actions_addchdir_np(&factions, cwd); + r = POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR(&factions, cwd); if (r != 0) { - php_error_docref(NULL, E_WARNING, "posix_spawn_file_actions_addchdir_np() failed: %s", strerror(r)); + php_error_docref(NULL, E_WARNING, ZEND_TOSTR(POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR) "() failed: %s", strerror(r)); } } From cd753007ce15665620343dc9244b37526c805cb4 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Mon, 30 Mar 2026 14:48:12 +0100 Subject: [PATCH 3/3] Audit INI functions and macros, and replace them with better alternatives (#21146) Audit zend_ini_string(), INI_STR(), and related functions and macros, and all other INI_* macros. The primary motivation is that these APIs should return a `const char*` because the `char*` is owned by a zend_string, and thus should never be released. Moreover, the INI_STR() API doesn't follow our usual naming scheme as it returns a char* rather than a zend_string*. In this PR new zend_ini_{bool|long|double|str|string}_literal() macros are introduced replacing the INI_{BOOL|INT|FLT|STR} macros which follow our more modern naming convention. Moreover, the INI_BOOL() macro didn't produce correct values if the INI string is a word like "true" or "on". The INI_ORIG_* APIs are removed because a Sourcegraph search shows 0 results, other than the one case used in ext/tidy that we fix using the typical API. Add some additional checks for the value of an INI string to ensure it doesn't contain a nul byte when describing a path. --- UPGRADING.INTERNALS | 8 ++++++++ Zend/zend_fibers.c | 6 +++--- Zend/zend_highlight.c | 4 ++-- Zend/zend_highlight.h | 10 +++++----- Zend/zend_ini.c | 4 ++-- Zend/zend_ini.h | 20 ++++++++------------ Zend/zend_multibyte.c | 4 ++-- ext/com_dotnet/com_dotnet.c | 7 +++---- ext/curl/interface.c | 6 ++---- ext/date/php_date.c | 8 ++++---- ext/gd/gd.c | 2 +- ext/openssl/openssl_backend_common.c | 6 ++---- ext/openssl/xp_ssl.c | 12 ++++++------ ext/session/session.c | 8 ++++---- ext/soap/soap.c | 4 ++-- ext/standard/basic_functions.c | 17 ++++++++--------- ext/standard/browscap.c | 6 +++--- ext/standard/dl.c | 4 ++-- ext/standard/mail.c | 16 ++++++++-------- ext/standard/tests/mail/mail_basic2.phpt | 7 +++++-- ext/tidy/tidy.c | 2 +- ext/zlib/zlib.c | 5 ++--- main/main.c | 8 ++++---- main/php_ini.c | 2 +- main/rfc1867.c | 4 ++-- sapi/phpdbg/phpdbg_prompt.c | 17 +++++++++-------- win32/sendmail.c | 8 ++++---- 27 files changed, 103 insertions(+), 102 deletions(-) diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index 656e1d556a506..1072d822ee492 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -75,6 +75,14 @@ PHP 8.6 INTERNALS UPGRADE NOTES . Added a C23_ENUM() helper macro to define forward-compatible fixed-size enums. . Extended php_stream_filter_ops with seek method. + . The INI_STR(), INI_INT(), INI_FLT(), and INI_BOOL() macros have been + removed. Instead new zend_ini_{bool|long|double|str|string}_literal() + macros have been added. This fixes an internal naming inconsistency as + "str" usually means zend_string*, and "string" means char*. + However INI_STR() returned a char* + . The INI_ORIG_{INT|STR|FLT|BOOL}() macros have been removed as they are + unused. If this behaviour is required fall back to the zend_ini_* + functions. ======================== 2. Build system changes diff --git a/Zend/zend_fibers.c b/Zend/zend_fibers.c index d571a622e476b..248b4f401a71a 100644 --- a/Zend/zend_fibers.c +++ b/Zend/zend_fibers.c @@ -572,9 +572,9 @@ static ZEND_STACK_ALIGNED void zend_fiber_execute(zend_fiber_transfer *transfer) zend_fiber *fiber = EG(active_fiber); /* Determine the current error_reporting ini setting. */ - zend_long error_reporting = INI_INT("error_reporting"); - /* If error_reporting is 0 and not explicitly set to 0, INI_STR returns a null pointer. */ - if (!error_reporting && !INI_STR("error_reporting")) { + zend_long error_reporting = zend_ini_long_literal("error_reporting"); + /* If error_reporting is 0 and not explicitly set to 0, zend_ini_str returns a null pointer. */ + if (!error_reporting && !zend_ini_str_literal("error_reporting")) { error_reporting = E_ALL; } diff --git a/Zend/zend_highlight.c b/Zend/zend_highlight.c index 5c3cd136d5807..c5fc874b9cb5f 100644 --- a/Zend/zend_highlight.c +++ b/Zend/zend_highlight.c @@ -79,8 +79,8 @@ ZEND_API void zend_highlight(zend_syntax_highlighter_ini *syntax_highlighter_ini { zval token; int token_type; - char *last_color = syntax_highlighter_ini->highlight_html; - char *next_color; + const char *last_color = syntax_highlighter_ini->highlight_html; + const char *next_color; zend_printf("
", last_color);
 	/* highlight stuff coming back from zendlex() */
diff --git a/Zend/zend_highlight.h b/Zend/zend_highlight.h
index 04688d65132b2..adc1d3c8c81e0 100644
--- a/Zend/zend_highlight.h
+++ b/Zend/zend_highlight.h
@@ -30,11 +30,11 @@
 
 
 typedef struct _zend_syntax_highlighter_ini {
-	char *highlight_html;
-	char *highlight_comment;
-	char *highlight_default;
-	char *highlight_string;
-	char *highlight_keyword;
+	const char *highlight_html;
+	const char *highlight_comment;
+	const char *highlight_default;
+	const char *highlight_string;
+	const char *highlight_keyword;
 } zend_syntax_highlighter_ini;
 
 
diff --git a/Zend/zend_ini.c b/Zend/zend_ini.c
index d8f1d4f50f95d..85739415feb32 100644
--- a/Zend/zend_ini.c
+++ b/Zend/zend_ini.c
@@ -491,7 +491,7 @@ ZEND_API double zend_ini_double(const char *name, size_t name_length, bool orig)
 }
 /* }}} */
 
-ZEND_API char *zend_ini_string_ex(const char *name, size_t name_length, bool orig, bool *exists) /* {{{ */
+ZEND_API const char *zend_ini_string_ex(const char *name, size_t name_length, bool orig, bool *exists) /* {{{ */
 {
 	zend_string *str = zend_ini_str_ex(name, name_length, orig, exists);
 
@@ -499,7 +499,7 @@ ZEND_API char *zend_ini_string_ex(const char *name, size_t name_length, bool ori
 }
 /* }}} */
 
-ZEND_API char *zend_ini_string(const char *name, size_t name_length, bool orig) /* {{{ */
+ZEND_API const char *zend_ini_string(const char *name, size_t name_length, bool orig) /* {{{ */
 {
 	zend_string *str = zend_ini_str(name, name_length, orig);
 
diff --git a/Zend/zend_ini.h b/Zend/zend_ini.h
index d8d7f599f461e..d2419bb160fda 100644
--- a/Zend/zend_ini.h
+++ b/Zend/zend_ini.h
@@ -88,13 +88,19 @@ ZEND_API void display_ini_entries(zend_module_entry *module);
 
 ZEND_API zend_long zend_ini_long(const char *name, size_t name_length, bool orig);
 ZEND_API double zend_ini_double(const char *name, size_t name_length, bool orig);
-ZEND_API char *zend_ini_string(const char *name, size_t name_length, bool orig);
-ZEND_API char *zend_ini_string_ex(const char *name, size_t name_length, bool orig, bool *exists);
+ZEND_API const char *zend_ini_string(const char *name, size_t name_length, bool orig);
+ZEND_API const char *zend_ini_string_ex(const char *name, size_t name_length, bool orig, bool *exists);
 ZEND_API zend_string *zend_ini_str(const char *name, size_t name_length, bool orig);
 ZEND_API zend_string *zend_ini_str_ex(const char *name, size_t name_length, bool orig, bool *exists);
 ZEND_API zend_string *zend_ini_get_value(zend_string *name);
 ZEND_API bool zend_ini_parse_bool(const zend_string *str);
 
+#define zend_ini_bool_literal(name) zend_ini_parse_bool(zend_ini_str((name), sizeof("" name) - 1, false))
+#define zend_ini_long_literal(name) zend_ini_long((name), sizeof("" name) - 1, false)
+#define zend_ini_double_literal(name) zend_ini_double((name), sizeof("" name) - 1, false)
+#define zend_ini_str_literal(name) zend_ini_str((name), sizeof("" name) - 1, false)
+#define zend_ini_string_literal(name) zend_ini_string((name), sizeof("" name) - 1, false)
+
 /**
  * Parses an ini quantity
  *
@@ -191,16 +197,6 @@ END_EXTERN_C()
 	ZEND_INI_ENTRY3_EX(name, default_value, modifiable, on_modify, (void *) XtOffsetOf(struct_type, property_name), (void *) &struct_ptr, NULL, zend_ini_boolean_displayer_cb)
 #endif
 
-#define INI_INT(name) zend_ini_long((name), strlen(name), 0)
-#define INI_FLT(name) zend_ini_double((name), strlen(name), 0)
-#define INI_STR(name) zend_ini_string_ex((name), strlen(name), 0, NULL)
-#define INI_BOOL(name) ((bool) INI_INT(name))
-
-#define INI_ORIG_INT(name)	zend_ini_long((name), strlen(name), 1)
-#define INI_ORIG_FLT(name)	zend_ini_double((name), strlen(name), 1)
-#define INI_ORIG_STR(name)	zend_ini_string((name), strlen(name), 1)
-#define INI_ORIG_BOOL(name) ((bool) INI_ORIG_INT(name))
-
 #define REGISTER_INI_ENTRIES() zend_register_ini_entries_ex(ini_entries, module_number, type)
 #define UNREGISTER_INI_ENTRIES() zend_unregister_ini_entries_ex(module_number, type)
 #define DISPLAY_INI_ENTRIES() display_ini_entries(zend_module)
diff --git a/Zend/zend_multibyte.c b/Zend/zend_multibyte.c
index f61ed79fd1f7a..fc130162f0837 100644
--- a/Zend/zend_multibyte.c
+++ b/Zend/zend_multibyte.c
@@ -114,8 +114,8 @@ ZEND_API zend_result zend_multibyte_set_functions(const zend_multibyte_functions
 	 * populated, we need to reinitialize script_encoding here.
 	 */
 	{
-		const char *value = zend_ini_string("zend.script_encoding", sizeof("zend.script_encoding") - 1, 0);
-		zend_multibyte_set_script_encoding_by_string(value, strlen(value));
+		const zend_string *value = zend_ini_str_literal("zend.script_encoding");
+		zend_multibyte_set_script_encoding_by_string(ZSTR_VAL(value), ZSTR_LEN(value));
 	}
 	return SUCCESS;
 }
diff --git a/ext/com_dotnet/com_dotnet.c b/ext/com_dotnet/com_dotnet.c
index f8b4a828e154a..e4ab984858312 100644
--- a/ext/com_dotnet/com_dotnet.c
+++ b/ext/com_dotnet/com_dotnet.c
@@ -127,7 +127,6 @@ static HRESULT dotnet_bind_runtime(LPVOID FAR *ppv)
 	typedef HRESULT (STDAPICALLTYPE *cbtr_t)(LPCWSTR pwszVersion, LPCWSTR pwszBuildFlavor, REFCLSID rclsid, REFIID riid, LPVOID FAR *ppv);
 	cbtr_t CorBindToRuntime;
 	OLECHAR *oleversion;
-	char *version;
 
 	mscoree = LoadLibraryA("mscoree.dll");
 	if (mscoree == NULL) {
@@ -140,11 +139,11 @@ static HRESULT dotnet_bind_runtime(LPVOID FAR *ppv)
 		return S_FALSE;
 	}
 
-	version = INI_STR("com.dotnet_version");
-	if (version == NULL || *version == '\0') {
+	const zend_string *version = zend_ini_str_literal("com.dotnet_version");
+	if (version == NULL || ZSTR_LEN(version) == 0) {
 		oleversion = NULL;
 	} else {
-		oleversion = php_com_string_to_olestring(version, strlen(version), COMG(code_page));
+		oleversion = php_com_string_to_olestring(ZSTR_VAL(version), ZSTR_LEN(version), COMG(code_page));
 	}
 
 	hr = CorBindToRuntime(oleversion, NULL, &CLSID_CorRuntimeHost, &IID_ICorRuntimeHost, ppv);
diff --git a/ext/curl/interface.c b/ext/curl/interface.c
index 6328538241fe6..57bc2f1ede3b9 100644
--- a/ext/curl/interface.c
+++ b/ext/curl/interface.c
@@ -1100,8 +1100,6 @@ static void create_certinfo(struct curl_certinfo *ci, zval *listcode)
    Set default options for a handle */
 static void _php_curl_set_default_options(php_curl *ch)
 {
-	char *cainfo;
-
 	curl_easy_setopt(ch->cp, CURLOPT_NOPROGRESS,        1L);
 	curl_easy_setopt(ch->cp, CURLOPT_VERBOSE,           0L);
 	curl_easy_setopt(ch->cp, CURLOPT_ERRORBUFFER,       ch->err.str);
@@ -1114,9 +1112,9 @@ static void _php_curl_set_default_options(php_curl *ch)
 	curl_easy_setopt(ch->cp, CURLOPT_DNS_CACHE_TIMEOUT, 120L);
 	curl_easy_setopt(ch->cp, CURLOPT_MAXREDIRS, 20L); /* prevent infinite redirects */
 
-	cainfo = INI_STR("openssl.cafile");
+	const char *cainfo = zend_ini_string_literal("openssl.cafile");
 	if (!(cainfo && cainfo[0] != '\0')) {
-		cainfo = INI_STR("curl.cainfo");
+		cainfo = zend_ini_string_literal("curl.cainfo");
 	}
 	if (cainfo && cainfo[0] != '\0') {
 		curl_easy_setopt(ch->cp, CURLOPT_CAINFO, cainfo);
diff --git a/ext/date/php_date.c b/ext/date/php_date.c
index dd80e15e9cfbe..e5b094acdb3cd 100644
--- a/ext/date/php_date.c
+++ b/ext/date/php_date.c
@@ -5478,18 +5478,18 @@ static void php_do_date_sunrise_sunset(INTERNAL_FUNCTION_PARAMETERS, bool calc_s
 	ZEND_PARSE_PARAMETERS_END();
 
 	if (latitude_is_null) {
-		latitude = INI_FLT("date.default_latitude");
+		latitude = zend_ini_double_literal("date.default_latitude");
 	}
 
 	if (longitude_is_null) {
-		longitude = INI_FLT("date.default_longitude");
+		longitude = zend_ini_double_literal("date.default_longitude");
 	}
 
 	if (zenith_is_null) {
 		if (calc_sunset) {
-			zenith = INI_FLT("date.sunset_zenith");
+			zenith = zend_ini_double_literal("date.sunset_zenith");
 		} else {
-			zenith = INI_FLT("date.sunrise_zenith");
+			zenith = zend_ini_double_literal("date.sunrise_zenith");
 		}
 	}
 
diff --git a/ext/gd/gd.c b/ext/gd/gd.c
index 33b397b4f699b..1bd1a26f05199 100644
--- a/ext/gd/gd.c
+++ b/ext/gd/gd.c
@@ -1595,7 +1595,7 @@ static void _php_image_create_from(INTERNAL_FUNCTION_PARAMETERS, int image_type,
 
 #ifdef HAVE_GD_JPG
 			case PHP_GDIMG_TYPE_JPG:
-				ignore_warning = INI_INT("gd.jpeg_ignore_warning");
+				ignore_warning = zend_ini_bool_literal("gd.jpeg_ignore_warning");
 				im = gdImageCreateFromJpegEx(fp, ignore_warning);
 			break;
 #endif
diff --git a/ext/openssl/openssl_backend_common.c b/ext/openssl/openssl_backend_common.c
index 6f257af49819e..533d2f2c61f1b 100644
--- a/ext/openssl/openssl_backend_common.c
+++ b/ext/openssl/openssl_backend_common.c
@@ -504,10 +504,8 @@ void php_openssl_set_cert_locations(zval *return_value)
 	add_assoc_string(return_value, "default_cert_dir_env", (char *) X509_get_default_cert_dir_env());
 	add_assoc_string(return_value, "default_private_dir", (char *) X509_get_default_private_dir());
 	add_assoc_string(return_value, "default_default_cert_area", (char *) X509_get_default_cert_area());
-	add_assoc_string(return_value, "ini_cafile",
-		zend_ini_string("openssl.cafile", sizeof("openssl.cafile")-1, 0));
-	add_assoc_string(return_value, "ini_capath",
-		zend_ini_string("openssl.capath", sizeof("openssl.capath")-1, 0));
+	add_assoc_str(return_value, "ini_cafile", zend_string_copy(zend_ini_str_literal("openssl.cafile")));
+	add_assoc_str(return_value, "ini_capath", zend_string_copy(zend_ini_str_literal("openssl.capath")));
 }
 
 X509 *php_openssl_x509_from_str(
diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c
index 34b4acd9f1c74..8f9395e460223 100644
--- a/ext/openssl/xp_ssl.c
+++ b/ext/openssl/xp_ssl.c
@@ -880,16 +880,16 @@ static long php_openssl_load_stream_cafile(X509_STORE *cert_store, const char *c
 static zend_result php_openssl_enable_peer_verification(SSL_CTX *ctx, php_stream *stream) /* {{{ */
 {
 	zval *val = NULL;
-	char *cafile = NULL;
-	char *capath = NULL;
+	const char *cafile = NULL;
+	const char *capath = NULL;
 	php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t*)stream->abstract;
 
 	GET_VER_OPT_STRING("cafile", cafile);
 	GET_VER_OPT_STRING("capath", capath);
 
 	if (cafile == NULL) {
-		cafile = zend_ini_string("openssl.cafile", sizeof("openssl.cafile")-1, 0);
-		cafile = strlen(cafile) ? cafile : NULL;
+		const zend_string *cafile_str = zend_ini_str_literal("openssl.cafile");
+		cafile = ZSTR_LEN(cafile_str) ? ZSTR_VAL(cafile_str) : NULL;
 	} else if (!sslsock->is_client) {
 		/* Servers need to load and assign CA names from the cafile */
 		STACK_OF(X509_NAME) *cert_names = SSL_load_client_CA_file(cafile);
@@ -902,8 +902,8 @@ static zend_result php_openssl_enable_peer_verification(SSL_CTX *ctx, php_stream
 	}
 
 	if (capath == NULL) {
-		capath = zend_ini_string("openssl.capath", sizeof("openssl.capath")-1, 0);
-		capath = strlen(capath) ? capath : NULL;
+		const zend_string *capath_str = zend_ini_str_literal("openssl.capath");
+		capath = ZSTR_LEN(capath_str) ? ZSTR_VAL(capath_str) : NULL;
 	}
 
 	if (cafile || capath) {
diff --git a/ext/session/session.c b/ext/session/session.c
index c04e19e25edc7..51ab3922e0bdf 100644
--- a/ext/session/session.c
+++ b/ext/session/session.c
@@ -1654,7 +1654,7 @@ PHPAPI zend_result php_session_start(void)
 			return FAILURE;
 
 		case php_session_disabled: {
-			const char *value = zend_ini_string(ZEND_STRL("session.save_handler"), false);
+			const char *value = zend_ini_string_literal("session.save_handler");
 			if (!PS(mod) && value) {
 				PS(mod) = _php_find_ps_module(value);
 				if (!PS(mod)) {
@@ -1662,7 +1662,7 @@ PHPAPI zend_result php_session_start(void)
 					return FAILURE;
 				}
 			}
-			value = zend_ini_string(ZEND_STRL("session.serialize_handler"), false);
+			value = zend_ini_string_literal("session.serialize_handler");
 			if (!PS(serializer) && value) {
 				PS(serializer) = _php_find_ps_serializer(value);
 				if (!PS(serializer)) {
@@ -2768,14 +2768,14 @@ static zend_result php_rinit_session(bool auto_start)
 
 	PS(mod) = NULL;
 	{
-		const char *value = zend_ini_string(ZEND_STRL("session.save_handler"), false);
+		const char *value = zend_ini_string_literal("session.save_handler");
 		if (value) {
 			PS(mod) = _php_find_ps_module(value);
 		}
 	}
 
 	if (PS(serializer) == NULL) {
-		const char *value = zend_ini_string(ZEND_STRL("session.serialize_handler"), false);
+		const char *value = zend_ini_string_literal("session.serialize_handler");
 		if (value) {
 			PS(serializer) = _php_find_ps_serializer(value);
 		}
diff --git a/ext/soap/soap.c b/ext/soap/soap.c
index 178660ec4cd62..59e2bd4168052 100644
--- a/ext/soap/soap.c
+++ b/ext/soap/soap.c
@@ -1695,7 +1695,7 @@ PHP_METHOD(SoapServer, handle)
 			sapi_add_header("Content-Type: text/xml; charset=utf-8", sizeof("Content-Type: text/xml; charset=utf-8")-1, 1);
 		}
 
-		if (INI_INT("zlib.output_compression")) {
+		if (zend_ini_long_literal("zlib.output_compression")) {
 			sapi_add_header("Connection: close", sizeof("Connection: close")-1, 1);
 		} else {
 			snprintf(cont_len, sizeof(cont_len), "Content-Length: %d", size);
@@ -1857,7 +1857,7 @@ static void soap_server_fault_ex(sdlFunctionPtr function, zval* fault, soapHeade
 	if (use_http_error_status) {
 		sapi_add_header("HTTP/1.1 500 Internal Server Error", sizeof("HTTP/1.1 500 Internal Server Error")-1, 1);
 	}
-	if (INI_INT("zlib.output_compression")) {
+	if (zend_ini_long_literal("zlib.output_compression")) {
 		sapi_add_header("Connection: close", sizeof("Connection: close")-1, 1);
 	} else {
 		snprintf(cont_len, sizeof(cont_len), "Content-Length: %d", size);
diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c
index fbbeeb0b433f4..a7417f2a39036 100644
--- a/ext/standard/basic_functions.c
+++ b/ext/standard/basic_functions.c
@@ -1717,11 +1717,11 @@ PHPAPI bool append_user_shutdown_function(php_shutdown_function_entry *shutdown_
 
 ZEND_API void php_get_highlight_struct(zend_syntax_highlighter_ini *syntax_highlighter_ini) /* {{{ */
 {
-	syntax_highlighter_ini->highlight_comment = INI_STR("highlight.comment");
-	syntax_highlighter_ini->highlight_default = INI_STR("highlight.default");
-	syntax_highlighter_ini->highlight_html    = INI_STR("highlight.html");
-	syntax_highlighter_ini->highlight_keyword = INI_STR("highlight.keyword");
-	syntax_highlighter_ini->highlight_string  = INI_STR("highlight.string");
+	syntax_highlighter_ini->highlight_comment = zend_ini_string_literal("highlight.comment");
+	syntax_highlighter_ini->highlight_default = zend_ini_string_literal("highlight.default");
+	syntax_highlighter_ini->highlight_html    = zend_ini_string_literal("highlight.html");
+	syntax_highlighter_ini->highlight_keyword = zend_ini_string_literal("highlight.keyword");
+	syntax_highlighter_ini->highlight_string  = zend_ini_string_literal("highlight.string");
 }
 /* }}} */
 
@@ -2029,17 +2029,16 @@ PHP_FUNCTION(ini_restore)
 PHP_FUNCTION(set_include_path)
 {
 	zend_string *new_value;
-	char *old_value;
 	zend_string *key;
 
 	ZEND_PARSE_PARAMETERS_START(1, 1)
 		Z_PARAM_PATH_STR(new_value)
 	ZEND_PARSE_PARAMETERS_END();
 
-	old_value = zend_ini_string("include_path", sizeof("include_path") - 1, 0);
+	zend_string *old_value = zend_ini_str_literal("include_path");
 	/* copy to return here, because alter might free it! */
 	if (old_value) {
-		RETVAL_STRING(old_value);
+		RETVAL_STR_COPY(old_value);
 	} else {
 		RETVAL_FALSE;
 	}
@@ -2059,7 +2058,7 @@ PHP_FUNCTION(get_include_path)
 {
 	ZEND_PARSE_PARAMETERS_NONE();
 
-	zend_string *str = zend_ini_str("include_path", sizeof("include_path") - 1, 0);
+	zend_string *str = zend_ini_str_literal("include_path");
 
 	if (str == NULL) {
 		RETURN_FALSE;
diff --git a/ext/standard/browscap.c b/ext/standard/browscap.c
index 5009c7793688c..ec9e3a59e4b85 100644
--- a/ext/standard/browscap.c
+++ b/ext/standard/browscap.c
@@ -337,7 +337,7 @@ static void php_browscap_parser_cb(zval *arg1, zval *arg2, zval *arg3, int callb
 					) {
 						zend_error(E_CORE_ERROR, "Invalid browscap ini file: "
 							"'Parent' value cannot be same as the section name: %s "
-							"(in file %s)", ZSTR_VAL(ctx->current_section_name), INI_STR("browscap"));
+							"(in file %s)", ZSTR_VAL(ctx->current_section_name), zend_ini_string_literal("browscap"));
 						return;
 					}
 
@@ -399,7 +399,7 @@ static void php_browscap_parser_cb(zval *arg1, zval *arg2, zval *arg3, int callb
 }
 /* }}} */
 
-static zend_result browscap_read_file(char *filename, browser_data *browdata, bool persistent) /* {{{ */
+static zend_result browscap_read_file(const char *filename, browser_data *browdata, bool persistent) /* {{{ */
 {
 	zend_file_handle fh;
 	browscap_parser_ctx ctx = {0};
@@ -499,7 +499,7 @@ PHP_INI_MH(OnChangeBrowscap)
 
 PHP_MINIT_FUNCTION(browscap) /* {{{ */
 {
-	char *browscap = INI_STR("browscap");
+	const char *browscap = zend_ini_string_literal("browscap");
 
 #ifdef ZTS
 	ts_allocate_id(&browscap_globals_id, sizeof(browser_data), (ts_allocate_ctor) browscap_globals_ctor, NULL);
diff --git a/ext/standard/dl.c b/ext/standard/dl.c
index 31adbceac8c29..209b0c1d3b01c 100644
--- a/ext/standard/dl.c
+++ b/ext/standard/dl.c
@@ -114,11 +114,11 @@ PHPAPI int php_load_extension(const char *filename, int type, int start_now)
 	zend_module_entry *module_entry;
 	zend_module_entry *(*get_module)(void);
 	int error_type, slash_suffix = 0;
-	char *extension_dir;
+	const char *extension_dir;
 	char *err1, *err2;
 
 	if (type == MODULE_PERSISTENT) {
-		extension_dir = INI_STR("extension_dir");
+		extension_dir = zend_ini_string_literal("extension_dir");
 	} else {
 		extension_dir = PG(extension_dir);
 	}
diff --git a/ext/standard/mail.c b/ext/standard/mail.c
index 395c7bb81d4f9..c909c0ed55dd7 100644
--- a/ext/standard/mail.c
+++ b/ext/standard/mail.c
@@ -336,8 +336,8 @@ PHP_FUNCTION(mail)
 		subject_r = subject;
 	}
 
-	zend_string *force_extra_parameters = zend_ini_str_ex("mail.force_extra_parameters", strlen("mail.force_extra_parameters"), false, NULL);
-	if (force_extra_parameters) {
+	zend_string *force_extra_parameters = zend_ini_str_literal("mail.force_extra_parameters");
+	if (force_extra_parameters && ZSTR_LEN(force_extra_parameters) > 0) {
 		extra_cmd = php_escape_shell_cmd(force_extra_parameters);
 	} else if (extra_cmd) {
 		extra_cmd = php_escape_shell_cmd(extra_cmd);
@@ -437,9 +437,9 @@ static int php_mail_detect_multiple_crlf(const char *hdr) {
 PHPAPI bool php_mail(const char *to, const char *subject, const char *message, const char *headers, const zend_string *extra_cmd)
 {
 	FILE *sendmail;
-	char *sendmail_path = INI_STR("sendmail_path");
+	const char *sendmail_path = zend_ini_string_literal("sendmail_path");
 	char *sendmail_cmd = NULL;
-	const zend_string *mail_log = zend_ini_str(ZEND_STRL("mail.log"), false);
+	const zend_string *mail_log = zend_ini_str_literal("mail.log");
 	const char *hdr = headers;
 	char *ahdr = NULL;
 #if PHP_SIGCHILD
@@ -536,7 +536,7 @@ PHPAPI bool php_mail(const char *to, const char *subject, const char *message, c
 		char *tsm_errmsg = NULL;
 
 		/* handle old style win smtp sending */
-		if (TSendMail(INI_STR("SMTP"), &tsm_err, &tsm_errmsg, hdr, subject, to, message) == FAILURE) {
+		if (TSendMail(zend_ini_string_literal("SMTP"), &tsm_err, &tsm_errmsg, hdr, subject, to, message) == FAILURE) {
 			if (tsm_errmsg) {
 				php_error_docref(NULL, E_WARNING, "%s", tsm_errmsg);
 				efree(tsm_errmsg);
@@ -553,7 +553,7 @@ PHPAPI bool php_mail(const char *to, const char *subject, const char *message, c
 	if (extra_cmd != NULL) {
 		spprintf(&sendmail_cmd, 0, "%s %s", sendmail_path, ZSTR_VAL(extra_cmd));
 	} else {
-		sendmail_cmd = sendmail_path;
+		sendmail_cmd = (char*)sendmail_path;
 	}
 
 #if PHP_SIGCHILD
@@ -576,7 +576,7 @@ PHPAPI bool php_mail(const char *to, const char *subject, const char *message, c
 	sendmail = popen(sendmail_cmd, "w");
 #endif
 	if (extra_cmd != NULL) {
-		efree (sendmail_cmd);
+		efree(sendmail_cmd);
 	}
 
 	if (sendmail) {
@@ -701,7 +701,7 @@ PHPAPI bool php_mail(const char *to, const char *subject, const char *message, c
 /* {{{ PHP_MINFO_FUNCTION */
 PHP_MINFO_FUNCTION(mail)
 {
-	char *sendmail_path = INI_STR("sendmail_path");
+	const char *sendmail_path = zend_ini_string_literal("sendmail_path");
 
 #ifdef PHP_WIN32
 	if (!sendmail_path) {
diff --git a/ext/standard/tests/mail/mail_basic2.phpt b/ext/standard/tests/mail/mail_basic2.phpt
index 44d8d86310b01..96a5a4a10a113 100644
--- a/ext/standard/tests/mail/mail_basic2.phpt
+++ b/ext/standard/tests/mail/mail_basic2.phpt
@@ -20,14 +20,17 @@ $message = 'A Message';
 $additional_headers = 'KHeaders';
 $additional_parameters = "-n";
 $outFile = "/tmp/php_test_mailBasic2.out";
-@unlink($outFile);
 
 echo "-- extra parameters --\n";
 // Calling mail() with all possible arguments
 var_dump( mail($to, $subject, $message, $additional_headers, $additional_parameters) );
 
 echo file_get_contents($outFile);
-unlink($outFile);
+
+?>
+--CLEAN--
+
 --EXPECTF--
 *** Testing mail() : basic functionality ***
diff --git a/ext/tidy/tidy.c b/ext/tidy/tidy.c
index 9dcfb733a591f..6b5d1061d5124 100644
--- a/ext/tidy/tidy.c
+++ b/ext/tidy/tidy.c
@@ -873,7 +873,7 @@ static PHP_RINIT_FUNCTION(tidy)
 
 static PHP_RSHUTDOWN_FUNCTION(tidy)
 {
-	TG(clean_output) = INI_ORIG_BOOL("tidy.clean_output");
+	TG(clean_output) = zend_ini_parse_bool(zend_ini_str(ZEND_STRL("tidy.clean_output"), /* orig */ true));
 
 	return SUCCESS;
 }
diff --git a/ext/zlib/zlib.c b/ext/zlib/zlib.c
index ad2c9edb9ddaf..ef792b374b11d 100644
--- a/ext/zlib/zlib.c
+++ b/ext/zlib/zlib.c
@@ -1263,7 +1263,6 @@ ZEND_GET_MODULE(php_zlib)
 static PHP_INI_MH(OnUpdate_zlib_output_compression)
 {
 	int int_value;
-	char *ini_value;
 	if (new_value == NULL) {
 		return FAILURE;
 	}
@@ -1275,9 +1274,9 @@ static PHP_INI_MH(OnUpdate_zlib_output_compression)
 	} else {
 		int_value = (int) zend_ini_parse_quantity_warn(new_value, entry->name);
 	}
-	ini_value = zend_ini_string("output_handler", sizeof("output_handler") - 1, 0);
+	const zend_string *ini_value = zend_ini_str_literal("output_handler");
 
-	if (ini_value && *ini_value && int_value) {
+	if (ini_value && ZSTR_LEN(ini_value) > 0 && int_value) {
 		php_error_docref("ref.outcontrol", E_CORE_ERROR, "Cannot use both zlib.output_compression and output_handler together!!");
 		return FAILURE;
 	}
diff --git a/main/main.c b/main/main.c
index bbd2d18ba187b..bb5b700fe7ae1 100644
--- a/main/main.c
+++ b/main/main.c
@@ -1471,8 +1471,8 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c
 			if (PG(xmlrpc_errors)) {
 				php_printf("faultCode" ZEND_LONG_FMT "faultString%s:%s in %s on line %" PRIu32 "%s%s", PG(xmlrpc_error_number), error_type_str, ZSTR_VAL(message), ZSTR_VAL(error_filename), error_lineno, ZSTR_LEN(backtrace) ? "\nStack trace:\n" : "", ZSTR_VAL(backtrace));
 			} else {
-				char *prepend_string = INI_STR("error_prepend_string");
-				char *append_string = INI_STR("error_append_string");
+				const char *prepend_string = zend_ini_string_literal("error_prepend_string");
+				const char *append_string = zend_ini_string_literal("error_append_string");
 
 				if (PG(html_errors)) {
 					if (type == E_ERROR || type == E_PARSE) {
@@ -2374,7 +2374,7 @@ zend_result php_module_startup(sapi_module_struct *sf, zend_module_entry *additi
 	}
 
 	/* disable certain functions as requested by php.ini */
-	zend_disable_functions(INI_STR("disable_functions"));
+	zend_disable_functions(zend_ini_string_literal("disable_functions"));
 
 	/* make core report what it should */
 	if ((module = zend_hash_str_find_ptr(&module_registry, "core", sizeof("core")-1)) != NULL) {
@@ -2638,7 +2638,7 @@ PHPAPI bool php_execute_script_ex(zend_file_handle *primary_file, zval *retval)
 #ifdef PHP_WIN32
 			zend_unset_timeout();
 #endif
-			zend_set_timeout(INI_INT("max_execution_time"), 0);
+			zend_set_timeout(zend_ini_long_literal("max_execution_time"), false);
 		}
 
 		if (prepend_file_p && result) {
diff --git a/main/php_ini.c b/main/php_ini.c
index e464c05d1fcc1..4bac70e88778f 100644
--- a/main/php_ini.c
+++ b/main/php_ini.c
@@ -334,7 +334,7 @@ static void php_load_zend_extension_cb(void *arg)
 	} else {
 		DL_HANDLE handle;
 		char *libpath;
-		char *extension_dir = INI_STR("extension_dir");
+		const char *extension_dir = zend_ini_string_literal("extension_dir");
 		int slash_suffix = 0;
 		char *err1, *err2;
 
diff --git a/main/rfc1867.c b/main/rfc1867.c
index f6ffb6fabc7f1..161a0e4e487f5 100644
--- a/main/rfc1867.c
+++ b/main/rfc1867.c
@@ -665,8 +665,8 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler)
 	zend_llist header;
 	void *event_extra_data = NULL;
 	unsigned int llen = 0;
-	zend_long upload_cnt = REQUEST_PARSE_BODY_OPTION_GET(max_file_uploads, INI_INT("max_file_uploads"));
-	zend_long body_parts_cnt = REQUEST_PARSE_BODY_OPTION_GET(max_multipart_body_parts, INI_INT("max_multipart_body_parts"));
+	zend_long upload_cnt = REQUEST_PARSE_BODY_OPTION_GET(max_file_uploads, zend_ini_long_literal("max_file_uploads"));
+	zend_long body_parts_cnt = REQUEST_PARSE_BODY_OPTION_GET(max_multipart_body_parts, zend_ini_long_literal("max_multipart_body_parts"));
 	zend_long post_max_size = REQUEST_PARSE_BODY_OPTION_GET(post_max_size, SG(post_max_size));
 	zend_long max_input_vars = REQUEST_PARSE_BODY_OPTION_GET(max_input_vars, PG(max_input_vars));
 	zend_long upload_max_filesize = REQUEST_PARSE_BODY_OPTION_GET(upload_max_filesize, PG(upload_max_filesize));
diff --git a/sapi/phpdbg/phpdbg_prompt.c b/sapi/phpdbg/phpdbg_prompt.c
index 9566c1abd4eec..f3b4ff6b9ddbe 100644
--- a/sapi/phpdbg/phpdbg_prompt.c
+++ b/sapi/phpdbg/phpdbg_prompt.c
@@ -1190,19 +1190,20 @@ static void add_zendext_info(zend_extension *ext) /* {{{ */ {
 #ifdef HAVE_LIBDL
 PHPDBG_API const char *phpdbg_load_module_or_extension(char **path, const char **name) /* {{{ */ {
 	DL_HANDLE handle;
-	char *extension_dir;
-
-	extension_dir = INI_STR("extension_dir");
+	zend_string *extension_dir = zend_ini_str_literal("extension_dir");
 
+	if (UNEXPECTED(zend_str_has_nul_byte(extension_dir))) {
+		phpdbg_error("extension_dir ini setting contains a nul byte");
+		return NULL;
+	}
 	if (strchr(*path, '/') != NULL || strchr(*path, DEFAULT_SLASH) != NULL) {
 		/* path is fine */
-	} else if (extension_dir && extension_dir[0]) {
+	} else if (extension_dir && ZSTR_LEN(extension_dir) > 0) {
 		char *libpath;
-		int extension_dir_len = strlen(extension_dir);
-		if (IS_SLASH(extension_dir[extension_dir_len-1])) {
-			spprintf(&libpath, 0, "%s%s", extension_dir, *path); /* SAFE */
+		if (IS_SLASH(ZSTR_VAL(extension_dir)[ZSTR_LEN(extension_dir)-1])) {
+			spprintf(&libpath, 0, "%s%s", ZSTR_VAL(extension_dir), *path); /* SAFE */
 		} else {
-			spprintf(&libpath, 0, "%s%c%s", extension_dir, DEFAULT_SLASH, *path); /* SAFE */
+			spprintf(&libpath, 0, "%s%c%s", ZSTR_VAL(extension_dir), DEFAULT_SLASH, *path); /* SAFE */
 		}
 		efree(*path);
 		*path = libpath;
diff --git a/win32/sendmail.c b/win32/sendmail.c
index f267d1fae85ab..939106f31b00e 100644
--- a/win32/sendmail.c
+++ b/win32/sendmail.c
@@ -216,8 +216,8 @@ PHPAPI int TSendMail(const char *host, int *error, char **error_message,
 	}
 
 	/* Fall back to sendmail_from php.ini setting */
-	if (INI_STR("sendmail_from")) {
-		RPath = estrdup(INI_STR("sendmail_from"));
+	if (zend_ini_string_literal("sendmail_from")) {
+		RPath = estrdup(zend_ini_string_literal("sendmail_from"));
 	} else if (headers_lc) {
 		int found = 0;
 		const char *lookup = ZSTR_VAL(headers_lc);
@@ -276,7 +276,7 @@ PHPAPI int TSendMail(const char *host, int *error, char **error_message,
 		snprintf(*error_message, HOST_NAME_LEN + 128,
 			"Failed to connect to mailserver at \"%s\" port " ZEND_ULONG_FMT ", verify your \"SMTP\" "
 			"and \"smtp_port\" setting in php.ini or use ini_set()",
-			PW32G(mail_host), !INI_INT("smtp_port") ? 25 : INI_INT("smtp_port"));
+			PW32G(mail_host), !zend_ini_long_literal("smtp_port") ? 25 : zend_ini_long_literal("smtp_port"));
 		return FAILURE;
 	} else {
 		ret = SendText(RPath, Subject, mailTo, data, headers_trim, headers_lc, error_message);
@@ -789,7 +789,7 @@ return 0;
 	}
 	*/
 
-	portnum = (short) INI_INT("smtp_port");
+	portnum = (short) zend_ini_long_literal("smtp_port");
 	if (!portnum) {
 		portnum = 25;
 	}