diff --git a/stdio-common/Makefile b/stdio-common/Makefile
index e4f0146d2c..d16b6dc8bc 100644
--- a/stdio-common/Makefile
+++ b/stdio-common/Makefile
@@ -218,6 +218,8 @@ tests := \
   tst-fphex-wide \
   tst-fseek \
   tst-fwrite \
+  tst-getline \
+  tst-getline-enomem \
   tst-gets \
   tst-grouping \
   tst-grouping2 \
@@ -320,6 +322,8 @@ tests-special += \
   # tests-special
 
 generated += \
+  tst-getline-enomem.mtrace \
+  tst-getline.mtrace \
   tst-printf-bz18872-mem.out \
   tst-printf-bz18872.c \
   tst-printf-bz18872.mtrace \
@@ -424,6 +428,12 @@ tst-printf-fp-leak-ENV = \
 tst-scanf-bz27650-ENV = \
   MALLOC_TRACE=$(objpfx)tst-scanf-bz27650.mtrace \
   LD_PRELOAD=$(common-objpfx)malloc/libc_malloc_debug.so
+tst-scanf-getline-ENV = \
+  MALLOC_TRACE=$(objpfx)tst-getline.mtrace \
+  LD_PRELOAD=$(common-objpfx)malloc/libc_malloc_debug.so
+tst-scanf-getline-enomem-ENV = \
+  MALLOC_TRACE=$(objpfx)tst-getline-enomem.mtrace \
+  LD_PRELOAD=$(common-objpfx)malloc/libc_malloc_debug.so
 
 $(objpfx)tst-unbputc.out: tst-unbputc.sh $(objpfx)tst-unbputc
 	$(SHELL) $< $(common-objpfx) '$(test-program-prefix)'; \
