From 7e4c0c7c2d033bee0b78b1f2cf2b9157357d43f9 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+ndossche@users.noreply.github.com> Date: Sun, 28 Dec 2025 13:45:43 +0100 Subject: [PATCH 1/2] Fix GH-18139: Memory leak when overriding some settings via readline_info() The reason why freeing was not done yet is because the pointer in these variables may be: - Static data set by the readline/libedit library initially, not heap data. - Data set by another thread. Although the libraries appear to be not thread-safe anyway. To solve this, introduce some TLS variables to hold a pointer for us when we override the settings, such that we can free them and are certain they are allocated by us. --- ext/readline/readline.c | 28 +++++++++++++++++++--------- ext/readline/tests/gh18139.phpt | 18 ++++++++++++++++++ 2 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 ext/readline/tests/gh18139.phpt diff --git a/ext/readline/readline.c b/ext/readline/readline.c index 4da9f35951527..1489faca28ca2 100644 --- a/ext/readline/readline.c +++ b/ext/readline/readline.c @@ -47,6 +47,9 @@ static zval _prepped_callback; static zval _readline_completion; static zval _readline_array; +ZEND_TLS char *php_readline_custom_readline_name = NULL; +ZEND_TLS char *php_readline_custom_line_buffer = NULL; + PHP_MINIT_FUNCTION(readline); PHP_MSHUTDOWN_FUNCTION(readline); PHP_RSHUTDOWN_FUNCTION(readline); @@ -146,7 +149,6 @@ PHP_FUNCTION(readline_info) zend_string *what = NULL; zval *value = NULL; size_t oldval; - char *oldstr; if (zend_parse_parameters(ZEND_NUM_ARGS(), "|S!z!", &what, &value) == FAILURE) { RETURN_THROWS(); @@ -181,15 +183,19 @@ PHP_FUNCTION(readline_info) add_assoc_long(return_value,"attempted_completion_over",rl_attempted_completion_over); } else { if (zend_string_equals_literal_ci(what,"line_buffer")) { - oldstr = rl_line_buffer; + RETVAL_STRING(SAFE_STRING(rl_line_buffer)); if (value) { - /* XXX if (rl_line_buffer) free(rl_line_buffer); */ if (!try_convert_to_string(value)) { RETURN_THROWS(); } - rl_line_buffer = strdup(Z_STRVAL_P(value)); + char *copy = strdup(Z_STRVAL_P(value)); + /* XXX: This store would need to be atomic ideally or use a memory barrier */ + rl_line_buffer = copy; + if (php_readline_custom_line_buffer) { + free(php_readline_custom_line_buffer); + } + php_readline_custom_line_buffer = copy; } - RETVAL_STRING(SAFE_STRING(oldstr)); } else if (zend_string_equals_literal_ci(what, "point")) { RETVAL_LONG(rl_point); #ifndef PHP_WIN32 @@ -248,15 +254,19 @@ PHP_FUNCTION(readline_info) RETVAL_STRING((char *)SAFE_STRING(rl_library_version)); #endif } else if (zend_string_equals_literal_ci(what, "readline_name")) { - oldstr = (char*)rl_readline_name; + RETVAL_STRING(SAFE_STRING(rl_readline_name)); if (value) { - /* XXX if (rl_readline_name) free(rl_readline_name); */ if (!try_convert_to_string(value)) { RETURN_THROWS(); } - rl_readline_name = strdup(Z_STRVAL_P(value)); + char *copy = strdup(Z_STRVAL_P(value)); + /* XXX: This store would need to be atomic ideally or use a memory barrier */ + rl_readline_name = copy; + if (php_readline_custom_readline_name) { + free(php_readline_custom_readline_name); + } + php_readline_custom_readline_name = copy; } - RETVAL_STRING(SAFE_STRING(oldstr)); } else if (zend_string_equals_literal_ci(what, "attempted_completion_over")) { oldval = rl_attempted_completion_over; if (value) { diff --git a/ext/readline/tests/gh18139.phpt b/ext/readline/tests/gh18139.phpt new file mode 100644 index 0000000000000..9753b1224ec2d --- /dev/null +++ b/ext/readline/tests/gh18139.phpt @@ -0,0 +1,18 @@ +--TEST-- +GH-18139 (Memory leak when overriding some settings via readline_info()) +--EXTENSIONS-- +readline +--FILE-- + +--EXPECTF-- +string(%d) "%S" +string(5) "first" +string(0) "" +string(5) "third" From 649da43dadce5040a51bf0c8b4bb09221ca216e5 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+ndossche@users.noreply.github.com> Date: Sun, 28 Dec 2025 14:56:44 +0100 Subject: [PATCH 2/2] settings state is sticky, fix test expectation for that --- ext/readline/tests/gh18139.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/readline/tests/gh18139.phpt b/ext/readline/tests/gh18139.phpt index 9753b1224ec2d..a2de1f9720c79 100644 --- a/ext/readline/tests/gh18139.phpt +++ b/ext/readline/tests/gh18139.phpt @@ -14,5 +14,5 @@ var_dump(readline_info('line_buffer', 'fourth')); --EXPECTF-- string(%d) "%S" string(5) "first" -string(0) "" +string(%d) "%S" string(5) "third"