[RFC] Supporting malloc_usable_size

Siddhesh Poyarekar siddhesh@gotplt.org
Thu Nov 24 21:32:58 GMT 2022


Hello,

This is in context of this systemd issue:

https://github.com/systemd/systemd/issues/22801

through which I had discovered that systemd was (ab)using
malloc_usable_size to use spare space in an allocated object.  This was
discovered when _FORTIFY_SOURCE=3 flagged this as a buffer overflow,
since the compiler is unable to see that the space beyond the allocation
was safe to use.

At that time, I thought this use was to avoid reallocating[1] when
there's enough space in the usable size and that is not entirely false.
I had then proposed during my Cauldron talk (and in discussions with
some of you) that maybe providing a fast path for cases where the new
size was within the current chunk size was the solution to the problem
systemd was trying to solve with malloc_usable_size.

However I was wrong, it looks like the malloc_usable_size usage goes far
beyond simply optimizing allocation, it is used as a way to query sizes
of objects and hence eliminating the need to pass their corresponding
sizes.  This is probably suboptimal (given that there's a call into
libc.so at every point instead of what could have been just an
additional parameter alongside the object) but it's the design pattern
they've chosen.

I have an idea to support this abuse and at the same time, satisfy the
compiler by giving it a hint of the new size.  This could be done within
systemd by calling a dummy allocator (e.g. expand_to_usable) with the
result of malloc_usable_size(), thus telling the compiler that the
object has been expanded to the usable size, thus making accesses into
the spare space well defined.  I have a patch too (see below) to systemd
that demonstrates how this could be achieved, it's been tested with gcc
as well as clang and with _FORTIFY_SOURCE=3.

The larger consequence of this patch though is that we further support
the usage of malloc_usable_size for cases beyond diagnostics.  Do we
want to do that?  If we do, should we also then make clear what kind of
usage we support as a library, say, in the manual?  We don't have a
manual blurb for malloc_usable_size, so maybe this is a good opportunity
to do that.

Any other thoughts on malloc_usable_size?

Thanks,
Sid


[1] https://developers.redhat.com/articles/2022/09/17/gccs-new-fortification-level#the_gains_of_improved_security_coverage_outweigh_the_cost

---
 src/basic/alloc-util.c     |  5 +++++
 src/basic/alloc-util.h     | 16 ++++++++++++++--
 src/test/test-alloc-util.c |  5 +++--
 3 files changed, 22 insertions(+), 4 deletions(-)

diff --git a/src/basic/alloc-util.c b/src/basic/alloc-util.c
index b030f454b2..58cf7c7433 100644
--- a/src/basic/alloc-util.c
+++ b/src/basic/alloc-util.c
@@ -102,3 +102,8 @@ void* greedy_realloc0(
 
         return q;
 }
+
+void *expand_to_usable (void *ptr, size_t newsize _unused_)
+{
+        return ptr;
+}
diff --git a/src/basic/alloc-util.h b/src/basic/alloc-util.h
index b38db7d473..0c0b4851f5 100644
--- a/src/basic/alloc-util.h
+++ b/src/basic/alloc-util.h
@@ -4,6 +4,7 @@
 #include <alloca.h>
 #include <stddef.h>
 #include <stdlib.h>
+#include <malloc.h>
 #include <string.h>
 
 #include "macro.h"
@@ -184,6 +185,18 @@ void* greedy_realloc0(void **p, size_t need, size_t size);
 #  define msan_unpoison(r, s)
 #endif
 
+void *expand_to_usable (void *ptr, size_t newsize) _alloc_ (2);
+
+static inline size_t
+_malloc_usable_size(void **ptr)
+{
+        if (*ptr == NULL)
+                return 0;
+        size_t full_size = malloc_usable_size (*ptr);
+        *ptr = expand_to_usable (*ptr, full_size);
+        return full_size;
+}
+
 /* This returns the number of usable bytes in a malloc()ed region as per malloc_usable_size(), in a way that
  * is compatible with _FORTIFY_SOURCES. If _FORTIFY_SOURCES is used many memory operations will take the
  * object size as returned by __builtin_object_size() into account. Hence, let's return the smaller size of
@@ -193,8 +206,7 @@ void* greedy_realloc0(void **p, size_t need, size_t size);
  * too. Moreover, when NULL is passed malloc_usable_size() is documented to return zero, and
  * __builtin_object_size() returns SIZE_MAX too, hence we also return a sensible value of 0 in this corner
  * case. */
-#define MALLOC_SIZEOF_SAFE(x) \
-        MIN(malloc_usable_size(x), __builtin_object_size(x, 0))
+#define MALLOC_SIZEOF_SAFE(x) _malloc_usable_size((void **) &(x))
 
 /* Inspired by ELEMENTSOF() but operates on malloc()'ed memory areas: typesafely returns the number of items
  * that fit into the specified memory block */
diff --git a/src/test/test-alloc-util.c b/src/test/test-alloc-util.c
index df6139005f..f9fd192576 100644
--- a/src/test/test-alloc-util.c
+++ b/src/test/test-alloc-util.c
@@ -170,8 +170,9 @@ TEST(malloc_size_safe) {
         size_t n = 4711;
 
         /* Let's check the macros and built-ins work on NULL and return the expected values */
-        assert_se(MALLOC_ELEMENTSOF((float*) NULL) == 0);
-        assert_se(MALLOC_SIZEOF_SAFE((float*) NULL) == 0);
+        float *nullptr = NULL;
+        assert_se(MALLOC_ELEMENTSOF(nullptr) == 0);
+        assert_se(MALLOC_SIZEOF_SAFE(nullptr) == 0);
         assert_se(malloc_usable_size(NULL) == 0); /* as per man page, this is safe and defined */
         assert_se(__builtin_object_size(NULL, 0) == SIZE_MAX); /* as per docs SIZE_MAX is returned for pointers where the size isn't known */
 
-- 
2.38.1



More information about the Libc-alpha mailing list