diff --git a/stdio-common/tst-getline-enomem.c b/stdio-common/tst-getline-enomem.c
new file mode 100644
index 0000000000..7fc70ea9b5
--- /dev/null
+++ b/stdio-common/tst-getline-enomem.c
@@ -0,0 +1,78 @@
+/* Test getline: ENOMEM on allocation failure.
+   Copyright (C) 2024 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 <mcheck.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/resource.h>
+
+#include <support/check.h>
+#include <support/test-driver.h>
+
+/* Produce a stream of test data based on data in COOKIE (ignored),
+   storing up to SIZE bytes in BUF.  */
+
+static ssize_t
+io_read (void *cookie, char *buf, size_t size)
+{
+  memset (buf, 'x', size);
+  return size;
+}
+
+/* Set up a test stream with fopencookie.  */
+
+static FILE *
+open_test_stream (void)
+{
+  static cookie_io_functions_t io_funcs = { .read = io_read };
+  static int cookie;
+  FILE *fp = fopencookie (&cookie, "r", io_funcs);
+  TEST_VERIFY_EXIT (fp != NULL);
+  return fp;
+}
+
+int
+do_test (void)
+{
+  FILE *fp;
+  char *lineptr = NULL;
+  size_t size = 0;
+  ssize_t ret;
+  mtrace ();
+  /* Test ENOMEM (and error indicator for stream set) for memory
+     allocation failure.  */
+  verbose_printf ("Testing memory allocation failure\n");
+  fp = open_test_stream ();
+  struct rlimit limit;
+  TEST_VERIFY_EXIT (getrlimit (RLIMIT_AS, &limit) == 0);
+  limit.rlim_cur = 32 * 1024 * 1024;
+  TEST_VERIFY_EXIT (setrlimit (RLIMIT_AS, &limit) == 0);
+  errno = 0;
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, -1);
+  TEST_COMPARE (errno, ENOMEM);
+  TEST_COMPARE (!!ferror (fp), 1);
+  TEST_COMPARE (feof (fp), 0);
+  free (lineptr);
+  fclose (fp);
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/stdio-common/tst-getline.c b/stdio-common/tst-getline.c
new file mode 100644
index 0000000000..dd530e0204
--- /dev/null
+++ b/stdio-common/tst-getline.c
@@ -0,0 +1,387 @@
+/* Test getline.
+   Copyright (C) 2024 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 <mcheck.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <support/check.h>
+#include <support/test-driver.h>
+#include <support/support.h>
+#include <support/xstdio.h>
+#include <support/xunistd.h>
+
+static struct test_data
+{
+  /* Input test data for fopencookie stream.  */
+  const char *in_data;
+
+  /* The amount of test data left.  */
+  size_t in_data_left;
+
+  /* Error number for forcing an error on next read.  */
+  int in_error;
+
+  /* Error number for forcing an error (rather than EOF) after all
+     bytes read.  */
+  int in_error_after;
+} the_cookie;
+
+/* Produce a stream of test data based on data in COOKIE, storing up
+   to SIZE bytes in BUF.  */
+
+static ssize_t
+io_read (void *cookie, char *buf, size_t size)
+{
+  struct test_data *p = cookie;
+  if (p->in_error)
+    {
+      errno = p->in_error;
+      return -1;
+    }
+  if (size > p->in_data_left)
+    size = p->in_data_left;
+  memcpy (buf, p->in_data, size);
+  p->in_data += size;
+  p->in_data_left -= size;
+  if (p->in_data_left == 0)
+    p->in_error = p->in_error_after;
+  return size;
+}
+
+/* Set up a test stream with fopencookie.  */
+
+static FILE *
+open_test_stream (const char *in_data, size_t size)
+{
+  static cookie_io_functions_t io_funcs = { .read = io_read };
+  the_cookie.in_data = in_data;
+  the_cookie.in_data_left = size;
+  the_cookie.in_error = 0;
+  the_cookie.in_error_after = 0;
+  FILE *fp = fopencookie (&the_cookie, "r", io_funcs);
+  TEST_VERIFY_EXIT (fp != NULL);
+  return fp;
+}
+
+/* Set up a test stream with fopencookie, using data from a string
+   literal.  */
+#define OPEN_TEST_STREAM(IN_DATA) open_test_stream (IN_DATA, sizeof (IN_DATA))
+
+int
+do_test (void)
+{
+  FILE *fp;
+  char *lineptr = NULL;
+  size_t size = 0;
+  ssize_t ret;
+  mtrace ();
+  /* Test failure with EINVAL (and error indicator for stream set) if
+     lineptr is a null pointer.  */
+  verbose_printf ("Testing lineptr == NULL\n");
+  fp = OPEN_TEST_STREAM ("test");
+  errno = 0;
+  ret = getline (NULL, &size, fp);
+  TEST_COMPARE (ret, -1);
+  TEST_COMPARE (errno, EINVAL);
+  TEST_COMPARE (!!ferror (fp), 1);
+  TEST_COMPARE (feof (fp), 0);
+  fclose (fp);
+  /* Test failure with EINVAL (and error indicator for stream set) if
+     n is a null pointer.  */
+  verbose_printf ("Testing n == NULL\n");
+  fp = OPEN_TEST_STREAM ("test");
+  errno = 0;
+  ret = getline (&lineptr, NULL, fp);
+  TEST_COMPARE (ret, -1);
+  TEST_COMPARE (errno, EINVAL);
+  TEST_COMPARE (!!ferror (fp), 1);
+  TEST_COMPARE (feof (fp), 0);
+  fclose (fp);
+  /* Test failure with EINVAL (and error indicator for stream set) if
+     both lineptr and n are null pointers.  */
+  verbose_printf ("Testing lineptr == NULL and n == NULL\n");
+  fp = OPEN_TEST_STREAM ("test");
+  errno = 0;
+  ret = getline (NULL, NULL, fp);
+  TEST_COMPARE (ret, -1);
+  TEST_COMPARE (errno, EINVAL);
+  TEST_COMPARE (!!ferror (fp), 1);
+  TEST_COMPARE (feof (fp), 0);
+  fclose (fp);
+  /* Test normal line, fitting in available space (including case with
+     null bytes).  */
+  verbose_printf ("Testing normal nonempty input\n");
+  lineptr = xmalloc (10);
+  size = 10;
+  fp = OPEN_TEST_STREAM ("foo\nbar\0\n\0baz\nte\0st\n");
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 4);
+  TEST_COMPARE_BLOB (lineptr, 5, "foo\n", 5);
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 5);
+  TEST_COMPARE_BLOB (lineptr, 6, "bar\0\n", 6);
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 5);
+  TEST_COMPARE_BLOB (lineptr, 6, "\0baz\n", 6);
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 6);
+  TEST_COMPARE_BLOB (lineptr, 7, "te\0st\n", 7);
+  fclose (fp);
+  /* Test normal line, with reallocation (including case with null bytes).  */
+  verbose_printf ("Testing normal nonempty input with reallocation\n");
+  free (lineptr);
+  lineptr = NULL;
+  size = 0;
+  fp = OPEN_TEST_STREAM ("foo\nbar\0\n\0baz\nte\0st\n"
+			 "foo\nbar\0\n\0baz\nte\0st\n");
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 4);
+  TEST_COMPARE_BLOB (lineptr, 5, "foo\n", 5);
+  free (lineptr);
+  lineptr = NULL;
+  size = 0;
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 5);
+  TEST_COMPARE_BLOB (lineptr, 6, "bar\0\n", 6);
+  free (lineptr);
+  lineptr = NULL;
+  size = 0;
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 5);
+  TEST_COMPARE_BLOB (lineptr, 6, "\0baz\n", 6);
+  free (lineptr);
+  lineptr = NULL;
+  size = 0;
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 6);
+  TEST_COMPARE_BLOB (lineptr, 7, "te\0st\n", 7);
+  free (lineptr);
+  lineptr = xmalloc (1);
+  size = 1;
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 4);
+  TEST_COMPARE_BLOB (lineptr, 5, "foo\n", 5);
+  free (lineptr);
+  lineptr = xmalloc (1);
+  size = 1;
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 5);
+  TEST_COMPARE_BLOB (lineptr, 6, "bar\0\n", 6);
+  free (lineptr);
+  lineptr = xmalloc (1);
+  size = 1;
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 5);
+  TEST_COMPARE_BLOB (lineptr, 6, "\0baz\n", 6);
+  free (lineptr);
+  lineptr = xmalloc (1);
+  size = 1;
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 6);
+  TEST_COMPARE_BLOB (lineptr, 7, "te\0st\n", 7);
+  fclose (fp);
+  /* Test EOF before delimiter but after some bytes read, fitting in
+     available space (including case with null bytes).  */
+  verbose_printf ("Testing EOF before delimiter\n");
+  free (lineptr);
+  lineptr = xmalloc (10);
+  size = 10;
+  fp = open_test_stream ("foo", 3);
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 3);
+  TEST_COMPARE_BLOB (lineptr, 4, "foo", 4);
+  fclose (fp);
+  free (lineptr);
+  lineptr = xmalloc (10);
+  size = 10;
+  fp = open_test_stream ("bar\0", 4);
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 4);
+  TEST_COMPARE_BLOB (lineptr, 5, "bar\0", 5);
+  fclose (fp);
+  free (lineptr);
+  lineptr = xmalloc (10);
+  size = 10;
+  fp = open_test_stream ("\0baz", 4);
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 4);
+  TEST_COMPARE_BLOB (lineptr, 5, "\0baz", 5);
+  fclose (fp);
+  free (lineptr);
+  lineptr = xmalloc (10);
+  size = 10;
+  fp = open_test_stream ("te\0st", 5);
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 5);
+  TEST_COMPARE_BLOB (lineptr, 6, "te\0st", 6);
+  fclose (fp);
+  /* Test EOF before delimiter but after some bytes read, with
+     reallocation (including case with null bytes).  */
+  verbose_printf ("Testing EOF before delimiter with reallocation\n");
+  free (lineptr);
+  lineptr = NULL;
+  size = 0;
+  fp = open_test_stream ("foo", 3);
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 3);
+  TEST_COMPARE_BLOB (lineptr, 4, "foo", 4);
+  fclose (fp);
+  free (lineptr);
+  lineptr = NULL;
+  size = 0;
+  fp = open_test_stream ("bar\0", 4);
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 4);
+  TEST_COMPARE_BLOB (lineptr, 5, "bar\0", 5);
+  fclose (fp);
+  free (lineptr);
+  lineptr = NULL;
+  size = 0;
+  fp = open_test_stream ("\0baz", 4);
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 4);
+  TEST_COMPARE_BLOB (lineptr, 5, "\0baz", 5);
+  fclose (fp);
+  free (lineptr);
+  lineptr = NULL;
+  size = 0;
+  fp = open_test_stream ("te\0st", 5);
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 5);
+  TEST_COMPARE_BLOB (lineptr, 6, "te\0st", 6);
+  fclose (fp);
+  free (lineptr);
+  lineptr = xmalloc (1);
+  size = 1;
+  fp = open_test_stream ("foo", 3);
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 3);
+  TEST_COMPARE_BLOB (lineptr, 4, "foo", 4);
+  fclose (fp);
+  free (lineptr);
+  lineptr = xmalloc (1);
+  size = 1;
+  fp = open_test_stream ("bar\0", 4);
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 4);
+  TEST_COMPARE_BLOB (lineptr, 5, "bar\0", 5);
+  fclose (fp);
+  free (lineptr);
+  lineptr = xmalloc (1);
+  size = 1;
+  fp = open_test_stream ("\0baz", 4);
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 4);
+  TEST_COMPARE_BLOB (lineptr, 5, "\0baz", 5);
+  fclose (fp);
+  free (lineptr);
+  lineptr = xmalloc (1);
+  size = 1;
+  fp = open_test_stream ("te\0st", 5);
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 5);
+  TEST_COMPARE_BLOB (lineptr, 6, "te\0st", 6);
+  fclose (fp);
+  /* Test EOF with no bytes read (nothing is specified about anything
+     written to the buffer), including EOF again when already at end
+     of file.  */
+  verbose_printf ("Testing EOF with no bytes read\n");
+  free (lineptr);
+  lineptr = NULL;
+  size = 0;
+  fp = open_test_stream ("", 0);
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, -1);
+  TEST_COMPARE (ferror (fp), 0);
+  TEST_COMPARE (!!feof (fp), 1);
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, -1);
+  TEST_COMPARE (ferror (fp), 0);
+  TEST_COMPARE (!!feof (fp), 1);
+  fclose (fp);
+  free (lineptr);
+  lineptr = xmalloc (1);
+  size = 1;
+  fp = open_test_stream ("", 0);
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, -1);
+  TEST_COMPARE (ferror (fp), 0);
+  TEST_COMPARE (!!feof (fp), 1);
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, -1);
+  TEST_COMPARE (ferror (fp), 0);
+  TEST_COMPARE (!!feof (fp), 1);
+  fclose (fp);
+  /* Test error occurring with no bytes read, including calling
+     getline again while the file is in error state.  */
+  verbose_printf ("Testing error with no bytes read\n");
+  free (lineptr);
+  lineptr = NULL;
+  size = 0;
+  fp = open_test_stream ("", 0);
+  the_cookie.in_error = EINVAL;
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, -1);
+  TEST_COMPARE (errno, EINVAL);
+  TEST_COMPARE (!!ferror (fp), 1);
+  TEST_COMPARE (feof (fp), 0);
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, -1);
+  TEST_COMPARE (errno, EINVAL);
+  TEST_COMPARE (!!ferror (fp), 1);
+  TEST_COMPARE (feof (fp), 0);
+  fclose (fp);
+  /* Test error occurring after some bytes read.  Specifications are
+     ambiguous here; at least in the fopencookie case used for
+     testing, glibc returns the partial line (but with the error
+     indicator on the stream set).  */
+  verbose_printf ("Testing error after some bytes read\n");
+  free (lineptr);
+  lineptr = NULL;
+  size = 0;
+  fp = open_test_stream ("foo", 3);
+  the_cookie.in_error_after = EINVAL;
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, 3);
+  TEST_COMPARE_BLOB (lineptr, 4, "foo", 4);
+  TEST_COMPARE (errno, EINVAL);
+  TEST_COMPARE (!!ferror (fp), 1);
+  TEST_COMPARE (feof (fp), 0);
+  fclose (fp);
+  /* Test EBADF error as a representative example of an fgetc error
+     resulting in an error from getline.  We don't try to cover all
+     error cases for fgetc here.  */
+  free (lineptr);
+  lineptr = NULL;
+  size = 0;
+  fp = xfopen ("/dev/null", "r");
+  xclose (fileno (fp));
+  ret = getline (&lineptr, &size, fp);
+  TEST_COMPARE (ret, -1);
+  TEST_COMPARE (errno, EBADF);
+  TEST_COMPARE (!!ferror (fp), 1);
+  TEST_COMPARE (feof (fp), 0);
+  fclose (fp);
+  free (lineptr);
+  return 0;
+}
+
+#include <support/test-driver.c>
