Commit 30ed81314b1 for php.net
commit 30ed81314b119fb0d1e0112ec95eee3d29f143ba
Author: Niels Dossche <7771979+ndossche@users.noreply.github.com>
Date: Sun Dec 28 13:45:43 2025 +0100
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.
Closes GH-20794.
diff --git a/NEWS b/NEWS
index 16c8d51cba9..c0bc33dcae1 100644
--- a/NEWS
+++ b/NEWS
@@ -2,7 +2,9 @@ PHP NEWS
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
?? ??? ????, PHP 8.4.18
-
+- Readline:
+ . Fixed bug GH-18139 (Memory leak when overriding some settings
+ via readline_info()). (ndossche)
15 Jan 2026, PHP 8.4.17
diff --git a/ext/readline/readline.c b/ext/readline/readline.c
index d2929218569..838c74da86c 100644
--- a/ext/readline/readline.c
+++ b/ext/readline/readline.c
@@ -47,6 +47,11 @@ static zval _prepped_callback;
static zval _readline_completion;
static zval _readline_array;
+ZEND_TLS char *php_readline_custom_readline_name = NULL;
+#if defined(PHP_WIN32) || defined(HAVE_LIBEDIT)
+ZEND_TLS char *php_readline_custom_line_buffer = NULL;
+#endif
+
PHP_MINIT_FUNCTION(readline);
PHP_MSHUTDOWN_FUNCTION(readline);
PHP_RSHUTDOWN_FUNCTION(readline);
@@ -146,7 +151,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,35 +185,29 @@ 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 = strdup(rl_line_buffer ? rl_line_buffer : "");
+ RETVAL_STRING(SAFE_STRING(rl_line_buffer));
if (value) {
if (!try_convert_to_string(value)) {
RETURN_THROWS();
}
+ /* XXX: These stores would need to be atomic ideally or use a memory barrier */
#if !defined(PHP_WIN32) && !defined(HAVE_LIBEDIT)
- if (!rl_line_buffer) {
- rl_line_buffer = malloc(Z_STRLEN_P(value) + 1);
- } else if (strlen(oldstr) < Z_STRLEN_P(value)) {
- rl_extend_line_buffer(Z_STRLEN_P(value) + 1);
- free(oldstr);
- oldstr = strdup(rl_line_buffer ? rl_line_buffer : "");
+ rl_extend_line_buffer(Z_STRLEN_P(value) + 1);
+ if (EXPECTED(rl_line_buffer)) {
+ memcpy(rl_line_buffer, Z_STRVAL_P(value), Z_STRLEN_P(value) + 1);
}
- memcpy(rl_line_buffer, Z_STRVAL_P(value), Z_STRLEN_P(value) + 1);
#else
- char *tmp = strdup(Z_STRVAL_P(value));
- if (tmp) {
- if (rl_line_buffer) {
- free(rl_line_buffer);
- }
- rl_line_buffer = tmp;
+ char *copy = strdup(Z_STRVAL_P(value));
+ rl_line_buffer = copy;
+ if (php_readline_custom_line_buffer) {
+ free(php_readline_custom_line_buffer);
}
+ php_readline_custom_line_buffer = copy;
#endif
#if !defined(PHP_WIN32)
rl_end = Z_STRLEN_P(value);
#endif
}
- RETVAL_STRING(SAFE_STRING(oldstr));
- free(oldstr);
} else if (zend_string_equals_literal_ci(what, "point")) {
RETVAL_LONG(rl_point);
#ifndef PHP_WIN32
@@ -268,15 +266,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 00000000000..a2de1f9720c
--- /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--
+<?php
+
+var_dump(readline_info('readline_name', 'first'));
+var_dump(readline_info('readline_name', 'second'));
+var_dump(readline_info('line_buffer', 'third'));
+var_dump(readline_info('line_buffer', 'fourth'));
+
+?>
+--EXPECTF--
+string(%d) "%S"
+string(5) "first"
+string(%d) "%S"
+string(5) "third"