From 7ce8b9165d342e705b4554dcc991ec0d41ae36f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Gon=C3=A7alves?= Date: Sun, 21 Jun 2026 22:59:40 -0300 Subject: [PATCH] Accept NULL in free_sized() and free_aligned_sized() free_sized() and free_aligned_sized() forward straight to sdallocx(), which expects a non-NULL pointer and asserts on it in debug builds. C23 says both should accept NULL and do nothing, like free(NULL) does, so a NULL argument either trips that assert or feeds NULL into the dealloc path in release builds. It is not hard to hit. glibc 2.41 ships free_sized()/free_aligned_sized(), and a C++ sized delete of a null pointer compiles down to a free_sized() call. Once jemalloc is preloaded its versions take over, and that NULL call takes down the process. I ran into it with GTK4/GLib apps under LD_PRELOAD. Check for NULL first, the way free() already does, and add an integration test covering the NULL case for both functions. While here, give free_aligned_sized() its own core.free_aligned_sized.entry and .exit logging and call je_sdallocx_impl() directly rather than the je_sdallocx() wrapper, so it mirrors free_sized() and no longer logs under sdallocx. The C++ sized-delete paths (sizedDeleteImpl, alignedSizedDeleteImpl) get the same treatment: log entry/exit unconditionally and guard the call with likely(ptr != nullptr). --- Makefile.in | 1 + src/jemalloc.c | 25 +++++++++++++++++++++-- src/jemalloc_cpp.cpp | 14 ++++++------- test/integration/free_sized.c | 37 +++++++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 10 deletions(-) create mode 100644 test/integration/free_sized.c diff --git a/Makefile.in b/Makefile.in index a70bdf53..c7bab1a2 100644 --- a/Makefile.in +++ b/Makefile.in @@ -331,6 +331,7 @@ endif TESTS_INTEGRATION := $(srcroot)test/integration/aligned_alloc.c \ $(srcroot)test/integration/allocated.c \ $(srcroot)test/integration/extent.c \ + $(srcroot)test/integration/free_sized.c \ $(srcroot)test/integration/malloc.c \ $(srcroot)test/integration/mallocx.c \ $(srcroot)test/integration/MALLOCX_ARENA.c \ diff --git a/src/jemalloc.c b/src/jemalloc.c index 7629b1af..3644fe9a 100644 --- a/src/jemalloc.c +++ b/src/jemalloc.c @@ -1072,14 +1072,35 @@ JEMALLOC_EXPORT void JEMALLOC_NOTHROW je_free_sized(void *ptr, size_t size) { LOG("core.free_sized.entry", "ptr: %p, size: %zu", ptr, size); - je_sdallocx_noflags(ptr, size); + /* + * free_sized(NULL, size) is a no-op, like free(NULL). Do the NULL + * check here rather than deferring to the je_free_impl() fallback: + * without a size hint, free_fastpath() does an emap lookup that NULL + * fails, so it reaches free_default(), which handles NULL. With a + * size hint, the fast path skips that lookup and falls through to + * sdallocx_default(), which expects a non-NULL pointer. + */ + if (likely(ptr != NULL)) { + je_sdallocx_noflags(ptr, size); + } LOG("core.free_sized.exit", ""); } JEMALLOC_EXPORT void JEMALLOC_NOTHROW je_free_aligned_sized(void *ptr, size_t alignment, size_t size) { - je_sdallocx(ptr, size, /* flags */ MALLOCX_ALIGN(alignment)); + LOG("core.free_aligned_sized.entry", + "ptr: %p, alignment: %zu, size: %zu", ptr, alignment, size); + + /* + * Same as je_free_sized() above: the sdallocx path expects a non-NULL + * pointer, so handle the C23 free_aligned_sized(NULL, ...) no-op here. + */ + if (likely(ptr != NULL)) { + je_sdallocx_impl(ptr, size, /* flags */ MALLOCX_ALIGN(alignment)); + } + + LOG("core.free_aligned_sized.exit", ""); } /* diff --git a/src/jemalloc_cpp.cpp b/src/jemalloc_cpp.cpp index dff67150..a0fe92ca 100644 --- a/src/jemalloc_cpp.cpp +++ b/src/jemalloc_cpp.cpp @@ -231,12 +231,11 @@ operator delete[](void *ptr, const std::nothrow_t &) noexcept { JEMALLOC_ALWAYS_INLINE void sizedDeleteImpl(void *ptr, std::size_t size) noexcept { - if (unlikely(ptr == nullptr)) { - return; - } LOG("core.operator_delete.entry", "ptr: %p, size: %zu", ptr, size); - je_sdallocx_noflags(ptr, size); + if (likely(ptr != nullptr)) { + je_sdallocx_noflags(ptr, size); + } LOG("core.operator_delete.exit", ""); } @@ -262,13 +261,12 @@ alignedSizedDeleteImpl( if (config_debug) { assert(((size_t)alignment & ((size_t)alignment - 1)) == 0); } - if (unlikely(ptr == nullptr)) { - return; - } LOG("core.operator_delete.entry", "ptr: %p, size: %zu, alignment: %zu", ptr, size, alignment); - je_sdallocx_impl(ptr, size, MALLOCX_ALIGN(alignment)); + if (likely(ptr != nullptr)) { + je_sdallocx_impl(ptr, size, MALLOCX_ALIGN(alignment)); + } LOG("core.operator_delete.exit", ""); } diff --git a/test/integration/free_sized.c b/test/integration/free_sized.c new file mode 100644 index 00000000..a97d7ad8 --- /dev/null +++ b/test/integration/free_sized.c @@ -0,0 +1,37 @@ +#include "test/jemalloc_test.h" + +TEST_BEGIN(test_free_sized) { + void *p = mallocx(42, 0); + expect_ptr_not_null(p, "Unexpected mallocx() failure"); + free_sized(p, 42); +} +TEST_END + +TEST_BEGIN(test_free_aligned_sized) { + size_t alignment = 0x100; + void *p = mallocx(42, MALLOCX_ALIGN(alignment)); + expect_ptr_not_null(p, "Unexpected mallocx() failure"); + free_aligned_sized(p, alignment, 42); +} +TEST_END + +TEST_BEGIN(test_free_sized_null) { + /* + * C23 specifies that free_sized(NULL, size) and + * free_aligned_sized(NULL, alignment, size) do nothing, just as + * free(NULL) does. The size argument is ignored for a NULL pointer. + */ + free_sized(NULL, 0); + free_sized(NULL, 42); + free_aligned_sized(NULL, 0x100, 0); + free_aligned_sized(NULL, 0x100, 42); +} +TEST_END + +int +main(void) { + return test( + test_free_sized, + test_free_aligned_sized, + test_free_sized_null); +}