diff --git a/Makefile.in b/Makefile.in
index e4d21805..fede961d 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -235,7 +235,8 @@ TESTS_UNIT := \
$(srcroot)test/unit/zero.c \
$(srcroot)test/unit/zero_realloc_abort.c \
$(srcroot)test/unit/zero_realloc_free.c \
- $(srcroot)test/unit/zero_realloc_strict.c
+ $(srcroot)test/unit/zero_realloc_strict.c \
+ $(srcroot)test/unit/zero_reallocs.c
ifeq (@enable_prof@, 1)
TESTS_UNIT += \
$(srcroot)test/unit/arena_reset_prof.c
diff --git a/doc/jemalloc.xml.in b/doc/jemalloc.xml.in
index 746c6bdb..77afb00c 100644
--- a/doc/jemalloc.xml.in
+++ b/doc/jemalloc.xml.in
@@ -2451,6 +2451,21 @@ struct extent_hooks_s {
+
+
+ stats.zero_reallocs
+ (size_t)
+ r-
+ []
+
+ Number of times that the realloc()
+ was called with a non-NULL pointer argument and a
+ 0 size argument. This is a fundamentally unsafe
+ pattern in portable programs; see
+ opt.zero_realloc for details.
+
+
+
stats.background_thread.num_threads
diff --git a/include/jemalloc/internal/jemalloc_internal_externs.h b/include/jemalloc/internal/jemalloc_internal_externs.h
index dae77b42..e9dbde80 100644
--- a/include/jemalloc/internal/jemalloc_internal_externs.h
+++ b/include/jemalloc/internal/jemalloc_internal_externs.h
@@ -20,6 +20,7 @@ extern bool opt_zero;
extern unsigned opt_narenas;
extern zero_realloc_action_t opt_zero_realloc_action;
extern const char *zero_realloc_mode_names[];
+extern atomic_zu_t zero_realloc_count;
/* Number of CPUs. */
extern unsigned ncpus;
diff --git a/src/ctl.c b/src/ctl.c
index b51207f8..abb82b57 100644
--- a/src/ctl.c
+++ b/src/ctl.c
@@ -224,6 +224,7 @@ CTL_PROTO(stats_metadata_thp)
CTL_PROTO(stats_resident)
CTL_PROTO(stats_mapped)
CTL_PROTO(stats_retained)
+CTL_PROTO(stats_zero_reallocs)
CTL_PROTO(experimental_hooks_install)
CTL_PROTO(experimental_hooks_remove)
CTL_PROTO(experimental_utilization_query)
@@ -593,7 +594,8 @@ static const ctl_named_node_t stats_node[] = {
{NAME("background_thread"),
CHILD(named, stats_background_thread)},
{NAME("mutexes"), CHILD(named, stats_mutexes)},
- {NAME("arenas"), CHILD(indexed, stats_arenas)}
+ {NAME("arenas"), CHILD(indexed, stats_arenas)},
+ {NAME("zero_reallocs"), CTL(stats_zero_reallocs)},
};
static const ctl_named_node_t experimental_hooks_node[] = {
@@ -2841,6 +2843,9 @@ CTL_RO_CGEN(config_stats, stats_background_thread_num_runs,
CTL_RO_CGEN(config_stats, stats_background_thread_run_interval,
nstime_ns(&ctl_stats->background_thread.run_interval), uint64_t)
+CTL_RO_CGEN(config_stats, stats_zero_reallocs,
+ atomic_load_zu(&zero_realloc_count, ATOMIC_RELAXED), size_t)
+
CTL_RO_GEN(stats_arenas_i_dss, arenas_i(mib[2])->dss, const char *)
CTL_RO_GEN(stats_arenas_i_dirty_decay_ms, arenas_i(mib[2])->dirty_decay_ms,
ssize_t)
diff --git a/src/jemalloc.c b/src/jemalloc.c
index 35a9e7b5..88064df4 100644
--- a/src/jemalloc.c
+++ b/src/jemalloc.c
@@ -70,6 +70,8 @@ bool opt_junk_free =
zero_realloc_action_t opt_zero_realloc_action =
zero_realloc_action_strict;
+atomic_zu_t zero_realloc_count = ATOMIC_INIT(0);
+
const char *zero_realloc_mode_names[] = {
"strict",
"free",
@@ -3160,6 +3162,9 @@ je_rallocx(void *ptr, size_t size, int flags) {
static void *
do_realloc_nonnull_zero(void *ptr) {
+ if (config_stats) {
+ atomic_fetch_add_zu(&zero_realloc_count, 1, ATOMIC_RELAXED);
+ }
if (opt_zero_realloc_action == zero_realloc_action_strict) {
/*
* The user might have gotten a strict setting while expecting a
diff --git a/src/stats.c b/src/stats.c
index c9bab4f7..41b990e2 100644
--- a/src/stats.c
+++ b/src/stats.c
@@ -1252,6 +1252,7 @@ stats_print_helper(emitter_t *emitter, bool merged, bool destroyed,
size_t allocated, active, metadata, metadata_thp, resident, mapped,
retained;
size_t num_background_threads;
+ size_t zero_reallocs;
uint64_t background_thread_num_runs, background_thread_run_interval;
CTL_GET("stats.allocated", &allocated, size_t);
@@ -1262,6 +1263,8 @@ stats_print_helper(emitter_t *emitter, bool merged, bool destroyed,
CTL_GET("stats.mapped", &mapped, size_t);
CTL_GET("stats.retained", &retained, size_t);
+ CTL_GET("stats.zero_reallocs", &zero_reallocs, size_t);
+
if (have_background_thread) {
CTL_GET("stats.background_thread.num_threads",
&num_background_threads, size_t);
@@ -1285,12 +1288,18 @@ stats_print_helper(emitter_t *emitter, bool merged, bool destroyed,
emitter_json_kv(emitter, "resident", emitter_type_size, &resident);
emitter_json_kv(emitter, "mapped", emitter_type_size, &mapped);
emitter_json_kv(emitter, "retained", emitter_type_size, &retained);
+ emitter_json_kv(emitter, "zero_reallocs", emitter_type_size,
+ &zero_reallocs);
emitter_table_printf(emitter, "Allocated: %zu, active: %zu, "
"metadata: %zu (n_thp %zu), resident: %zu, mapped: %zu, "
"retained: %zu\n", allocated, active, metadata, metadata_thp,
resident, mapped, retained);
+ /* Strange behaviors */
+ emitter_table_printf(emitter,
+ "Count of realloc(non-null-ptr, 0) calls: %zu\n", zero_reallocs);
+
/* Background thread stats. */
emitter_json_object_kv_begin(emitter, "background_thread");
emitter_json_kv(emitter, "num_threads", emitter_type_size,
diff --git a/test/unit/zero_reallocs.c b/test/unit/zero_reallocs.c
new file mode 100644
index 00000000..fd33aaf6
--- /dev/null
+++ b/test/unit/zero_reallocs.c
@@ -0,0 +1,40 @@
+#include "test/jemalloc_test.h"
+
+static size_t
+zero_reallocs() {
+ if (!config_stats) {
+ return 0;
+ }
+ size_t count = 12345;
+ size_t sz = sizeof(count);
+
+ assert_d_eq(mallctl("stats.zero_reallocs", (void *)&count, &sz,
+ NULL, 0), 0, "Unexpected mallctl failure");
+ return count;
+}
+
+TEST_BEGIN(test_zero_reallocs) {
+ test_skip_if(!config_stats);
+
+ for (size_t i = 0; i < 100; ++i) {
+ void *ptr = mallocx(i * i + 1, 0);
+ assert_ptr_not_null(ptr, "Unexpected mallocx error");
+ size_t count = zero_reallocs();
+ assert_zu_eq(i, count, "Incorrect zero realloc count");
+ ptr = realloc(ptr, 0);
+ assert_ptr_null(ptr, "Realloc didn't free");
+ count = zero_reallocs();
+ assert_zu_eq(i + 1, count, "Realloc didn't adjust count");
+ }
+}
+TEST_END
+
+int
+main(void) {
+ /*
+ * We expect explicit counts; reentrant tests run multiple times, so
+ * counts leak across runs.
+ */
+ return test_no_reentrancy(
+ test_zero_reallocs);
+}
diff --git a/test/unit/zero_reallocs.sh b/test/unit/zero_reallocs.sh
new file mode 100644
index 00000000..51b01c91
--- /dev/null
+++ b/test/unit/zero_reallocs.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+export MALLOC_CONF="zero_realloc:free"