[v2] malloc:New test to check malloc alternate path using memory obstruction

Message ID 20240521151039.174595-1-saypaul@redhat.com
State Changes Requested
Headers
Series [v2] malloc:New test to check malloc alternate path using memory obstruction |

Checks

Context Check Description
redhat-pt-bot/TryBot-apply_patch fail Patch failed to apply to master at the time it was sent
redhat-pt-bot/TryBot-32bit fail Patch series failed to apply

Commit Message

sayan paul May 21, 2024, 3:10 p.m. UTC
  The test aims to ensure that malloc uses the alternate path to
allocate memory when sbrk() or brk() fails.To achieve this,
the test first creates an obstruction at current program break,
tests that obstruction with a failing sbrk(),then checks if malloc
is still returning a valid ptr thus inferring that malloc() used
mmap() instead of brk() or sbrk() to allocate the memory.
---
Changes as suggested by Zack:
* corrected email address
* simplified the approach by creating the obstruction in the test
* programme directly
---
 malloc/Makefile                    |   2 +
 malloc/tst-malloc-alternate-path.c | 100 +++++++++++++++++++++++++++++
 2 files changed, 102 insertions(+)
 create mode 100644 malloc/tst-malloc-alternate-path.c
  

Comments

Zack Weinberg May 22, 2024, 2:44 p.m. UTC | #1
On Tue, May 21, 2024, at 11:10 AM, sayan paul wrote:
> The test aims to ensure that malloc uses the alternate path to
> allocate memory when sbrk() or brk() fails.To achieve this,
> the test first creates an obstruction at current program break,
> tests that obstruction with a failing sbrk(),then checks if malloc
> is still returning a valid ptr thus inferring that malloc() used
> mmap() instead of brk() or sbrk() to allocate the memory.

This is definitely an improvement on the first version you sent.
Now that it is easier to understand what the test does, I have a
few more suggestions.

> @@ -408,3 +409,4 @@ tst-mallocstate-malloc-check-ENV = 
> LD_PRELOAD=$(objpfx)libc_malloc_debug.so
>  # libc_malloc_debug.so.
>  $(objpfx)tst-mallocstate: $(objpfx)libc_malloc_debug.so
>  $(objpfx)tst-mallocstate-malloc-check: $(objpfx)libc_malloc_debug.so
> +

This addition of a blank line at the end of the Makefile looks like
an editing mistake, please remove it again.

> --- /dev/null
> +++ b/malloc/tst-malloc-alternate-path.c
> @@ -0,0 +1,100 @@
> +/* Test that malloc uses mmap when sbrk or brk fails.
> +   This code returns success when there is an obstruction setup
> +   and sbrk() fails to grow the heap size forcing malloc to use mmap().
> +   Copyright (C) 2024 Free Software Foundation, Inc.
> +   This file is part of the GNU C Library.

Very minor, but the explanation on line one of what the file does
is supposed to be *exactly one line*.  Move the second sentence to a
separate comment below the copyright boilerplate, please.  This might
not be just cosmetic; I don't know if the script for updating copyright
years does the right thing if the 'Copyright (C)' isn't on line two.

...
> +  /* Get current program break */
...
> +  /* Get the runtime page size */
...
> +  /* Round up to the next page boundary */
...
> +  /* Place a mapping using mmap at the next page boundary */
> +  void *obstruction_addr
> +      = mmap (next_page_boundary, page_size, PROT_WRITE | PROT_READ,
> +	      MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);

Suggest using PROT_NONE, or PROT_READ alone, here, so that a malloc
implementation that tries to write into the obstruction will crash.

> +  if (obstruction_addr == MAP_FAILED)
> +    {
> +      perror ("mmap");
> +      return 1;

Isn't there a special exit code to distinguish "the test itself
malfunctioned" from "the test has failed"?  Returning 99 instead of 1
or something like that?

> +  /* Try to extend the heap beyond the obstruction using sbrk */
> +  int *ptr = (int *) sbrk (sizeof (int));
> +  if (ptr != (void *) -1)

Okay, here's the main thing I wanted to point out.  Did you know that
the break address is *not* necessarily aligned to a page boundary?
The kernel doesn't enforce any alignment at all, in fact.  You
correctly handle this possibility earlier in the code, rounding the
value returned by sbrk(0) up to the nearest page to use as the address
for the mmap obstruction.  But here, if the break address happened to
be in the middle of a page, the attempt to advance it by sizeof(int)
would probably *succeed* despite the obstruction at the next page
boundary.

Our malloc always asks sbrk() for a large chunk of memory that's a
multiple of the page size, so it won't come up as long as this test is
only used to test our malloc.  But, alternative malloc implementations
might move the break in small increments, and our malloc *testsuite*
should, as much as possible, be usable with alternative implementations.
This is both because we might change out our malloc implementation
someday, and because people *writing* malloc implementations might
like to steal our testsuite.

So, what you should be doing here is allocating an entire page:

    void *ptr = sbrk (page_size)

That will reliably hit the obstruction.

> +    {
> +      fprintf (stderr, "memory allocation can be done using sbrk.\n");
> +      free (ptr);
> +      return 1;
> +    }

ptr here was received directly from sbrk(), so passing it to free() is
incorrect.  The next line exits the process, so there's no need to
avoid a memory leak - just drop the free.

> +  /* Check if malloc changed program break */
> +  if (current_brk != new_brk)
> +    {
> +      fprintf (stderr, "malloc changed program break\n");
> +      free (memptr);
> +      return 1;
> +    }
> +  free (memptr);
> +
> +  /* Free the obstruction mapping */
> +  if (munmap (obstruction_addr, page_size) == -1)
> +    {
> +      perror ("munmap");
> +    }
> +
> +  return 0;
> +}

