[1/6] linux: Add buffer argument and line truncation to __libc_procutils_read_file

Message ID 20260915183510.2538349-2-adhemerval.zanella@linaro.org (mailing list archive)
State New
Headers
Series Consolidate /proc/self/maps parsing and fix pthread_getattr_np stack bounds |

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 fail Patch failed to apply
linaro-tcwg-bot/tcwg_glibc_build--master-arm fail Patch failed to apply

Commit Message

Adhemerval Zanella Sept. 15, 2026, 6:34 p.m. UTC
  The line reader used a fixed 256-byte buffer and stopped reading the
file once a line did not fit in the buffer.  To make it suitable for
parsing /proc/self/maps, where lines might be arbitrarily long due to
pathnames:

  - The caller now provides the buffer, so each user can size it for
    the expected file contents.

  - A line longer than the buffer is passed to the closure truncated
    to the buffer size, and the remainder of the line is discarded,
    instead of stopping the read.

  - The return value now distinguishes an early stop by the closure
    (procutils_read_stop) from end-of-file (procutils_read_eof) and
    from open/read errors (procutils_read_error, with errno set), so
    callers can act on the errno from a failed open.

  - The closure returns bool, true to stop the read.  The reader only
    uses whether to continue, and the former distinct nonzero values
    were not propagated to the callers.

Also add an internal test for the reader.

Checked on x86_64-linux-gnu.
---
 sysdeps/unix/sysv/linux/Makefile        |   7 ++
 sysdeps/unix/sysv/linux/pidfd_getpid.c  |  23 ++--
 sysdeps/unix/sysv/linux/procutils.c     | 159 +++++++++++++++---------
 sysdeps/unix/sysv/linux/procutils.h     |  38 ++++--
 sysdeps/unix/sysv/linux/tst-procutils.c | 130 +++++++++++++++++++
 5 files changed, 277 insertions(+), 80 deletions(-)
 create mode 100644 sysdeps/unix/sysv/linux/tst-procutils.c
  

Patch

diff --git a/sysdeps/unix/sysv/linux/Makefile b/sysdeps/unix/sysv/linux/Makefile
index 1b15cdc53e8..31f3a7534ae 100644
--- a/sysdeps/unix/sysv/linux/Makefile
+++ b/sysdeps/unix/sysv/linux/Makefile
@@ -303,6 +303,13 @@  tests-static += \
   $(tests-clone-internal) \
   # tests-static
 
+tests-internal += \
+  tst-procutils \
+  # tests-internal
+tests-static += \
+  tst-procutils \
+  # tests-static
+
 CFLAGS-tst-sigcontext-get_pc.c = -fasynchronous-unwind-tables
 
 # Generate the list of SYS_* macros for the system calls (__NR_*
diff --git a/sysdeps/unix/sysv/linux/pidfd_getpid.c b/sysdeps/unix/sysv/linux/pidfd_getpid.c
index 6bf65908772..898db15cf1e 100644
--- a/sysdeps/unix/sysv/linux/pidfd_getpid.c
+++ b/sysdeps/unix/sysv/linux/pidfd_getpid.c
@@ -33,6 +33,8 @@ 
 #define FDINFO_FILENAME_LEN \
   (sizeof (FDINFO_TO_FILENAME_PREFIX) + INT_STRLEN_BOUND (int))
 
+#define FDINFO_MAX_LINE_LEN 256
+
 struct parse_fdinfo_t
 {
   bool found;
@@ -41,12 +43,12 @@  struct parse_fdinfo_t
 
 /* Parse the PID field in the fdinfo entry, if existent.  Avoid strtol or
    similar to not be locale dependent.  */
-static int
+static bool
 parse_fdinfo (const char *l, void *arg)
 {
   enum { fieldlen = sizeof ("Pid:") - 1 };
   if (strncmp (l, "Pid:", fieldlen) != 0)
-    return 0;
+    return false;
 
   l += fieldlen;
 
@@ -62,36 +64,36 @@  parse_fdinfo (const char *l, void *arg)
       l++;
       break;
     case '+':
-      return -1;
+      return true;
     }
 
   if (*l == '\0')
-    return 0;
+    return false;
 
   int n = 0;
   while (*l != '\0')
     {
       /* Check if '*l' is a digit.  */
       if ('0' > *l || *l > '9')
-        return -1;
+        return true;
 
       /* Ignore invalid large values.  */
       if (INT_MULTIPLY_WRAPV (10, n, &n)
           || INT_ADD_WRAPV (n, *l - '0', &n))
-        return -1;
+        return true;
 
       l++;
     }
 
   /* -1 indicates that the process is terminated.  */
   if (neg && n != 1)
-    return -1;
+    return true;
 
   struct parse_fdinfo_t *fdinfo = arg;
   fdinfo->pid = neg ? -n : n;
   fdinfo->found = true;
 
-  return 1;
+  return true;
 }
 
 static pid_t
