]> git.ipfire.org Git - thirdparty/glibc.git/commitdiff
libio: null terminate the buffer upon initial allocation in getdelim master
authorCollin Funk <collin.funk1@gmail.com>
Wed, 3 Dec 2025 04:02:58 +0000 (20:02 -0800)
committerCollin Funk <collin.funk1@gmail.com>
Sat, 6 Dec 2025 04:09:36 +0000 (20:09 -0800)
Commit 33eff78c8b28adc4963987880e10d96761f2a167 caused issues in nbdkit
which had code similar to this to get the last line of the file:

    while (getline (&line, &len, fp) != -1)
      ;
    /* Process LINE.  */

After that commit, line[0] would be equal to '\0' instead of containing
the last line of the file like before that commit. A recent POSIX issue
clarified that the behavior before and after that commit are allowed,
since the contents of LINE are unspecified after -1 is returned
[1]. However, some programs rely on the previous behavior.

This patch null terminates the buffer upon getdelim/getline's initial
allocation. This is compatible with previous glibc versions, while also
protecting the caller from reading uninitialized memory if the file is
empty, as long as getline/getdelim does the initial allocation.

[1] https://www.austingroupbugs.net/bug_view_page.php?bug_id=1953

Suggested-by: Eric Blake <eblake@redhat.com>
Reviewed-by: Eric Blake <eblake@redhat.com>
libio/iogetdelim.c
libio/tst-getdelim.c
manual/stdio.texi

