[compiler-rt] [compiler-rt][memprof] clear histogram tail granule on allocation. (PR #208911)
David CARLIER via llvm-commits
llvm-commits at lists.llvm.org
Thu Jul 16 11:48:48 PDT 2026
https://github.com/devnexen updated https://github.com/llvm/llvm-project/pull/208911
>From 35a450f0eb31ad97693a8e81fd77dbf78dc8acfa Mon Sep 17 00:00:00 2001
From: David Carlier <devnexen at gmail.com>
Date: Sat, 11 Jul 2026 14:11:45 +0100
Subject: [PATCH 1/3] [compiler-rt][memprof] clear histogram tail granule on
allocation.
ClearShadow rounded the size down, leaving the partial tail counter
(ceil(size/8)-th) uncleared, so a recycled chunk leaked a stale count
into the last histogram bucket. Round up instead.
---
compiler-rt/lib/memprof/memprof_allocator.cpp | 5 +--
.../memprof_histogram_tail_clear.cpp | 44 +++++++++++++++++++
2 files changed, 45 insertions(+), 4 deletions(-)
create mode 100644 compiler-rt/test/memprof/TestCases/memprof_histogram_tail_clear.cpp
diff --git a/compiler-rt/lib/memprof/memprof_allocator.cpp b/compiler-rt/lib/memprof/memprof_allocator.cpp
index d11a909a02c5e..4ddf8dfcf06ab 100644
--- a/compiler-rt/lib/memprof/memprof_allocator.cpp
+++ b/compiler-rt/lib/memprof/memprof_allocator.cpp
@@ -457,10 +457,7 @@ struct Allocator {
m->timestamp_ms = GetTimestamp();
m->alloc_context_id = StackDepotPut(*stack);
- uptr size_rounded_down_to_granularity =
- RoundDownTo(size, SHADOW_GRANULARITY);
- if (size_rounded_down_to_granularity)
- ClearShadow(user_beg, size_rounded_down_to_granularity);
+ ClearShadow(user_beg, RoundUpTo(size, SHADOW_GRANULARITY));
MemprofStats &thread_stats = GetCurrentThreadStats();
thread_stats.mallocs++;
diff --git a/compiler-rt/test/memprof/TestCases/memprof_histogram_tail_clear.cpp b/compiler-rt/test/memprof/TestCases/memprof_histogram_tail_clear.cpp
new file mode 100644
index 0000000000000..119be97b76839
--- /dev/null
+++ b/compiler-rt/test/memprof/TestCases/memprof_histogram_tail_clear.cpp
@@ -0,0 +1,44 @@
+// A non-granule-multiple allocation owns a partial tail histogram counter that
+// must be cleared on allocation; otherwise a recycled chunk leaks the previous
+// allocation's count into the last bucket.
+
+// RUN: %clangxx_memprof -O0 -mllvm -memprof-histogram -mllvm -memprof-use-callbacks=true %s -o %t
+// RUN: %env_memprof_opts=print_text=1:histogram=1:log_path=stdout %run %t 2>&1 | FileCheck %s
+
+#include <stdio.h>
+#include <stdlib.h>
+
+// Distinct call sites keep the two allocations as separate (unmerged) MIBs.
+__attribute__((noinline)) static char *alloc_first() {
+ return (char *)malloc(20);
+}
+__attribute__((noinline)) static char *alloc_second() {
+ return (char *)malloc(20);
+}
+
+int main() {
+ // Leave a count of 42 in the tail granule (byte 16), then free.
+ char *a = alloc_first();
+ if (!a)
+ return 1;
+ for (int i = 0; i < 42; ++i)
+ a[16] = 'A';
+ free(a);
+
+ // Reuse the chunk without touching the tail; its bucket must read 0.
+ char *b = alloc_second();
+ if (!b)
+ return 1;
+ for (int i = 0; i < 5; ++i)
+ b[0] = 'B';
+ for (int i = 0; i < 7; ++i)
+ b[8] = 'C';
+ free(b);
+
+ printf("Test completed successfully\n");
+ return 0;
+}
+
+// CHECK: AccessCountHistogram[3]: 5 7 0
+// CHECK-NOT: AccessCountHistogram[3]: 5 7 42
+// CHECK: Test completed successfully
>From cc02d991c375c26c678039e63c5c89958eece002 Mon Sep 17 00:00:00 2001
From: David Carlier <devnexen at gmail.com>
Date: Tue, 14 Jul 2026 19:33:24 +0100
Subject: [PATCH 2/3] update test
---
.../memprof/TestCases/memprof_histogram_tail_clear.cpp | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/compiler-rt/test/memprof/TestCases/memprof_histogram_tail_clear.cpp b/compiler-rt/test/memprof/TestCases/memprof_histogram_tail_clear.cpp
index 119be97b76839..9bb7226b39d78 100644
--- a/compiler-rt/test/memprof/TestCases/memprof_histogram_tail_clear.cpp
+++ b/compiler-rt/test/memprof/TestCases/memprof_histogram_tail_clear.cpp
@@ -25,9 +25,12 @@ int main() {
a[16] = 'A';
free(a);
- // Reuse the chunk without touching the tail; its bucket must read 0.
+ // The stale-tail bug only manifests on a recycled chunk (a fresh chunk's
+ // shadow is already zero). memprof has no quarantine, so with no intervening
+ // same-size-class allocation the allocator returns the just-freed chunk;
+ // require the reuse so the test can't pass vacuously. Its bucket must read 0.
char *b = alloc_second();
- if (!b)
+ if (b != a)
return 1;
for (int i = 0; i < 5; ++i)
b[0] = 'B';
@@ -40,5 +43,4 @@ int main() {
}
// CHECK: AccessCountHistogram[3]: 5 7 0
-// CHECK-NOT: AccessCountHistogram[3]: 5 7 42
// CHECK: Test completed successfully
>From 3a969bedd090d36f99f3cd24053f57879c0d16a0 Mon Sep 17 00:00:00 2001
From: David Carlier <devnexen at gmail.com>
Date: Thu, 16 Jul 2026 19:48:01 +0100
Subject: [PATCH 3/3] [compiler-rt][memprof] add test for the non-histogram
case.
---
.../memprof_nonhistogram_tail_clear.cpp | 40 +++++++++++++++++++
1 file changed, 40 insertions(+)
create mode 100644 compiler-rt/test/memprof/TestCases/memprof_nonhistogram_tail_clear.cpp
diff --git a/compiler-rt/test/memprof/TestCases/memprof_nonhistogram_tail_clear.cpp b/compiler-rt/test/memprof/TestCases/memprof_nonhistogram_tail_clear.cpp
new file mode 100644
index 0000000000000..95d1128748f05
--- /dev/null
+++ b/compiler-rt/test/memprof/TestCases/memprof_nonhistogram_tail_clear.cpp
@@ -0,0 +1,40 @@
+// The non-histogram shadow keeps one counter per MEM_GRANULARITY block. An
+// allocation smaller than SHADOW_GRANULARITY used to round its size down to 0,
+// which skipped ClearShadow entirely, so a recycled chunk reported the previous
+// allocation's access count.
+
+// RUN: %clangxx_memprof -O0 -mllvm -memprof-use-callbacks=true %s -o %t
+// RUN: %env_memprof_opts=print_text=1:log_path=stdout %run %t 2>&1 | FileCheck %s
+
+#include <stdio.h>
+#include <stdlib.h>
+
+int main() {
+ char *first = nullptr;
+ // A single call site, so both allocations merge into one MIB.
+ for (int i = 0; i < 2; ++i) {
+ char *p = (char *)malloc(4);
+ if (!p)
+ return 1;
+ if (i == 0) {
+ first = p;
+ for (int j = 0; j < 42; ++j)
+ p[0] = 'A';
+ } else if (p != first) {
+ // The stale count only manifests on a recycled chunk. memprof has no
+ // quarantine, so with no intervening allocation the just-freed chunk
+ // comes back; require it so the test cannot pass vacuously.
+ return 1;
+ }
+ free(p);
+ }
+
+ printf("Test completed successfully\n");
+ return 0;
+}
+
+// The second allocation never touches the chunk, so it must contribute 0:
+// 42 accesses over 2 allocations gives an average of 21.
+// CHECK: access_count (ave/min/max): 21.00 / 0 / 42
+// CHECK-NOT: access_count (ave/min/max): 42.00 / 42 / 42
+// CHECK: Test completed successfully
More information about the llvm-commits
mailing list