@@ -104,7 +106,10 @@  getpid_fdinfo (int fd)
   *_fitoa_word (fd, p, 10, 0) = '\0';
 
   struct parse_fdinfo_t fdinfo = { .found = false, .pid = -1 };
-  if (!__libc_procutils_read_file (fdinfoname, parse_fdinfo, &fdinfo))
+  char buffer[FDINFO_MAX_LINE_LEN];
+  if (__libc_procutils_read_file (fdinfoname, buffer, sizeof buffer,
+				  parse_fdinfo, &fdinfo)
+      == procutils_read_error)
     /* The fdinfo contains an invalid 'Pid:' value.  */
     return INLINE_SYSCALL_ERROR_RETURN_VALUE (EBADF);
 
diff --git a/sysdeps/unix/sysv/linux/procutils.c b/sysdeps/unix/sysv/linux/procutils.c
index a32f931fe30..5d71192ceb0 100644
--- a/sysdeps/unix/sysv/linux/procutils.c
+++ b/sysdeps/unix/sysv/linux/procutils.c
@@ -16,83 +16,126 @@ 
    License along with the GNU C Library; if not, see
    <https://www.gnu.org/licenses/>.  */
 
-#include <assert.h>
 #include <not-cancel.h>
 #include <procutils.h>
+#include <stdbool.h>
 #include <string.h>
 
-static int
-next_line (char **r, int fd, char *const buffer, char **cp, char **re,
-           char *const buffer_end)
+struct line_reader
 {
-  char *res = *cp;
-  char *nl = memchr (*cp, '\n', *re - *cp);
-  if (nl == NULL)
+  int fd;
+  char *buffer;
+  /* The last byte of the buffer is reserved for the null terminator.  */
+  char *buffer_end;
+  char *cp;   /* Start of the unprocessed data.  */
+  char *re;   /* End of the unprocessed data.  */
+  bool eof;   /* No more data to read.  */
+  bool skip;  /* Discard the input up to the next '\n', the initial part of
+		 the line has already been returned truncated.  */
+};
+
+enum next_line_result_t
+{
+  next_line_ok,
+  next_line_eof,
+  next_line_error
+};
+
+/* Read the next line into *R.  Return next_line_ok on success,
+   next_line_eof on EOF, or next_line_error on read error.  */
+static enum next_line_result_t
+next_line (struct line_reader *lr, char **r)
+{
+  while (true)
     {
-      if (*cp != buffer)
-        {
-          if (*re == buffer_end)
-            {
-              memmove (buffer, *cp, *re - *cp);
-              *re = buffer + (*re - *cp);
-              *cp = buffer;
+      char *nl = memchr (lr->cp, '\n', lr->re - lr->cp);
+      if (nl != NULL)
+	{
+	  char *line = lr->cp;
+	  *nl = '\0';
+	  lr->cp = nl + 1;
+	  if (lr->skip)
+	    {
+	      /* End of a line longer than the buffer, start over.  */
+	      lr->skip = false;
+	      continue;
+	    }
+	  *r = line;
+	  return next_line_ok;
+	}
 
-              ssize_t n = TEMP_FAILURE_RETRY (
-		__read_nocancel (fd, *re, buffer_end - *re));
-              if (n < 0)
-                return -1;
+      if (lr->eof)
+	{
+	  if (lr->cp == lr->re || lr->skip)
+	    return next_line_eof;
+	  /* Last line without a trailing newline.  */
+	  *lr->re = '\0';
+	  *r = lr->cp;
+	  lr->cp = lr->re;
+	  return next_line_ok;
+	}
 
-              *re += n;
+      /* Move the partial line to the start of the buffer to maximize
+	 the read size.  */
+      if (lr->cp != lr->buffer)
+	{
+	  memmove (lr->buffer, lr->cp, lr->re - lr->cp);
+	  lr->re = lr->buffer + (lr->re - lr->cp);
+	  lr->cp = lr->buffer;
+	}
 
-              nl = memchr (*cp, '\n', *re - *cp);
-	      if (nl == NULL)
-	        /* Line too long.  */
-		return 0;
-            }
-          else
-            nl = memchr (*cp, '\n', *re - *cp);
+      if (lr->re == lr->buffer_end)
+	{
+	  /* A line longer than the buffer.  Consume the buffered data
+	     and, for the initial part of the line, also return it
+	     truncated.  */
+	  lr->cp = lr->re = lr->buffer;
+	  if (!lr->skip)
+	    {
+	      *lr->buffer_end = '\0';
+	      *r = lr->buffer;
+	      lr->skip = true;
+	      return next_line_ok;
+	    }
+	}
 
-          res = *cp;
-        }
-
-      if (nl == NULL)
-        nl = *re - 1;
+      ssize_t n = TEMP_FAILURE_RETRY (
+	__read_nocancel (lr->fd, lr->re, lr->buffer_end - lr->re));
+      if (n < 0)
+	return next_line_error;
+      if (n == 0)
+	lr->eof = true;
+      lr->re += n;
     }
-
-  *nl = '\0';
-  *cp = nl + 1;
-  assert (*cp <= *re);
-
-  if (res == *re)
-    return 0;
-
-  *r = res;
-  return 1;
 }
 