Similarly, I would just have

    if (current_brk != new_brk)
      {
        fputs ("malloc changed program break\n", stderr);
        return 1;
      }
    return 0;
  }

as the last few lines.

Finally, I think it would be a a good idea to test doing a bunch of
*small* allocations with malloc, as well as the one allocation of
LARGE_SIZE that you do have.  It's possible that the test is
succeeding only because LARGE_SIZE is large enough to be allocated
with mmap regardless.  Something like

  for (size_t i = 0; i < page_size / alignof (max_align_t); i++)
    {
      // intentional memory leak on next line
      if (! malloc (alignof (max_align_t)))
        {
          perror ("malloc [small]");
          return 1;
        }
    }

This should be first, and the large allocation second.  That way, if
the test fails spuriously because someone set the memory rlimits too
low, it will be easier to figure out that that's the problem.

zw
  
Arjun Shankar May 22, 2024, 10:04 p.m. UTC | #2
> > +  if (obstruction_addr == MAP_FAILED)
> > +    {
> > +      perror ("mmap");
> > +      return 1;
>
> Isn't there a special exit code to distinguish "the test itself
> malfunctioned" from "the test has failed"?  Returning 99 instead of 1
> or something like that?

I don't think there's a way to explicitly indicate test rig failures,
but one change that could still be made for test assertions here is to
use the (relatively) new way we make test assertions using
support/check.h.

e.g. the above block can reasonably be replaced with the single line
"TEST_VERIFY_EXIT (obstruction_addr != MAP_FAILED);". The assertion
and line number information this macro prints upon failure can
possibly make it easier to recognize the cause of a test failure. It
looks like both consecutive checks against obstruction_addr can
possibly be replaced with a single check directly against the result
of mmap, i.e. "TEST_VERIFY_EXIT (mmap (...) == next_page_boundary);".
There is also TEST_VERIFY that can log errors (and guarantees test
failure at the end of execution) but lets the test program continue
executing.
  

Patch

diff --git a/malloc/Makefile b/malloc/Makefile
index 77ba1a9109..ed666e2132 100644
--- a/malloc/Makefile
+++ b/malloc/Makefile
@@ -33,6 +33,7 @@  tests := \
   tst-interpose-nothread \
   tst-interpose-thread \
   tst-malloc \
+  tst-malloc-alternate-path \
   tst-malloc-backtrace \
   tst-malloc-check \
   tst-malloc-fork-deadlock \
@@ -408,3 +409,4 @@  tst-mallocstate-malloc-check-ENV = LD_PRELOAD=$(objpfx)libc_malloc_debug.so
 # libc_malloc_debug.so.
 $(objpfx)tst-mallocstate: $(objpfx)libc_malloc_debug.so
 $(objpfx)tst-mallocstate-malloc-check: $(objpfx)libc_malloc_debug.so
+
diff --git a/malloc/tst-malloc-alternate-path.c b/malloc/tst-malloc-alternate-path.c
new file mode 100644
index 0000000000..fdb0ec5a7c
--- /dev/null
+++ b/malloc/tst-malloc-alternate-path.c
@@ -0,0 +1,100 @@ 
+/* Test that malloc uses mmap when sbrk or brk fails.
+   This code returns success when there is an obstruction setup
+   and sbrk() fails to grow the heap size forcing malloc to use mmap().
+   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 <stdio.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <errno.h>
+#include <string.h>
+#include <stdlib.h>
+#include <libc-pointer-arith.h>
+
+#define LARGE_SIZE (10 * (1 << 20)) // 10 MB
+static long page_size;
+
+static int
+do_test (void)
+{
+  /* Get current program break */
+  void *current_brk = sbrk (0);
+
+  /* Get the runtime page size */
+  page_size = sysconf (_SC_PAGESIZE);
+
+  /* Round up to the next page boundary */
+  void *next_page_boundary = PTR_ALIGN_UP (current_brk, page_size);
+
+  /* Place a mapping using mmap at the next page boundary */
+  void *obstruction_addr
+      = mmap (next_page_boundary, page_size, PROT_WRITE | PROT_READ,
+	      MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
+  if (obstruction_addr == MAP_FAILED)
+    {
+      perror ("mmap");
+      return 1;
+    }
+
+  if (obstruction_addr != next_page_boundary)
+    {
+      fprintf (stderr, "memory obstruction not setup correctly!");
+      return 1;
+    }
+
+  /* Try to extend the heap beyond the obstruction using sbrk */
+  int *ptr = (int *) sbrk (sizeof (int));
+  if (ptr != (void *) -1)
+    {
+      fprintf (stderr, "memory allocation can be done using sbrk.\n");
+      free (ptr);
+      return 1;
+    }
+
+  /* Attempt to allocate memory using malloc */
+  void *memptr = malloc (LARGE_SIZE);
+  if (memptr == NULL)
+    {
+      perror ("malloc");
+      return 1;
+    }
+
+  printf ("malloc used alternate path to allocate memory\n");
+
+  /* Get program break after malloc */
+  void *new_brk = sbrk (0);
+
+  /* Check if malloc changed program break */
+  if (current_brk != new_brk)
+    {
+      fprintf (stderr, "malloc changed program break\n");
+      free (memptr);
+      return 1;
+    }
+  free (memptr);
+
+  /* Free the obstruction mapping */
+  if (munmap (obstruction_addr, page_size) == -1)
+    {
+      perror ("munmap");
+    }
+
+  return 0;
+}
+
+#include <support/test-driver.c>