[2/3] posix: Fix spurious wordexp WRDE_NOSPACE for empty expansions (BZ 34629)

Message ID 20260911165932.1737113-3-adhemerval.zanella@linaro.org (mailing list archive)
State Under Review
Delegated to: Wilco Dijkstra
Headers
Series posix: Fix wordexp handling of empty expansions |

Checks

Context Check Description
redhat-pt-bot/TryBot-apply_patch success Patch applied to master at the time it was sent
linaro-tcwg-bot/tcwg_glibc_build--master-aarch64 success Build passed
linaro-tcwg-bot/tcwg_glibc_build--master-arm success Build passed
linaro-tcwg-bot/tcwg_glibc_check--master-aarch64 fail Test failed
linaro-tcwg-bot/tcwg_glibc_check--master-arm success Test passed

Commit Message

Adhemerval Zanella Netto Sept. 11, 2026, 4:59 p.m. UTC
  w_addstr returns NULL both when it runs out of memory and when the
string being appended is empty and the word has not been started yet
started.

Several callers treat a NULL result as an allocation failure
unconditionally, so a legitimate expansion to an empty string is
reported as WRDE_NOSPACE.  For instance:

  wordexp ("\"$var\"", &we, 0)      with var set and null
  wordexp ("\"$var\"x", &we, 0)     likewise
  wordexp ("\"$*\"", &we, 0)        with no positional parameters
  wordexp ("~", &we, 0)             with HOME set and null
  wordexp ("\"$@\"", &we, 0)        with more than two positional
                                    parameters, the first one being null

Add a w_addstr_status helper that does the same, and use it in parse_tilde
and in the quoted path of parse_param

Checked on x86_64-linux-gnu and i686-linux-gnu.
---
 posix/wordexp-test.c | 10 ++++++++++
 posix/wordexp.c      | 41 +++++++++++++++++++++++++++++------------
 2 files changed, 39 insertions(+), 12 deletions(-)
  

Patch

diff --git a/posix/wordexp-test.c b/posix/wordexp-test.c
index 86be38e5966..6c10bd3458a 100644
--- a/posix/wordexp-test.c
+++ b/posix/wordexp-test.c
@@ -95,6 +95,16 @@  struct test_case_struct
     { 0, NULL, "contin\\\nuation", 0, 1, { "continuation", }, IFS },
     { 0, NULL, "explicit ''", 0, 2, { "explicit", "", }, IFS },
     { 0, NULL, "explicit \"\"", 0, 2, { "explicit", "", }, IFS },
+
+    /* Quoted expansion of a parameter that is set but null.  No field
+       splitting is done, so the result is a quoted null string rather than
+       no field at all.  */
+    { 0, "", "\"$var\"", 0, 1, { "", }, IFS },
+    { 0, "", "\"${var}\"", 0, 1, { "", }, IFS },
+    { 0, "", "\"$var$var\"", 0, 1, { "", }, IFS },
+    { 0, "", "x\"$var\"", 0, 1, { "x", }, IFS },
+    { 0, "", "\"a${var}b\"", 0, 1, { "ab", }, IFS },
+    { 0, "", "one \"$var\" two", 0, 3, { "one", "", "two", }, IFS },
     { 0, NULL, "explicit ``", 0, 1, { "explicit", }, IFS },
 
     /* Simple command substitution */
diff --git a/posix/wordexp.c b/posix/wordexp.c
index 0ad005c0f38..7741a85c23a 100644
--- a/posix/wordexp.c
+++ b/posix/wordexp.c
@@ -137,6 +137,18 @@  w_addstr (char *buffer, size_t *actlen, size_t *maxlen, const char *str)
   return w_addmem (buffer, actlen, maxlen, str, len);
 }
 
+/* w_addstr returns NULL both when it runs out of memory and when STR is
+   empty and *WORD has not been started yet.  Only the former is a failure.  */
+static bool
+w_addstr_status (char **word, size_t *word_length, size_t *max_length,
+		 const char *str)
+{
+  bool empty = *str == '\0';
+
+  *word = w_addstr (*word, word_length, max_length, str);
+  return *word != NULL || empty;
+}
+
 static int
 w_addword (wordexp_t *pwordexp, char *word)
 {
@@ -292,8 +304,7 @@  parse_tilde (char **word, size_t *word_length, size_t *max_length,
       home = getenv ("HOME");
       if (home != NULL)
 	{
-	  *word = w_addstr (*word, word_length, max_length, home);
-	  if (*word == NULL)
+	  if (!w_addstr_status (word, word_length, max_length, home))
 	    return WRDE_NOSPACE;
 	}
       else
@@ -313,8 +324,7 @@  parse_tilde (char **word, size_t *word_length, size_t *max_length,
 
 	  if (result == 0 && tpwd != NULL && pwd.pw_dir != NULL)
 	    {
-	      *word = w_addstr (*word, word_length, max_length, pwd.pw_dir);
-	      if (*word == NULL)
+	      if (!w_addstr_status (word, word_length, max_length, pwd.pw_dir))
 		{
 		  scratch_buffer_free (&tmpbuf);
 		  return WRDE_NOSPACE;
@@ -359,21 +369,24 @@  parse_tilde (char **word, size_t *word_length, size_t *max_length,
 	  user = tmpbuf.data;
 	}
 
+      bool ok;
       if (result == 0 && tpwd != NULL && pwd.pw_dir)
-	*word = w_addstr (*word, word_length, max_length, pwd.pw_dir);
+	ok = w_addstr_status (word, word_length, max_length, pwd.pw_dir);
       else
 	{
 	  /* (invalid login name) */
 	  *word = w_addchar (*word, word_length, max_length, '~');
-	  if (*word != NULL)
-	    *word = w_addstr (*word, word_length, max_length, user);
+	  ok = *word != NULL
+	       && w_addstr_status (word, word_length, max_length, user);
 	}
 
       scratch_buffer_free (&tmpbuf);
 
       *offset = i - 1;
+      if (!ok)
+	return WRDE_NOSPACE;
     }
-  return *word ? 0 : WRDE_NOSPACE;
+  return 0;
 }
 
 
@@ -1476,10 +1489,13 @@  envsubst:
 	    {
 	      int p;
 
-	      /* Append first parameter to current word. */
+	      /* Append first parameter to current word.  When the word is
+		 still empty and it is the first parameter, w_addword turns it
+		 into an empty word.  */
 	      value = w_addstr (*word, word_length, max_length,
 				__libc_argv[1]);
-	      if (value == NULL || w_addword (pwordexp, value))
+	      if ((value == NULL && __libc_argv[1][0] != '\0')
+		  || w_addword (pwordexp, value))
 		goto no_space;
 
 	      for (p = 2; __libc_argv[p + 1]; p++)
@@ -1907,11 +1923,12 @@  envsubst:
   if (quoted || !pwordexp)
     {
       /* Quoted - no field split */
-      *word = w_addstr (*word, word_length, max_length, value);
+      bool ok = w_addstr_status (word, word_length, max_length, value);
+
       if (free_value)
 	free (value);
 
-      return *word ? 0 : WRDE_NOSPACE;
+      return ok ? 0 : WRDE_NOSPACE;
     }
   else
     {