-bool
-__libc_procutils_read_file (const char *filename,
-			    procutils_closure_t closure,
+enum procutils_read_result_t
+__libc_procutils_read_file (const char *filename, char *buffer,
+			    size_t buffer_size, procutils_closure_t closure,
 			    void *arg)
 {
-  enum { buffer_size = PROCUTILS_MAX_LINE_LEN };
-  char buffer[buffer_size];
-  char *buffer_end = buffer + buffer_size;
-  char *cp = buffer_end;
-  char *re = buffer_end;
+  struct line_reader lr =
+    {
+      .buffer = buffer,
+      .buffer_end = buffer + buffer_size - 1,
+      .cp = buffer,
+      .re = buffer,
+    };
 
-  int fd = TEMP_FAILURE_RETRY (
+  lr.fd = TEMP_FAILURE_RETRY (
     __open64_nocancel (filename, O_RDONLY | O_CLOEXEC));
-  if (fd == -1)
-    return false;
+  if (lr.fd == -1)
+    return procutils_read_error;
 
-  char *l;
-  int r;
-  while ((r = next_line (&l, fd, buffer, &cp, &re, buffer_end)) > 0)
-    if (closure (l, arg) != 0)
+  enum next_line_result_t r;
+  char *line;
+  while ((r = next_line (&lr, &line)) == next_line_ok)
+    if (closure (line, arg))
       break;
 
-  __close_nocancel_nostatus (fd);
+  __close_nocancel_nostatus (lr.fd);
 
-  return r == 1;
+  if (r == next_line_error)
+    return procutils_read_error;
+  return r == next_line_ok ? procutils_read_stop : procutils_read_eof;
 }
diff --git a/sysdeps/unix/sysv/linux/procutils.h b/sysdeps/unix/sysv/linux/procutils.h
index bebffa2acd1..221f13e8152 100644
--- a/sysdeps/unix/sysv/linux/procutils.h
+++ b/sysdeps/unix/sysv/linux/procutils.h
@@ -20,25 +20,37 @@ 
 #define _PROCUTILS_H
 
 #include <stdbool.h>
+#include <stddef.h>
 
-typedef int (*procutils_closure_t) (const char *line, void *arg);
+typedef bool (*procutils_closure_t) (const char *line, void *arg);
 
-#define PROCUTILS_MAX_LINE_LEN 256
+enum procutils_read_result_t
+{
+  procutils_read_stop,
+  procutils_read_eof,
+  procutils_read_error
+};
 
 /* Open and read the path FILENAME, line per line, and call CLOSURE with
-   argument ARG on each line.  The read is done with a static buffer,
-   with non-cancellable calls, and the line is null terminated.
+   argument ARG on each line.  The read is done with non-cancellable
+   calls using BUFFER of BUFFER_SIZE bytes as scratch area, and the line
+   is null terminated (the '\n' is not included).
 
-   The CLOSURE should return 0 if the read should continue, otherwise the
-   the function should stop and return early.
+   A line longer than BUFFER_SIZE - 1 characters is passed to CLOSURE
+   truncated to BUFFER_SIZE - 1 characters, and the rest of the line up
+   to the next '\n' is discarded.
 
-   The '\n' is not included in the CLOSURE input argument and lines longer
-   than PROCUTILS_MAX_LINE_LEN characteres are ignored.
+   The CLOSURE should return true to stop the read, in which case the
+   function returns early.
 
-   It returns true in case the file is fully read or false if CLOSURE
-   returns a value diferent than 0.  */
-bool __libc_procutils_read_file (const char *filename,
-				 procutils_closure_t closure,
-				 void *arg) attribute_hidden;
+   It returns procutils_read_stop if CLOSURE returned true,
+   procutils_read_eof if the file was fully read, or
+   procutils_read_error if the file could not be opened or a read error
+   occurred (with errno set).  */
+enum procutils_read_result_t
+__libc_procutils_read_file (const char *filename,
+			    char *buffer, size_t buffer_size,
+			    procutils_closure_t closure,
+			    void *arg) attribute_hidden;
 
 #endif
diff --git a/sysdeps/unix/sysv/linux/tst-procutils.c b/sysdeps/unix/sysv/linux/tst-procutils.c
new file mode 100644
index 00000000000..4502bd08596
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/tst-procutils.c
@@ -0,0 +1,130 @@ 
+/* Tests for the procutils line reader.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <procutils.h>
+#include <string.h>
+#include <support/check.h>
+#include <support/temp_file.h>
+#include <support/xunistd.h>
+
+enum
+{
+  max_lines = 8,
+  max_line_len = 63,
+};
+
+struct capture
+{
+  int n;
+  /* Make the closure stop the read after this number of lines, or 0 to
+     always read the whole file.  */
+  int stop_at;
+  char lines[max_lines][max_line_len + 1];
+};
+
+static bool
+capture_line (const char *line, void *arg)
+{
+  struct capture *c = arg;
+  TEST_VERIFY_EXIT (c->n < max_lines);
+  TEST_VERIFY_EXIT (strlen (line) <= max_line_len);
+  strcpy (c->lines[c->n++], line);
+  return c->n == c->stop_at;
+}
+
+static char *tempfile;
+
+static void
+run_test (const char *content, char *buffer, size_t buffer_size, int stop_at,
+	  enum procutils_read_result_t expected_ret,
+	  const char *const expected_lines[],
+	  int expected_n)
+{
+  int fd = xopen (tempfile, O_WRONLY | O_TRUNC, 0600);
+  xwrite (fd, content, strlen (content));
+  xclose (fd);
+
+  struct capture c = { .stop_at = stop_at };
+  TEST_COMPARE (__libc_procutils_read_file (tempfile, buffer, buffer_size,
+					    capture_line, &c),
+		expected_ret);
+  TEST_COMPARE (c.n, expected_n);
+  for (int i = 0; i < expected_n && i < c.n; i++)
+    TEST_COMPARE_STRING (c.lines[i], expected_lines[i]);
+}
+
+static int
+do_test (void)
+{
+  xclose (create_temp_file ("tst-procutils", &tempfile));
+
+  char buf64[64];
+  /* A small buffer, so lines longer than 7 characters are truncated.  */
+  char buf8[8];
+
+  /* Regular lines with a trailing newline, including an empty line.  */
+  run_test ("line1\n\nline3\n", buf64, sizeof buf64, 0, procutils_read_eof,
+	    (const char *const []) { "line1", "", "line3" }, 3);
+
+  /* Last line without a trailing newline.  */
+  run_test ("foo\nbar", buf64, sizeof buf64, 0, procutils_read_eof,
+	    (const char *const []) { "foo", "bar" }, 2);
+
+  /* Empty file.  */
+  run_test ("", buf64, sizeof buf64, 0, procutils_read_eof, NULL, 0);
+
+  /* A line longer than the buffer is passed truncated to
+     buffer_size - 1 characters and the remainder is discarded.  */
+  run_test ("abcdefghijklm\nxy\n", buf8, sizeof buf8, 0, procutils_read_eof,
+	    (const char *const []) { "abcdefg", "xy" }, 2);
+
+  /* A long line spanning multiple buffer refills.  */
+  run_test ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nend\n", buf8, sizeof buf8, 0,
+	    procutils_read_eof,
+	    (const char *const []) { "aaaaaaa", "end" }, 2);
+
+  /* A line of exactly buffer_size - 1 characters is not truncated.  */
+  run_test ("1234567\n89\n", buf8, sizeof buf8, 0, procutils_read_eof,
+	    (const char *const []) { "1234567", "89" }, 2);
+
+  /* A truncated line without a trailing newline at the end of file.  */
+  run_test ("abcdefghijklm", buf8, sizeof buf8, 0, procutils_read_eof,
+	    (const char *const []) { "abcdefg" }, 1);
+
+  /* The closure stopping the read early.  */
+  run_test ("one\ntwo\nthree\n", buf64, sizeof buf64, 2, procutils_read_stop,
+	    (const char *const []) { "one", "two" }, 2);
+
+  /* Nonexistent file.  */
+  {
+    struct capture c = { 0 };
+    errno = 0;
+    TEST_COMPARE (__libc_procutils_read_file ("/tst-procutils-nonexistent",
+					      buf64, sizeof buf64,
+					      capture_line, &c),
+		  procutils_read_error);
+    TEST_COMPARE (errno, ENOENT);
+    TEST_COMPARE (c.n, 0);
+  }
+
+  return 0;
+}
+
+#include <support/test-driver.c>