index 1d8975735284c6b9fbd93feb8e86bd51f674817c..8528e2a01efd47f0d035c98afa925abe9635e376 100644 (file)
@@ -63,7 +63,11 @@ __getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp)
     {
       *n = 120;
       *lineptr = (char *) malloc (*n);
     {
       *n = 120;
       *lineptr = (char *) malloc (*n);
-      if (*lineptr == NULL)
+      /* Null terminate the buffer upon allocation otherwise it will not be
+         null-terminated upon reading from an empty file.  */
+      if (*lineptr != NULL)
+        (*lineptr)[0] = '\0';
+      else
        {
          fseterr_unlocked (fp);
          result = -1;
        {
          fseterr_unlocked (fp);
          result = -1;
@@ -77,7 +81,6 @@ __getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp)
       if (__underflow (fp) == EOF)
        {
          result = -1;
       if (__underflow (fp) == EOF)
        {
          result = -1;
-         (*lineptr)[0] = '\0';
          goto unlock_return;
        }
       len = fp->_IO_read_end - fp->_IO_read_ptr;
          goto unlock_return;
        }
       len = fp->_IO_read_end - fp->_IO_read_ptr;
index 556697453c98a12724eb38b4ae02de4aa3d548e0..a5a6a80caf1c2f87227d383eea90723f74bf24f6 100644 (file)
@@ -22,6 +22,7 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <errno.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <errno.h>
+#include <string.h>
 
 #include <support/check.h>
 #include <support/support.h>
 
 #include <support/check.h>
 #include <support/support.h>
@@ -51,20 +52,53 @@ do_test (void)
   xfclose (memstream);
   free (lineptr);
 
   xfclose (memstream);
   free (lineptr);
 
-  /* Test that getdelim NUL terminates upon reading an EOF from an empty
-     file (BZ #28038).  This test fails on glibc 2.42 and earlier.  */
-  lineptr = xmalloc (1);
-  lineptr[0] = 'A';
-  linelen = 1;
+  /* Test that we null-terminate the buffer upon allocating it (BZ #28038).  */
+  lineptr = NULL;
+  linelen = 0;
   char *file_name;
   TEST_VERIFY_EXIT (create_temp_file ("tst-getdelim.", &file_name) != -1);
   char *file_name;
   TEST_VERIFY_EXIT (create_temp_file ("tst-getdelim.", &file_name) != -1);
-  FILE *fp = fopen (file_name, "r");
+  FILE *fp = fopen (file_name, "w+");
+  free (file_name);
   TEST_VERIFY_EXIT (fp != NULL);
   TEST_VERIFY (getdelim (&lineptr, &linelen, '\n', fp) == -1);
   TEST_VERIFY_EXIT (fp != NULL);
   TEST_VERIFY (getdelim (&lineptr, &linelen, '\n', fp) == -1);
+  TEST_VERIFY (feof (fp));
   TEST_VERIFY (linelen > 0);
   TEST_VERIFY (lineptr[0] == '\0');
   TEST_VERIFY (linelen > 0);
   TEST_VERIFY (lineptr[0] == '\0');
+  free (lineptr);
+
+  /* Test that we can read until -1 is returned and then access the last line
+     of the file.  This behavior was broken by commit
+     33eff78c8b28adc4963987880e10d96761f2a167 and later fixed.  */
+  lineptr = NULL;
+  linelen = 0;
+  char input[] = "a\nb\nc\n";
+  TEST_VERIFY_EXIT (fwrite (input, 1, sizeof input - 1, fp)
+                    == sizeof input - 1);
+  TEST_VERIFY_EXIT (fseeko (fp, 0, SEEK_SET) == 0);
+  char expect[] = { 'a' - 1, '\n', '\0' };
+  for (int i = 0; i < 3; ++i)
+    {
+      ++expect[0];
+      TEST_VERIFY (getdelim (&lineptr, &linelen, '\n', fp) == 2);
+      TEST_VERIFY (linelen > 2);
+      TEST_VERIFY (strcmp (lineptr, expect) == 0);
+    }
+  TEST_VERIFY (getdelim (&lineptr, &linelen, '\n', fp) == -1);
+  TEST_VERIFY (feof (fp));
+  TEST_VERIFY (linelen > 2);
+  TEST_VERIFY (strcmp (lineptr, expect) == 0);
+
+  /* Test the same thing without a newline.  */
+  TEST_VERIFY_EXIT (fwrite ("d", 1, 1, fp) == 1);
+  TEST_VERIFY_EXIT (fseeko (fp, -1, SEEK_CUR) == 0);
+  TEST_VERIFY (getdelim (&lineptr, &linelen, '\n', fp) == 1);
+  TEST_VERIFY (linelen > 2);
+  TEST_VERIFY (strcmp (lineptr, "d") == 0);
+  TEST_VERIFY (getdelim (&lineptr, &linelen, '\n', fp) == -1);
+  TEST_VERIFY (feof (fp));
+  TEST_VERIFY (linelen > 2);
+  TEST_VERIFY (strcmp (lineptr, "d") == 0);
   fclose (fp);
   fclose (fp);
-  free (file_name);
   free (lineptr);
 
   return 0;
   free (lineptr);
 
   return 0;
index e8f60b09c1c9f5a4d8df0ffa4b7132ba07c1e2c2..c01feaed7d59c2d7d193af5ddf7711ba40dcd56f 100644 (file)
@@ -1279,7 +1279,12 @@ This function was originally a GNU extension, but was added in
 POSIX.1-2008.
 
 If an error occurs or end of file is reached without any bytes read,
 POSIX.1-2008.
 
 If an error occurs or end of file is reached without any bytes read,
-@code{getline} returns @code{-1}.
+@code{getline} returns @code{-1}.  POSIX leaves the contents of
+@code{*@var{lineptr}} undefined when @code{getline} returns @code{-1}.
+If the @glibcadj{} implementation of @code{getline} allocates the
+initial buffer it will null terminate it to prevent the caller from
+reading uninitialized memory if no characters can be read from
+@code{stream}.
 @end deftypefun
 
 @deftypefun ssize_t getdelim (char **restrict @var{lineptr}, size_t *restrict @var{n}, int @var{delimiter}, FILE *restrict @var{stream})
 @end deftypefun
 
 @deftypefun ssize_t getdelim (char **restrict @var{lineptr}, size_t *restrict @var{n}, int @var{delimiter}, FILE *restrict @var{stream})