* [PATCH v3 00/10] stackdepot: store compressed frames in stack records
@ 2026-11-13 18:56 Matt Fleming
2026-11-13 18:56 ` [PATCH v3 01/10] stackdepot: add caller-owned stack trace fetching Matt Fleming
` (9 more replies)
0 siblings, 10 replies; 11+ messages in thread
From: Matt Fleming @ 2026-11-13 18:56 UTC (permalink / raw)
To: Andrew Morton, Marco Elver
Cc: Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov,
Oscar Salvador, Vlastimil Babka, Caleb Kan, Caleb Kan, linux-mm,
linux-kernel, kasan-dev, kernel-team, Matt Fleming
From: Matt Fleming <mfleming@cloudflare.com>
Hi,
Cloudflare runs KASAN on pre-production servers, and on some of them
stack depot keeps running out of pools even with stack_depot_max_pools
raised to 65,536. Once the depot is full, new persistent traces get no
handle and later KASAN reports lose the allocation and free history.
v2 of this series [1] tackled that with a trie that shares common frame
prefixes. Marco asked how much of the saving came from compressed frames
alone [2]. It turns out we can get a decent saving with just frame
compression and none of the trie complexity.
When every frame of a persistent record can be compressed with the arch
hooks, the record stores 32-bit frames instead of unsigned longs.
Evictable records are always allocated at maximum size, so they stay
uncompressed.
On our production KASAN hosts this reduced stack depot pools per 1000
stacks by 41.5% on x86-64 and 27.1% on arm64. arm64 saves less because
we run a lot of BPF programs, and their code lives outside the +/-2 GiB
range around _text that can be compressed.
Compressed records aren't contiguous unsigned long arrays, so callers
can no longer borrow stack depot's storage. Patches 1-7 move every
stack_depot_fetch() user to stack_depot_fetch_into() or the print
helpers and then remove stack_depot_fetch(). Patch 8 adds the arch
hooks, patch 9 stores compressed frames, and patch 10 teaches the GDB
helper to decode them.
Changes in v3:
- Drop the trie backend, the stackdepot.trie_enabled boot parameter and
the trie KUnit tests.
- Drop STACK_DEPOT_FLAG_COUNTABLE. page_owner reads traces with
stack_depot_fetch_into() instead.
- Remove stack_depot_fetch() now that it has no users.
- stack_depot_print() and stack_depot_snprint() decompress one frame at
a time instead of copying 16-frame chunks.
- Store compressed frames directly in hash records, using the u16 size
and u16 flags header layout from v2's COUNTABLE patch.
- The GDB helper decodes compressed frames instead of rejecting trie
handles.
- v2's patch 1 landed as 8f56ea8e0706 ("stackdepot: stop preallocating
after the final pool").
Thanks to Caleb, who wrote v1 and v2.
[1] https://lore.kernel.org/all/20260908-stackdepot-trie-v2-0-1996d5cef732@cloudflare.com/
[2] https://lore.kernel.org/r/CANpmjNMP=NdD8ZE9rPwikUBcLwc8V=J648X05YGPn41yAZ5hmw@mail.gmail.com/
Caleb Kan (6):
stackdepot: add caller-owned stack trace fetching
mm/kmemleak: use stack_depot_fetch_into() for leak reports
kmsan: use stack_depot_fetch_into() for origin reports
mm/slub: use stack_depot_fetch_into() for owner tracking
drm/locking: print deadlock backtraces with stack_depot_snprint()
stackdepot: add architecture hooks for compact frame storage
Matt Fleming (4):
mm/page_owner: Read stack traces with stack_depot_fetch_into()
stackdepot: Remove stack_depot_fetch()
stackdepot: Store compressed frames in stack records
scripts/gdb: Decode compressed stack depot frames
arch/arm64/include/asm/stackdepot.h | 37 ++++
arch/um/include/asm/Kbuild | 1 +
arch/x86/include/asm/stackdepot.h | 37 ++++
drivers/gpu/drm/drm_modeset_lock.c | 5 +-
include/asm-generic/Kbuild | 1 +
include/asm-generic/stackdepot.h | 19 ++
include/linux/stackdepot.h | 45 +++-
lib/Kconfig.debug | 16 ++
lib/stackdepot.c | 164 ++++++++++++---
lib/tests/Makefile | 1 +
lib/tests/stackdepot_kunit.c | 312 ++++++++++++++++++++++++++++
mm/kmemleak.c | 4 +-
mm/kmsan/kmsan_test.c | 4 +-
mm/kmsan/report.c | 28 +--
mm/page_owner.c | 8 +-
mm/slub.c | 12 +-
scripts/gdb/linux/constants.py.in | 5 +
scripts/gdb/linux/stackdepot.py | 23 +-
18 files changed, 650 insertions(+), 72 deletions(-)
create mode 100644 arch/arm64/include/asm/stackdepot.h
create mode 100644 arch/x86/include/asm/stackdepot.h
create mode 100644 include/asm-generic/stackdepot.h
create mode 100644 lib/tests/stackdepot_kunit.c
--
2.43.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v3 01/10] stackdepot: add caller-owned stack trace fetching
2026-11-13 18:56 [PATCH v3 00/10] stackdepot: store compressed frames in stack records Matt Fleming
@ 2026-11-13 18:56 ` Matt Fleming
2026-11-13 18:56 ` [PATCH v3 02/10] mm/kmemleak: use stack_depot_fetch_into() for leak reports Matt Fleming
` (8 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Matt Fleming @ 2026-11-13 18:56 UTC (permalink / raw)
To: Andrew Morton, Marco Elver
Cc: Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov,
Oscar Salvador, Vlastimil Babka, Caleb Kan, Caleb Kan, linux-mm,
linux-kernel, kasan-dev, kernel-team, Matt Fleming
From: Caleb Kan <ckan@cloudflare.com>
stack_depot_fetch() returns a pointer to contiguous storage owned by stack
depot. That cannot work once a later patch stores compressed frames, so
callers need an interface that copies the trace.
Add stack_depot_fetch_into() to copy a complete trace into caller-owned
storage. Leave the destination unchanged when it is too small, keep a zero
handle as a no-op, and document that callers must keep the handle valid
while copying it. Warn and return 0 if a caller passes a NULL buffer or
zero capacity for a valid handle.
Unpoison the copied entries before returning them because
lib/stackdepot.c is not instrumented by KMSAN. Add built-in KUnit tests for
exact and oversized destinations, zero handles, and undersized buffers.
Later patches add tests for stack depot internals.
Signed-off-by: Caleb Kan <ckan@cloudflare.com>
[mfleming: reworded for compressed frames]
Signed-off-by: Matt Fleming <mfleming@cloudflare.com>
---
include/linux/stackdepot.h | 36 +++++++++++++++
lib/Kconfig.debug | 16 +++++++
lib/stackdepot.c | 29 ++++++++++++
lib/tests/Makefile | 1 +
lib/tests/stackdepot_kunit.c | 89 ++++++++++++++++++++++++++++++++++++
5 files changed, 171 insertions(+)
create mode 100644 lib/tests/stackdepot_kunit.c
diff --git a/include/linux/stackdepot.h b/include/linux/stackdepot.h
index 2cc21ffcdaf9..734529767c8a 100644
--- a/include/linux/stackdepot.h
+++ b/include/linux/stackdepot.h
@@ -199,6 +199,42 @@ struct stack_record *__stack_depot_get_stack_record(depot_stack_handle_t handle)
unsigned int stack_depot_fetch(depot_stack_handle_t handle,
unsigned long **entries);
+/**
+ * stack_depot_fetch_into - Fetch a stack trace into caller-owned storage
+ *
+ * @handle: Stack depot handle
+ * @entries: Caller-owned buffer to copy the stack trace into
+ * @max_entries: Number of frames that fit in @entries
+ *
+ * Copies the stored frames into caller-owned @entries. If fewer frames are
+ * stored than @max_entries, only the stored frames are written and their count
+ * is returned. If more frames are stored than @max_entries, the copy is skipped
+ * entirely and 0 is returned.
+ *
+ * Passing a NULL @entries buffer or zero @max_entries for a valid @handle is
+ * invalid. Callers must provide storage for @max_entries frames.
+ *
+ * Callers should size @entries to match the save-side stack depth cap (for
+ * example, %CONFIG_STACKDEPOT_MAX_FRAMES or the local stack_trace_save() limit)
+ * when losing diagnostics on an undersized buffer would be surprising.
+ *
+ * A non-zero invalid @handle, including a post-put handle, may WARN. Its return
+ * value and copied contents are undefined because the record may have been
+ * reused for another stack.
+ *
+ * Callers must ensure @handle remains valid for the duration of this call.
+ * Persistent handles saved without %STACK_DEPOT_FLAG_GET require no extra
+ * reference; handles saved with %STACK_DEPOT_FLAG_GET require a held reference.
+ * Callers must not call stack_depot_put() on persistent handles.
+ * Racing this helper with stack_depot_put() on the same handle is invalid.
+ *
+ * Return: Number of frames copied, 0 if @handle is 0, stack depot is disabled,
+ * or @max_entries is less than the number of stored frames.
+ */
+unsigned int stack_depot_fetch_into(depot_stack_handle_t handle,
+ unsigned long *entries,
+ unsigned int max_entries);
+
/**
* stack_depot_print - Print a stack trace from stack depot
*
diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
index 2d22638ec13a..861d0caf9391 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -2818,6 +2818,22 @@ config RESOURCE_KUNIT_TEST
If unsure, say N.
+config STACKDEPOT_KUNIT_TEST
+ bool "KUnit test for stack depot" if !KUNIT_ALL_TESTS
+ depends on KUNIT=y && STACKDEPOT
+ depends on STACKDEPOT_MAX_FRAMES >= 3
+ default KUNIT_ALL_TESTS
+ help
+ Enable this option to test stack depot API behavior at boot.
+ This test is built in, so KUNIT must also be built in.
+
+ KUnit tests run during boot and output the results to the debug log
+ in TAP format (https://testanything.org/). Only useful for kernel
+ developers running the KUnit test harness, and not intended for
+ inclusion into a production build.
+
+ If unsure, say N.
+
config SYSCTL_KUNIT_TEST
tristate "KUnit test for sysctl" if !KUNIT_ALL_TESTS
depends on KUNIT
diff --git a/lib/stackdepot.c b/lib/stackdepot.c
index 90c52f2e0d3f..556f3ecdcc8f 100644
--- a/lib/stackdepot.c
+++ b/lib/stackdepot.c
@@ -785,6 +785,35 @@ unsigned int stack_depot_fetch(depot_stack_handle_t handle,
}
EXPORT_SYMBOL_GPL(stack_depot_fetch);
+unsigned int stack_depot_fetch_into(depot_stack_handle_t handle,
+ unsigned long *entries,
+ unsigned int max_entries)
+{
+ struct stack_record *stack;
+ unsigned int nr_entries;
+
+ if (!handle)
+ return 0;
+ if (stack_depot_disabled)
+ return 0;
+ if (WARN_ON_ONCE(!entries || !max_entries))
+ return 0;
+
+ stack = depot_fetch_stack(handle);
+ if (!stack)
+ return 0;
+ nr_entries = stack->size;
+ if (WARN_ON_ONCE(!nr_entries))
+ return 0;
+ if (nr_entries > max_entries)
+ return 0;
+
+ memcpy(entries, stack->entries, nr_entries * sizeof(*entries));
+ kmsan_unpoison_memory(entries, nr_entries * sizeof(*entries));
+ return nr_entries;
+}
+EXPORT_SYMBOL_GPL(stack_depot_fetch_into);
+
void stack_depot_put(depot_stack_handle_t handle)
{
struct stack_record *stack;
diff --git a/lib/tests/Makefile b/lib/tests/Makefile
index df0537e1b91f..bc6532d88150 100644
--- a/lib/tests/Makefile
+++ b/lib/tests/Makefile
@@ -49,6 +49,7 @@ obj-$(CONFIG_RANDSTRUCT_KUNIT_TEST) += randstruct_kunit.o
obj-$(CONFIG_SCANF_KUNIT_TEST) += scanf_kunit.o
obj-$(CONFIG_SEQ_BUF_KUNIT_TEST) += seq_buf_kunit.o
obj-$(CONFIG_SLUB_KUNIT_TEST) += slub_kunit.o
+obj-$(CONFIG_STACKDEPOT_KUNIT_TEST) += stackdepot_kunit.o
obj-$(CONFIG_TEST_SORT) += test_sort.o
CFLAGS_stackinit_kunit.o += $(call cc-disable-warning, switch-unreachable)
obj-$(CONFIG_STACKINIT_KUNIT_TEST) += stackinit_kunit.o
diff --git a/lib/tests/stackdepot_kunit.c b/lib/tests/stackdepot_kunit.c
new file mode 100644
index 000000000000..b0c44c096976
--- /dev/null
+++ b/lib/tests/stackdepot_kunit.c
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <kunit/test.h>
+#include <linux/array_size.h>
+#include <linux/gfp.h>
+#include <linux/stackdepot.h>
+#include <linux/string.h>
+
+static void stackdepot_fetch_into_roundtrip(struct kunit *test)
+{
+ unsigned long entries[] = {
+ 0x101000UL,
+ 0x102000UL,
+ 0x103000UL,
+ };
+ unsigned long exact[ARRAY_SIZE(entries)] = {};
+ unsigned long fetched[ARRAY_SIZE(entries) + 1] = {
+ [ARRAY_SIZE(entries)] = 0xa5a5a5a5UL,
+ };
+ unsigned long expected_tail = fetched[ARRAY_SIZE(entries)];
+ depot_stack_handle_t handle;
+ unsigned int nr_entries;
+
+ KUNIT_ASSERT_EQ(test, stack_depot_init(), 0);
+
+ handle = stack_depot_save(entries, ARRAY_SIZE(entries), GFP_KERNEL);
+ KUNIT_ASSERT_NE(test, handle, (depot_stack_handle_t)0);
+
+ nr_entries = stack_depot_fetch_into(handle, exact, ARRAY_SIZE(exact));
+ KUNIT_EXPECT_EQ(test, nr_entries, (unsigned int)ARRAY_SIZE(entries));
+ KUNIT_EXPECT_MEMEQ(test, exact, entries, sizeof(entries));
+
+ nr_entries = stack_depot_fetch_into(handle, fetched, ARRAY_SIZE(fetched));
+ KUNIT_EXPECT_EQ(test, nr_entries, (unsigned int)ARRAY_SIZE(entries));
+ KUNIT_EXPECT_MEMEQ(test, fetched, entries, sizeof(entries));
+ KUNIT_EXPECT_EQ(test, fetched[ARRAY_SIZE(entries)], expected_tail);
+}
+
+static void stackdepot_fetch_into_rejects_missing_or_short_stack(struct kunit *test)
+{
+ unsigned long entries[] = {
+ 0x111000UL,
+ 0x112000UL,
+ 0x113000UL,
+ };
+ unsigned long fetched[ARRAY_SIZE(entries)] = {
+ 0xa1a1a1a1UL,
+ 0xb2b2b2b2UL,
+ 0xc3c3c3c3UL,
+ };
+ unsigned long expected[ARRAY_SIZE(fetched)];
+ depot_stack_handle_t handle;
+ unsigned int nr_entries;
+
+ KUNIT_ASSERT_EQ(test, stack_depot_init(), 0);
+
+ handle = stack_depot_save(entries, ARRAY_SIZE(entries), GFP_KERNEL);
+ KUNIT_ASSERT_NE(test, handle, (depot_stack_handle_t)0);
+ memcpy(expected, fetched, sizeof(expected));
+
+ nr_entries = stack_depot_fetch_into(0, fetched, ARRAY_SIZE(fetched));
+ KUNIT_EXPECT_EQ(test, nr_entries, 0U);
+ KUNIT_EXPECT_MEMEQ(test, fetched, expected, sizeof(expected));
+
+ nr_entries = stack_depot_fetch_into(0, NULL, 0);
+ KUNIT_EXPECT_EQ(test, nr_entries, 0U);
+
+ nr_entries = stack_depot_fetch_into(handle, fetched,
+ ARRAY_SIZE(fetched) - 1);
+ KUNIT_EXPECT_EQ(test, nr_entries, 0U);
+ KUNIT_EXPECT_MEMEQ(test, fetched, expected, sizeof(expected));
+}
+
+static struct kunit_case stackdepot_test_cases[] = {
+ KUNIT_CASE(stackdepot_fetch_into_roundtrip),
+ KUNIT_CASE(stackdepot_fetch_into_rejects_missing_or_short_stack),
+ {}
+};
+
+static struct kunit_suite stackdepot_test_suite = {
+ .name = "stackdepot",
+ .test_cases = stackdepot_test_cases,
+};
+
+kunit_test_suite(stackdepot_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for stack depot");
+MODULE_AUTHOR("Caleb Kan <ckan@cloudflare.com>");
+MODULE_LICENSE("GPL");
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v3 02/10] mm/kmemleak: use stack_depot_fetch_into() for leak reports
2026-11-13 18:56 [PATCH v3 00/10] stackdepot: store compressed frames in stack records Matt Fleming
2026-11-13 18:56 ` [PATCH v3 01/10] stackdepot: add caller-owned stack trace fetching Matt Fleming
@ 2026-11-13 18:56 ` Matt Fleming
2026-11-13 18:56 ` [PATCH v3 03/10] kmsan: use stack_depot_fetch_into() for origin reports Matt Fleming
` (7 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Matt Fleming @ 2026-11-13 18:56 UTC (permalink / raw)
To: Andrew Morton, Marco Elver
Cc: Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov,
Oscar Salvador, Vlastimil Babka, Caleb Kan, Caleb Kan, linux-mm,
linux-kernel, kasan-dev, kernel-team, Matt Fleming
From: Caleb Kan <ckan@cloudflare.com>
kmemleak stores allocation backtraces as persistent stack depot handles.
With compressed frames, stack_depot_fetch() cannot return a pointer
to unsigned long stack-record entries, so leak reports would omit the
saved backtrace.
Use stack_depot_fetch_into() with a MAX_TRACE-sized local array before
formatting the report. MAX_TRACE matches the save-side limit, so every
valid kmemleak trace fits without truncation. Preserve frame order and the
existing report format.
Signed-off-by: Caleb Kan <ckan@cloudflare.com>
[mfleming: reworded for compressed frames]
Signed-off-by: Matt Fleming <mfleming@cloudflare.com>
---
mm/kmemleak.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/mm/kmemleak.c b/mm/kmemleak.c
index 20c0eb41c873..61ff4a10ce11 100644
--- a/mm/kmemleak.c
+++ b/mm/kmemleak.c
@@ -378,10 +378,10 @@ static void __print_unreferenced(struct seq_file *seq,
bool hex_dump)
{
int i;
- unsigned long *entries;
+ unsigned long entries[MAX_TRACE];
unsigned int nr_entries;
- nr_entries = stack_depot_fetch(object->trace_handle, &entries);
+ nr_entries = stack_depot_fetch_into(object->trace_handle, entries, ARRAY_SIZE(entries));
warn_or_seq_printf(seq, "unreferenced object%s 0x%08lx (size %zu):\n",
__object_type_str(object),
object->pointer, object->size);
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v3 03/10] kmsan: use stack_depot_fetch_into() for origin reports
2026-11-13 18:56 [PATCH v3 00/10] stackdepot: store compressed frames in stack records Matt Fleming
2026-11-13 18:56 ` [PATCH v3 01/10] stackdepot: add caller-owned stack trace fetching Matt Fleming
2026-11-13 18:56 ` [PATCH v3 02/10] mm/kmemleak: use stack_depot_fetch_into() for leak reports Matt Fleming
@ 2026-11-13 18:56 ` Matt Fleming
2026-11-13 18:56 ` [PATCH v3 04/10] mm/slub: use stack_depot_fetch_into() for owner tracking Matt Fleming
` (6 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Matt Fleming @ 2026-11-13 18:56 UTC (permalink / raw)
To: Andrew Morton, Marco Elver
Cc: Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov,
Oscar Salvador, Vlastimil Babka, Caleb Kan, Caleb Kan, linux-mm,
linux-kernel, kasan-dev, kernel-team, Matt Fleming
From: Caleb Kan <ckan@cloudflare.com>
KMSAN stores ordinary origin stacks and synthetic alloca and chain origins
in stack depot and retains the resulting persistent handles. Once a later
patch stores compressed frames, these handles can refer to records whose
frames must be decompressed before use, while kmsan_print_origin() still
relies on the pointer-returning stack_depot_fetch() API.
Use one KMSAN_STACK_DEPTH array to materialize each origin and chained
stack in turn. Preserve the chain's head and next-origin handles before
reusing the array for the chained stack. The array covers both the regular
save limit and the smaller synthetic records.
Pass the scratch array into a common origin-printing helper. Keep local
storage for standalone kmsan_print_origin() calls, but let kmsan_report()
reuse its existing stack_entries array after printing the report stack.
With x86-64 Clang 19, the regular nested report path uses 752 bytes, below
its 768-byte size before this conversion.
lib/stackdepot.c is uninstrumented, so stack_depot_fetch_into() unpoisons
the successfully copied range before returning it to KMSAN. Remove the
now-redundant explicit unpoisoning of chained entries. Origin depth and
use-after-free metadata remain in the handle's extra bits and are
unchanged.
Update test_stackdepot_roundtrip() to use caller-owned storage while
retaining its frame-count and kmsan_check_memory() checks. This verifies
that the copy-out API returns initialized entries to instrumented callers.
Signed-off-by: Caleb Kan <ckan@cloudflare.com>
[mfleming: reworded for compressed frames]
Signed-off-by: Matt Fleming <mfleming@cloudflare.com>
---
mm/kmsan/kmsan_test.c | 4 ++--
mm/kmsan/report.c | 28 ++++++++++++++++------------
2 files changed, 18 insertions(+), 14 deletions(-)
diff --git a/mm/kmsan/kmsan_test.c b/mm/kmsan/kmsan_test.c
index 31f47cc4dab4..7c04e4b21873 100644
--- a/mm/kmsan/kmsan_test.c
+++ b/mm/kmsan/kmsan_test.c
@@ -669,7 +669,7 @@ static void test_long_origin_chain(struct kunit *test)
*/
static void test_stackdepot_roundtrip(struct kunit *test)
{
- unsigned long src_entries[16], *dst_entries;
+ unsigned long src_entries[16], dst_entries[16];
unsigned int src_nentries, dst_nentries;
EXPECTATION_NO_REPORT(expect);
depot_stack_handle_t handle;
@@ -680,7 +680,7 @@ static void test_stackdepot_roundtrip(struct kunit *test)
stack_trace_save(src_entries, ARRAY_SIZE(src_entries), 1);
handle = stack_depot_save(src_entries, src_nentries, GFP_KERNEL);
stack_depot_print(handle);
- dst_nentries = stack_depot_fetch(handle, &dst_entries);
+ dst_nentries = stack_depot_fetch_into(handle, dst_entries, ARRAY_SIZE(dst_entries));
KUNIT_EXPECT_TRUE(test, src_nentries == dst_nentries);
kmsan_check_memory((void *)dst_entries,
diff --git a/mm/kmsan/report.c b/mm/kmsan/report.c
index d6853ce08954..0770658ba932 100644
--- a/mm/kmsan/report.c
+++ b/mm/kmsan/report.c
@@ -83,9 +83,9 @@ static char *pretty_descr(char *descr)
return report_local_descr;
}
-void kmsan_print_origin(depot_stack_handle_t origin)
+static void kmsan_print_origin_with_buf(depot_stack_handle_t origin,
+ unsigned long *entries)
{
- unsigned long *entries = NULL, *chained_entries = NULL;
unsigned int nr_entries, chained_nr_entries, skipnr;
void *pc1 = NULL, *pc2 = NULL;
depot_stack_handle_t head;
@@ -97,7 +97,8 @@ void kmsan_print_origin(depot_stack_handle_t origin)
return;
while (true) {
- nr_entries = stack_depot_fetch(origin, &entries);
+ nr_entries =
+ stack_depot_fetch_into(origin, entries, KMSAN_STACK_DEPTH);
depth = kmsan_depth_from_eb(stack_depot_get_extra_bits(origin));
magic = nr_entries ? entries[0] : 0;
if ((nr_entries == 4) && (magic == KMSAN_ALLOCA_MAGIC_ORIGIN)) {
@@ -123,14 +124,10 @@ void kmsan_print_origin(depot_stack_handle_t origin)
origin = entries[2];
pr_err("Uninit was stored to memory at:\n");
chained_nr_entries =
- stack_depot_fetch(head, &chained_entries);
- kmsan_internal_unpoison_memory(
- chained_entries,
- chained_nr_entries * sizeof(*chained_entries),
- /*checked*/ false);
- skipnr = get_stack_skipnr(chained_entries,
- chained_nr_entries);
- stack_trace_print(chained_entries + skipnr,
+ stack_depot_fetch_into(head, entries,
+ KMSAN_STACK_DEPTH);
+ skipnr = get_stack_skipnr(entries, chained_nr_entries);
+ stack_trace_print(entries + skipnr,
chained_nr_entries - skipnr, 0);
pr_err("\n");
continue;
@@ -147,6 +144,13 @@ void kmsan_print_origin(depot_stack_handle_t origin)
}
}
+void kmsan_print_origin(depot_stack_handle_t origin)
+{
+ unsigned long entries[KMSAN_STACK_DEPTH];
+
+ kmsan_print_origin_with_buf(origin, entries);
+}
+
void kmsan_report(depot_stack_handle_t origin, void *address, int size,
int off_first, int off_last, const void __user *user_addr,
enum kmsan_bug_reason reason)
@@ -193,7 +197,7 @@ void kmsan_report(depot_stack_handle_t origin, void *address, int size,
0);
pr_err("\n");
- kmsan_print_origin(origin);
+ kmsan_print_origin_with_buf(origin, stack_entries);
if (size) {
pr_err("\n");
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v3 04/10] mm/slub: use stack_depot_fetch_into() for owner tracking
2026-11-13 18:56 [PATCH v3 00/10] stackdepot: store compressed frames in stack records Matt Fleming
` (2 preceding siblings ...)
2026-11-13 18:56 ` [PATCH v3 03/10] kmsan: use stack_depot_fetch_into() for origin reports Matt Fleming
@ 2026-11-13 18:56 ` Matt Fleming
2026-11-13 18:56 ` [PATCH v3 05/10] drm/locking: print deadlock backtraces with stack_depot_snprint() Matt Fleming
` (5 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Matt Fleming @ 2026-11-13 18:56 UTC (permalink / raw)
To: Andrew Morton, Marco Elver
Cc: Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov,
Oscar Salvador, Vlastimil Babka, Caleb Kan, Caleb Kan, linux-mm,
linux-kernel, kasan-dev, kernel-team, Matt Fleming
From: Caleb Kan <ckan@cloudflare.com>
SLUB owner tracking stores allocation and free stacks as persistent stack
depot handles. Compressed frames must be decompressed before use, so
__kmem_obj_info() and the alloc_traces and free_traces debugfs files
cannot use stack_depot_fetch().
Use stack_depot_fetch_into() with TRACK_ADDRS_COUNT-sized local arrays.
This matches the save-side limit. Keep the existing KS_ADDRS_COUNT copy
limit and debugfs formatting unchanged. Continue
to copy or print no frames when the fetch returns zero.
Signed-off-by: Caleb Kan <ckan@cloudflare.com>
[mfleming: reworded for compressed frames]
Signed-off-by: Matt Fleming <mfleming@cloudflare.com>
---
mm/slub.c | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/mm/slub.c b/mm/slub.c
index cd671b934719..32e6597a5c03 100644
--- a/mm/slub.c
+++ b/mm/slub.c
@@ -8328,12 +8328,12 @@ void __kmem_obj_info(struct kmem_obj_info *kpp, void *object, struct slab *slab)
#ifdef CONFIG_STACKDEPOT
{
depot_stack_handle_t handle;
- unsigned long *entries;
+ unsigned long entries[TRACK_ADDRS_COUNT];
unsigned int nr_entries;
handle = READ_ONCE(trackp->handle);
if (handle) {
- nr_entries = stack_depot_fetch(handle, &entries);
+ nr_entries = stack_depot_fetch_into(handle, entries, ARRAY_SIZE(entries));
for (i = 0; i < KS_ADDRS_COUNT && i < nr_entries; i++)
kpp->kp_stack[i] = (void *)entries[i];
}
@@ -8341,7 +8341,7 @@ void __kmem_obj_info(struct kmem_obj_info *kpp, void *object, struct slab *slab)
trackp = get_track(s, objp, TRACK_FREE);
handle = READ_ONCE(trackp->handle);
if (handle) {
- nr_entries = stack_depot_fetch(handle, &entries);
+ nr_entries = stack_depot_fetch_into(handle, entries, ARRAY_SIZE(entries));
for (i = 0; i < KS_ADDRS_COUNT && i < nr_entries; i++)
kpp->kp_free_stack[i] = (void *)entries[i];
}
@@ -10031,12 +10031,14 @@ static int slab_debugfs_show(struct seq_file *seq, void *v)
#ifdef CONFIG_STACKDEPOT
{
depot_stack_handle_t handle;
- unsigned long *entries;
+ unsigned long entries[TRACK_ADDRS_COUNT];
unsigned int nr_entries, j;
handle = READ_ONCE(l->handle);
if (handle) {
- nr_entries = stack_depot_fetch(handle, &entries);
+ nr_entries =
+ stack_depot_fetch_into(handle, entries,
+ ARRAY_SIZE(entries));
seq_puts(seq, "\n");
for (j = 0; j < nr_entries; j++)
seq_printf(seq, " %pS\n", (void *)entries[j]);
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v3 05/10] drm/locking: print deadlock backtraces with stack_depot_snprint()
2026-11-13 18:56 [PATCH v3 00/10] stackdepot: store compressed frames in stack records Matt Fleming
` (3 preceding siblings ...)
2026-11-13 18:56 ` [PATCH v3 04/10] mm/slub: use stack_depot_fetch_into() for owner tracking Matt Fleming
@ 2026-11-13 18:56 ` Matt Fleming
2026-11-13 18:56 ` [PATCH v3 06/10] mm/page_owner: Read stack traces with stack_depot_fetch_into() Matt Fleming
` (4 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Matt Fleming @ 2026-11-13 18:56 UTC (permalink / raw)
To: Andrew Morton, Marco Elver
Cc: Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov,
Oscar Salvador, Vlastimil Babka, Caleb Kan, Caleb Kan, linux-mm,
linux-kernel, kasan-dev, kernel-team, Matt Fleming
From: Caleb Kan <ckan@cloudflare.com>
When a modeset lock acquisition returns -EDEADLK, DRM saves the call chain
and prints it if the caller later attempts another lock or drops its locks
without first calling drm_modeset_backoff(). This diagnostic currently
fetches the saved stack through stack_depot_fetch().
Later patches compress the frames of persistent stack depot records and
remove stack_depot_fetch(), which cannot return a pointer to them.
Use stack_depot_snprint() to format the stack. Keep the PAGE_SIZE
buffer and leave the warning text, existing indentation, and backtrace
unchanged.
Signed-off-by: Caleb Kan <ckan@cloudflare.com>
[mfleming: reworded for compressed frames]
Signed-off-by: Matt Fleming <mfleming@cloudflare.com>
---
drivers/gpu/drm/drm_modeset_lock.c | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/drivers/gpu/drm/drm_modeset_lock.c b/drivers/gpu/drm/drm_modeset_lock.c
index e14814c30d8c..f48b02379b71 100644
--- a/drivers/gpu/drm/drm_modeset_lock.c
+++ b/drivers/gpu/drm/drm_modeset_lock.c
@@ -94,16 +94,13 @@ static noinline depot_stack_handle_t __drm_stack_depot_save(void)
static void __drm_stack_depot_print(depot_stack_handle_t stack_depot)
{
struct drm_printer p = drm_dbg_printer(NULL, DRM_UT_KMS, "drm_modeset_lock");
- unsigned long *entries;
- unsigned int nr_entries;
char *buf;
buf = kmalloc(PAGE_SIZE, GFP_NOWAIT | __GFP_NOWARN);
if (!buf)
return;
- nr_entries = stack_depot_fetch(stack_depot, &entries);
- stack_trace_snprint(buf, PAGE_SIZE, entries, nr_entries, 2);
+ stack_depot_snprint(stack_depot, buf, PAGE_SIZE, 2);
drm_printf(&p, "attempting to lock a contended lock without backoff:\n%s", buf);
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v3 06/10] mm/page_owner: Read stack traces with stack_depot_fetch_into()
2026-11-13 18:56 [PATCH v3 00/10] stackdepot: store compressed frames in stack records Matt Fleming
` (4 preceding siblings ...)
2026-11-13 18:56 ` [PATCH v3 05/10] drm/locking: print deadlock backtraces with stack_depot_snprint() Matt Fleming
@ 2026-11-13 18:56 ` Matt Fleming
2026-11-13 18:56 ` [PATCH v3 07/10] stackdepot: Remove stack_depot_fetch() Matt Fleming
` (3 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Matt Fleming @ 2026-11-13 18:56 UTC (permalink / raw)
To: Andrew Morton, Marco Elver
Cc: Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov,
Oscar Salvador, Vlastimil Babka, Caleb Kan, Caleb Kan, linux-mm,
linux-kernel, kasan-dev, kernel-team, Matt Fleming
From: Matt Fleming <mfleming@cloudflare.com>
stack_print() reads stack_record->entries directly. A later patch
stores compressed frames inside struct stack_record, so rather than
decompressing those frames let's use stack_depot_fetch_into().
Copy the trace with stack_depot_fetch_into() into a stack-local buffer
with PAGE_OWNER_STACK_DEPTH elements, which is the maximum number of
frames page_owner saves.
Signed-off-by: Matt Fleming <mfleming@cloudflare.com>
---
mm/page_owner.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/mm/page_owner.c b/mm/page_owner.c
index cfc31c92d765..16429e70e00d 100644
--- a/mm/page_owner.c
+++ b/mm/page_owner.c
@@ -1068,8 +1068,8 @@ static int stack_print(struct seq_file *m, void *v)
{
int i, nr_base_pages;
struct stack *stack = v;
- unsigned long *entries;
- unsigned long nr_entries;
+ unsigned long entries[PAGE_OWNER_STACK_DEPTH];
+ unsigned int nr_entries;
struct stack_record *stack_record = stack->stack_record;
struct stack_print_ctx *ctx = m->private;
@@ -1083,8 +1083,8 @@ static int stack_print(struct seq_file *m, void *v)
return 0;
if (ctx->flags & STACK_PRINT_FLAG_STACK) {
- nr_entries = stack_record->size;
- entries = stack_record->entries;
+ nr_entries = stack_depot_fetch_into(stack_record->handle.handle,
+ entries, ARRAY_SIZE(entries));
for (i = 0; i < nr_entries; i++)
seq_printf(m, " %pS\n", (void *)entries[i]);
}
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v3 07/10] stackdepot: Remove stack_depot_fetch()
2026-11-13 18:56 [PATCH v3 00/10] stackdepot: store compressed frames in stack records Matt Fleming
` (5 preceding siblings ...)
2026-11-13 18:56 ` [PATCH v3 06/10] mm/page_owner: Read stack traces with stack_depot_fetch_into() Matt Fleming
@ 2026-11-13 18:56 ` Matt Fleming
2026-11-13 18:57 ` [PATCH v3 08/10] stackdepot: add architecture hooks for compact frame storage Matt Fleming
` (2 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Matt Fleming @ 2026-11-13 18:56 UTC (permalink / raw)
To: Andrew Morton, Marco Elver
Cc: Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov,
Oscar Salvador, Vlastimil Babka, Caleb Kan, Caleb Kan, linux-mm,
linux-kernel, kasan-dev, kernel-team, Matt Fleming
From: Matt Fleming <mfleming@cloudflare.com>
All callers now use stack_depot_fetch_into(), stack_depot_print() or
stack_depot_snprint(). stack_depot_fetch() hands out a pointer into
depot storage, which leaks how frames are stored internally by
stackdepot. This will be undesirable when compressed frame support
lands in a later patch.
Add a KUnit test that stack_depot_snprint() output and return value
match stack_trace_snprint(), including truncation.
Signed-off-by: Matt Fleming <mfleming@cloudflare.com>
---
include/linux/stackdepot.h | 11 ------
lib/stackdepot.c | 68 ++++++++++++++++--------------------
lib/tests/stackdepot_kunit.c | 36 +++++++++++++++++++
3 files changed, 66 insertions(+), 49 deletions(-)
diff --git a/include/linux/stackdepot.h b/include/linux/stackdepot.h
index 734529767c8a..dcc2e1d21f92 100644
--- a/include/linux/stackdepot.h
+++ b/include/linux/stackdepot.h
@@ -188,17 +188,6 @@ depot_stack_handle_t stack_depot_save(unsigned long *entries,
*/
struct stack_record *__stack_depot_get_stack_record(depot_stack_handle_t handle);
-/**
- * stack_depot_fetch - Fetch a stack trace from stack depot
- *
- * @handle: Stack depot handle returned from stack_depot_save()
- * @entries: Pointer to store the address of the stack trace
- *
- * Return: Number of frames for the fetched stack
- */
-unsigned int stack_depot_fetch(depot_stack_handle_t handle,
- unsigned long **entries);
-
/**
* stack_depot_fetch_into - Fetch a stack trace into caller-owned storage
*
diff --git a/lib/stackdepot.c b/lib/stackdepot.c
index 556f3ecdcc8f..8f1aaa0c6d88 100644
--- a/lib/stackdepot.c
+++ b/lib/stackdepot.c
@@ -757,33 +757,14 @@ struct stack_record *__stack_depot_get_stack_record(depot_stack_handle_t handle)
return depot_fetch_stack(handle);
}
-unsigned int stack_depot_fetch(depot_stack_handle_t handle,
- unsigned long **entries)
+/* Copy the first @nr_entries frames of @stack into @entries. */
+static void depot_copy_entries(const struct stack_record *stack,
+ unsigned long *entries, unsigned int nr_entries)
{
- struct stack_record *stack;
-
- *entries = NULL;
- /*
- * Let KMSAN know *entries is initialized. This shall prevent false
- * positive reports if instrumented code accesses it.
- */
- kmsan_unpoison_memory(entries, sizeof(*entries));
-
- if (!handle || stack_depot_disabled)
- return 0;
-
- stack = depot_fetch_stack(handle);
- /*
- * Should never be NULL, otherwise this is a use-after-put (or just a
- * corrupt handle).
- */
- if (WARN(!stack, "corrupt handle or use after stack_depot_put()"))
- return 0;
-
- *entries = stack->entries;
- return stack->size;
+ memcpy(entries, stack->entries, nr_entries * sizeof(*entries));
+ /* stackdepot.c is not instrumented; tell KMSAN the copy is initialized. */
+ kmsan_unpoison_memory(entries, nr_entries * sizeof(*entries));
}
-EXPORT_SYMBOL_GPL(stack_depot_fetch);
unsigned int stack_depot_fetch_into(depot_stack_handle_t handle,
unsigned long *entries,
@@ -808,12 +789,27 @@ unsigned int stack_depot_fetch_into(depot_stack_handle_t handle,
if (nr_entries > max_entries)
return 0;
- memcpy(entries, stack->entries, nr_entries * sizeof(*entries));
- kmsan_unpoison_memory(entries, nr_entries * sizeof(*entries));
+ depot_copy_entries(stack, entries, nr_entries);
return nr_entries;
}
EXPORT_SYMBOL_GPL(stack_depot_fetch_into);
+static struct stack_record *depot_print_fetch(depot_stack_handle_t handle)
+{
+ struct stack_record *stack;
+
+ if (!handle || stack_depot_disabled)
+ return NULL;
+
+ stack = depot_fetch_stack(handle);
+ /*
+ * Should never be NULL, otherwise this is a use-after-put (or just a
+ * corrupt handle).
+ */
+ WARN(!stack, "corrupt handle or use after stack_depot_put()");
+ return stack;
+}
+
void stack_depot_put(depot_stack_handle_t handle)
{
struct stack_record *stack;
@@ -834,26 +830,22 @@ void stack_depot_put(depot_stack_handle_t handle)
}
EXPORT_SYMBOL_GPL(stack_depot_put);
-void stack_depot_print(depot_stack_handle_t stack)
+void stack_depot_print(depot_stack_handle_t handle)
{
- unsigned long *entries;
- unsigned int nr_entries;
+ struct stack_record *stack = depot_print_fetch(handle);
- nr_entries = stack_depot_fetch(stack, &entries);
- if (nr_entries > 0)
- stack_trace_print(entries, nr_entries, 0);
+ if (stack)
+ stack_trace_print(stack->entries, stack->size, 0);
}
EXPORT_SYMBOL_GPL(stack_depot_print);
int stack_depot_snprint(depot_stack_handle_t handle, char *buf, size_t size,
int spaces)
{
- unsigned long *entries;
- unsigned int nr_entries;
+ struct stack_record *stack = depot_print_fetch(handle);
- nr_entries = stack_depot_fetch(handle, &entries);
- return nr_entries ? stack_trace_snprint(buf, size, entries, nr_entries,
- spaces) : 0;
+ return stack ? stack_trace_snprint(buf, size, stack->entries,
+ stack->size, spaces) : 0;
}
EXPORT_SYMBOL_GPL(stack_depot_snprint);
diff --git a/lib/tests/stackdepot_kunit.c b/lib/tests/stackdepot_kunit.c
index b0c44c096976..ae4f7661a48f 100644
--- a/lib/tests/stackdepot_kunit.c
+++ b/lib/tests/stackdepot_kunit.c
@@ -3,6 +3,8 @@
#include <kunit/test.h>
#include <linux/array_size.h>
#include <linux/gfp.h>
+#include <linux/minmax.h>
+#include <linux/stacktrace.h>
#include <linux/stackdepot.h>
#include <linux/string.h>
@@ -71,9 +73,43 @@ static void stackdepot_fetch_into_rejects_missing_or_short_stack(struct kunit *t
KUNIT_EXPECT_MEMEQ(test, fetched, expected, sizeof(expected));
}
+/* stack_depot_snprint() must match stack_trace_snprint(), with truncation. */
+static void stackdepot_snprint_matches_stack_trace(struct kunit *test)
+{
+ const unsigned int nr = min(40, CONFIG_STACKDEPOT_MAX_FRAMES);
+ const size_t size = 4096;
+ unsigned long entries[40];
+ depot_stack_handle_t handle;
+ char *got, *want;
+ unsigned int i;
+
+ for (i = 0; i < nr; i++)
+ entries[i] = 0x301000UL + 0x1000UL * i;
+ got = kunit_kzalloc(test, size, GFP_KERNEL);
+ want = kunit_kzalloc(test, size, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, got);
+ KUNIT_ASSERT_NOT_NULL(test, want);
+
+ KUNIT_ASSERT_EQ(test, stack_depot_init(), 0);
+ handle = stack_depot_save(entries, nr, GFP_KERNEL);
+ KUNIT_ASSERT_NE(test, handle, (depot_stack_handle_t)0);
+
+ KUNIT_EXPECT_EQ(test, stack_depot_snprint(handle, got, size, 2),
+ stack_trace_snprint(want, size, entries, nr, 2));
+ KUNIT_EXPECT_STREQ(test, got, want);
+
+ /* Truncate part-way through the trace. */
+ memset(got, 0, size);
+ memset(want, 0, size);
+ KUNIT_EXPECT_EQ(test, stack_depot_snprint(handle, got, 700, 0),
+ stack_trace_snprint(want, 700, entries, nr, 0));
+ KUNIT_EXPECT_STREQ(test, got, want);
+}
+
static struct kunit_case stackdepot_test_cases[] = {
KUNIT_CASE(stackdepot_fetch_into_roundtrip),
KUNIT_CASE(stackdepot_fetch_into_rejects_missing_or_short_stack),
+ KUNIT_CASE(stackdepot_snprint_matches_stack_trace),
{}
};
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v3 08/10] stackdepot: add architecture hooks for compact frame storage
2026-11-13 18:56 [PATCH v3 00/10] stackdepot: store compressed frames in stack records Matt Fleming
` (6 preceding siblings ...)
2026-11-13 18:56 ` [PATCH v3 07/10] stackdepot: Remove stack_depot_fetch() Matt Fleming
@ 2026-11-13 18:57 ` Matt Fleming
2026-11-13 18:57 ` [PATCH v3 09/10] stackdepot: Store compressed frames in stack records Matt Fleming
2026-11-13 18:57 ` [PATCH v3 10/10] scripts/gdb: Decode compressed stack depot frames Matt Fleming
9 siblings, 0 replies; 11+ messages in thread
From: Matt Fleming @ 2026-11-13 18:57 UTC (permalink / raw)
To: Andrew Morton, Marco Elver
Cc: Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov,
Oscar Salvador, Vlastimil Babka, Caleb Kan, Caleb Kan, linux-mm,
linux-kernel, kasan-dev, kernel-team, Matt Fleming
From: Caleb Kan <ckan@cloudflare.com>
Stack depot records can reduce their frame storage when an
architecture can represent kernel text and module addresses in 32 bits.
Add architecture hooks that compress a frame only when decompression
reproduces the original address exactly.
Store arm64 frames as signed 32-bit offsets from _text. This covers the
2 GB module relocation window without depending on a 4 GB high-bit
boundary. On x86-64, store the low 32 bits when the upper 32 bits are all
set. Keep other frames full-width, and provide a generic implementation
that always rejects compression.
Make the generic header available to architectures without a specialized
implementation and wire it explicitly for UML. Add KUnit coverage for raw
fallback, arm64 boundary round trips, and native x86-64 prefix compression.
These hooks do not change stack depot behavior until a later patch uses
them to store compressed frames.
Signed-off-by: Caleb Kan <ckan@cloudflare.com>
[mfleming: simplify arm64 decode, reworded]
Signed-off-by: Matt Fleming <mfleming@cloudflare.com>
---
arch/arm64/include/asm/stackdepot.h | 37 +++++++++++++
arch/um/include/asm/Kbuild | 1 +
arch/x86/include/asm/stackdepot.h | 37 +++++++++++++
include/asm-generic/Kbuild | 1 +
include/asm-generic/stackdepot.h | 19 +++++++
lib/tests/stackdepot_kunit.c | 86 +++++++++++++++++++++++++++++
6 files changed, 181 insertions(+)
create mode 100644 arch/arm64/include/asm/stackdepot.h
create mode 100644 arch/x86/include/asm/stackdepot.h
create mode 100644 include/asm-generic/stackdepot.h
diff --git a/arch/arm64/include/asm/stackdepot.h b/arch/arm64/include/asm/stackdepot.h
new file mode 100644
index 000000000000..72053527ab21
--- /dev/null
+++ b/arch/arm64/include/asm/stackdepot.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __ASM_STACKDEPOT_H
+#define __ASM_STACKDEPOT_H
+
+#include <linux/types.h>
+#include <asm/sections.h>
+
+/*
+ * Modules are allocated inside a 2 GB relocation window containing the
+ * kernel image. Store a signed 32-bit offset from _text so compression is
+ * independent of 4 GB high-bit boundaries crossed by that window.
+ */
+static inline unsigned long arch_stack_depot_frame_from_payload(u32 payload)
+{
+ return (unsigned long)_text + (s64)(s32)payload;
+}
+
+static inline bool
+arch_stack_depot_frame_try_compress(unsigned long frame, u32 *payload)
+{
+ u32 candidate;
+
+ candidate = (u32)(frame - (unsigned long)_text);
+ if (arch_stack_depot_frame_from_payload(candidate) != frame)
+ return false;
+
+ *payload = candidate;
+ return true;
+}
+
+static inline void
+arch_stack_depot_frame_decompress(u32 payload, unsigned long *frame)
+{
+ *frame = arch_stack_depot_frame_from_payload(payload);
+}
+
+#endif /* __ASM_STACKDEPOT_H */
diff --git a/arch/um/include/asm/Kbuild b/arch/um/include/asm/Kbuild
index 8fdc0bd9ab6f..14778d2457d7 100644
--- a/arch/um/include/asm/Kbuild
+++ b/arch/um/include/asm/Kbuild
@@ -21,6 +21,7 @@ generic-y += preempt.h
generic-y += ring_buffer.h
generic-y += runtime-const.h
generic-y += softirq_stack.h
+generic-y += stackdepot.h
generic-y += switch_to.h
generic-y += topology.h
generic-y += trace_clock.h
diff --git a/arch/x86/include/asm/stackdepot.h b/arch/x86/include/asm/stackdepot.h
new file mode 100644
index 000000000000..9a8d04fa8c1c
--- /dev/null
+++ b/arch/x86/include/asm/stackdepot.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _ASM_X86_STACKDEPOT_H
+#define _ASM_X86_STACKDEPOT_H
+
+#include <linux/types.h>
+
+#ifdef CONFIG_X86_64
+/*
+ * Compress canonical kernel text/module addresses whose upper 32 bits are all
+ * ones. Other kernel virtual addresses stay raw, so decompression reconstructs
+ * the original frame by restoring this prefix.
+ */
+#define STACK_DEPOT_X86_64_FRAME_PREFIX 0xffffffff00000000UL
+#define STACK_DEPOT_X86_64_FRAME_LOW_MASK 0x00000000ffffffffUL
+
+static inline bool
+arch_stack_depot_frame_try_compress(unsigned long frame, u32 *low)
+{
+ if ((frame & ~STACK_DEPOT_X86_64_FRAME_LOW_MASK) !=
+ STACK_DEPOT_X86_64_FRAME_PREFIX)
+ return false;
+
+ *low = (u32)frame;
+ return true;
+}
+
+static inline void
+arch_stack_depot_frame_decompress(u32 low, unsigned long *frame)
+{
+ *frame = STACK_DEPOT_X86_64_FRAME_PREFIX | low;
+}
+
+#else
+#include <asm-generic/stackdepot.h>
+#endif /* CONFIG_X86_64 */
+
+#endif /* _ASM_X86_STACKDEPOT_H */
diff --git a/include/asm-generic/Kbuild b/include/asm-generic/Kbuild
index b7a388bef6ea..76d4792a325a 100644
--- a/include/asm-generic/Kbuild
+++ b/include/asm-generic/Kbuild
@@ -55,6 +55,7 @@ mandatory-y += serial.h
mandatory-y += shmparam.h
mandatory-y += simd.h
mandatory-y += softirq_stack.h
+mandatory-y += stackdepot.h
mandatory-y += switch_to.h
mandatory-y += timex.h
mandatory-y += tlbflush.h
diff --git a/include/asm-generic/stackdepot.h b/include/asm-generic/stackdepot.h
new file mode 100644
index 000000000000..846975767bdd
--- /dev/null
+++ b/include/asm-generic/stackdepot.h
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __ASM_GENERIC_STACKDEPOT_H
+#define __ASM_GENERIC_STACKDEPOT_H
+
+#include <linux/types.h>
+
+static inline bool
+arch_stack_depot_frame_try_compress(unsigned long frame, u32 *low)
+{
+ return false;
+}
+
+static inline void
+arch_stack_depot_frame_decompress(u32 low, unsigned long *frame)
+{
+ /* Generic code never compresses frames, so this hook is unreachable. */
+}
+
+#endif /* __ASM_GENERIC_STACKDEPOT_H */
diff --git a/lib/tests/stackdepot_kunit.c b/lib/tests/stackdepot_kunit.c
index ae4f7661a48f..dc3f8e207bdd 100644
--- a/lib/tests/stackdepot_kunit.c
+++ b/lib/tests/stackdepot_kunit.c
@@ -3,11 +3,23 @@
#include <kunit/test.h>
#include <linux/array_size.h>
#include <linux/gfp.h>
+#include <linux/limits.h>
#include <linux/minmax.h>
#include <linux/stacktrace.h>
#include <linux/stackdepot.h>
#include <linux/string.h>
+#include <asm/stackdepot.h>
+
+#ifdef CONFIG_ARM64
+#include <asm/sections.h>
+
+static inline unsigned long stackdepot_arm64_frame(long offset)
+{
+ return (unsigned long)((long)_text + offset);
+}
+#endif
+
static void stackdepot_fetch_into_roundtrip(struct kunit *test)
{
unsigned long entries[] = {
@@ -73,6 +85,73 @@ static void stackdepot_fetch_into_rejects_missing_or_short_stack(struct kunit *t
KUNIT_EXPECT_MEMEQ(test, fetched, expected, sizeof(expected));
}
+static void stackdepot_frame_raw_fallback(struct kunit *test)
+{
+ unsigned long frame = 0x1000UL;
+ bool compressed;
+ u32 payload;
+
+#ifdef CONFIG_ARM64
+ frame = (unsigned long)_text + (unsigned long)S32_MAX + 1UL;
+#endif
+
+ compressed = arch_stack_depot_frame_try_compress(frame, &payload);
+ KUNIT_EXPECT_FALSE(test, compressed);
+}
+
+#if defined(CONFIG_X86_64) && !defined(CONFIG_UML)
+static void stackdepot_frame_x86_64(struct kunit *test)
+{
+ unsigned long direct_map = 0xffff888000001000UL;
+ unsigned long frame = 0xffffffff81234567UL;
+ unsigned long out;
+ bool compressed;
+ u32 low;
+
+ compressed = arch_stack_depot_frame_try_compress(frame, &low);
+ KUNIT_EXPECT_TRUE(test, compressed);
+ KUNIT_EXPECT_EQ(test, low, (u32)0x81234567);
+ arch_stack_depot_frame_decompress(low, &out);
+ KUNIT_EXPECT_EQ(test, out, frame);
+
+ compressed = arch_stack_depot_frame_try_compress(direct_map, &low);
+ KUNIT_EXPECT_FALSE(test, compressed);
+}
+#endif /* CONFIG_X86_64 && !CONFIG_UML */
+
+#ifdef CONFIG_ARM64
+static void stackdepot_frame_arm64(struct kunit *test)
+{
+ long negative_offset = S32_MIN;
+ long positive_offset = S32_MAX;
+ long offset = 0x123456;
+ unsigned long frame = stackdepot_arm64_frame(offset);
+ unsigned long out;
+ bool compressed;
+ u32 payload;
+
+ compressed = arch_stack_depot_frame_try_compress(frame, &payload);
+ KUNIT_EXPECT_TRUE(test, compressed);
+ KUNIT_EXPECT_EQ(test, payload, (u32)(s32)offset);
+ arch_stack_depot_frame_decompress(payload, &out);
+ KUNIT_EXPECT_EQ(test, out, frame);
+
+ frame = stackdepot_arm64_frame(negative_offset);
+ compressed = arch_stack_depot_frame_try_compress(frame, &payload);
+ KUNIT_EXPECT_TRUE(test, compressed);
+ KUNIT_EXPECT_EQ(test, payload, (u32)(s32)negative_offset);
+ arch_stack_depot_frame_decompress(payload, &out);
+ KUNIT_EXPECT_EQ(test, out, frame);
+
+ frame = stackdepot_arm64_frame(positive_offset);
+ compressed = arch_stack_depot_frame_try_compress(frame, &payload);
+ KUNIT_EXPECT_TRUE(test, compressed);
+ KUNIT_EXPECT_EQ(test, payload, (u32)(s32)positive_offset);
+ arch_stack_depot_frame_decompress(payload, &out);
+ KUNIT_EXPECT_EQ(test, out, frame);
+}
+#endif /* CONFIG_ARM64 */
+
/* stack_depot_snprint() must match stack_trace_snprint(), with truncation. */
static void stackdepot_snprint_matches_stack_trace(struct kunit *test)
{
@@ -110,6 +189,13 @@ static struct kunit_case stackdepot_test_cases[] = {
KUNIT_CASE(stackdepot_fetch_into_roundtrip),
KUNIT_CASE(stackdepot_fetch_into_rejects_missing_or_short_stack),
KUNIT_CASE(stackdepot_snprint_matches_stack_trace),
+ KUNIT_CASE(stackdepot_frame_raw_fallback),
+#if defined(CONFIG_X86_64) && !defined(CONFIG_UML)
+ KUNIT_CASE(stackdepot_frame_x86_64),
+#endif
+#ifdef CONFIG_ARM64
+ KUNIT_CASE(stackdepot_frame_arm64),
+#endif
{}
};
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v3 09/10] stackdepot: Store compressed frames in stack records
2026-11-13 18:56 [PATCH v3 00/10] stackdepot: store compressed frames in stack records Matt Fleming
` (7 preceding siblings ...)
2026-11-13 18:57 ` [PATCH v3 08/10] stackdepot: add architecture hooks for compact frame storage Matt Fleming
@ 2026-11-13 18:57 ` Matt Fleming
2026-11-13 18:57 ` [PATCH v3 10/10] scripts/gdb: Decode compressed stack depot frames Matt Fleming
9 siblings, 0 replies; 11+ messages in thread
From: Matt Fleming @ 2026-11-13 18:57 UTC (permalink / raw)
To: Andrew Morton, Marco Elver
Cc: Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov,
Oscar Salvador, Vlastimil Babka, Caleb Kan, Caleb Kan, linux-mm,
linux-kernel, kasan-dev, kernel-team, Matt Fleming
From: Matt Fleming <mfleming@cloudflare.com>
On KASAN machines stack depot can exhaust stack_depot_max_pools. Frame
addresses for some architectures (currently x86-64 and arm64) can be
losslessly compressed when stored in stackdepot, thereby reducing the
number of pools and saving memory.
When every frame of a persistent record can be compressed with the arch
hooks, store the 32-bit compressed frames in a new 'compressed_entries'
union member and mark the record STACK_RECORD_COMPRESSED. Narrow 'size'
to u16 and add a u16 'flags' field so the 32-byte header size is
unchanged (CONFIG_STACKDEPOT_MAX_FRAMES is at most 256). A record with
any frame that cannot be compressed keeps all its frames uncompressed.
Evictable records (saved with STACK_DEPOT_FLAG_GET) are not compressed
because those records always allocate the maximum size anyway and
compressing frames doesn't shrink the allocation.
The hash is still computed over the uncompressed frames, so identical
traces deduplicate regardless of representation.
Record sizes for common frame counts:
frames uncompressed compressed reduction
------ ------------ ---------- ---------
8 96 B 64 B 33.3%
16 160 B 96 B 40.0%
28 256 B 144 B 43.8%
32 288 B 160 B 44.4%
64 544 B 288 B 47.1%
On production KASAN hosts this reduced the number of stack depot pools
per 1000 stacks:
arch uncompressed compressed saving
------ ------------ ---------- ------
x86-64 15.80 9.25 41.5%
arm64 15.97 11.64 27.1%
arm64 saves less because we run a lot of BPF programs whose code
addresses can't be compressed.
Suggested-by: Marco Elver <elver@google.com>
Signed-off-by: Matt Fleming <mfleming@cloudflare.com>
---
include/linux/stackdepot.h | 8 ++-
lib/stackdepot.c | 101 ++++++++++++++++++++++++++++++----
lib/tests/stackdepot_kunit.c | 103 ++++++++++++++++++++++++++++++++++-
3 files changed, 199 insertions(+), 13 deletions(-)
diff --git a/include/linux/stackdepot.h b/include/linux/stackdepot.h
index dcc2e1d21f92..de42f6540266 100644
--- a/include/linux/stackdepot.h
+++ b/include/linux/stackdepot.h
@@ -20,6 +20,7 @@
#ifndef _LINUX_STACKDEPOT_H
#define _LINUX_STACKDEPOT_H
+#include <linux/bits.h>
#include <linux/gfp.h>
typedef u32 depot_stack_handle_t;
@@ -50,14 +51,19 @@ union handle_parts {
};
};
+/* Frames are compressed into @compressed_entries; see asm/stackdepot.h. */
+#define STACK_RECORD_COMPRESSED BIT(0)
+
struct stack_record {
struct list_head hash_list; /* Links in the hash table */
u32 hash; /* Hash in hash table */
- u32 size; /* Number of stored frames */
+ u16 size; /* Number of stored frames */
+ u16 flags; /* STACK_RECORD_* */
union handle_parts handle; /* Constant after initialization */
refcount_t count;
union {
unsigned long entries[CONFIG_STACKDEPOT_MAX_FRAMES]; /* Frames */
+ u32 compressed_entries[CONFIG_STACKDEPOT_MAX_FRAMES];
struct {
/*
* An important invariant of the implementation is to
diff --git a/lib/stackdepot.c b/lib/stackdepot.c
index 8f1aaa0c6d88..f323f1e97e12 100644
--- a/lib/stackdepot.c
+++ b/lib/stackdepot.c
@@ -36,6 +36,8 @@
#include <linux/memblock.h>
#include <linux/kasan-enabled.h>
+#include <asm/stackdepot.h>
+
/*
* The pool_index is offset by 1 so the first record does not have a 0 handle.
*/
@@ -94,6 +96,7 @@ static const char *const counter_names[] = {
[DEPOT_COUNTER_PERSIST_BYTES] = "persistent_bytes",
};
static_assert(ARRAY_SIZE(counter_names) == DEPOT_COUNTER_COUNT);
+static_assert(CONFIG_STACKDEPOT_MAX_FRAMES <= U16_MAX);
static int __init disable_stack_depot(char *str)
{
@@ -419,9 +422,37 @@ static struct stack_record *depot_pop_free(void)
return stack;
}
-static inline size_t depot_stack_record_size(struct stack_record *s, unsigned int nr_entries)
+/* Check whether every frame of a stack can be stored in 32 bits. */
+static bool depot_entries_compressible(const unsigned long *entries,
+ unsigned int nr_entries)
+{
+ u32 payload;
+
+ for (unsigned int i = 0; i < nr_entries; i++) {
+ if (!arch_stack_depot_frame_try_compress(entries[i], &payload))
+ return false;
+ }
+ return true;
+}
+
+static inline unsigned long depot_stack_entry(const struct stack_record *stack,
+ unsigned int i)
{
- const size_t used = flex_array_size(s, entries, nr_entries);
+ unsigned long frame;
+
+ if (!(stack->flags & STACK_RECORD_COMPRESSED))
+ return stack->entries[i];
+ arch_stack_depot_frame_decompress(stack->compressed_entries[i], &frame);
+ return frame;
+}
+
+static inline size_t depot_stack_record_size(struct stack_record *s,
+ unsigned int nr_entries,
+ bool compressed)
+{
+ const size_t used = compressed ?
+ flex_array_size(s, compressed_entries, nr_entries) :
+ flex_array_size(s, entries, nr_entries);
const size_t unused = sizeof(s->entries) - used;
WARN_ON_ONCE(sizeof(s->entries) < used);
@@ -436,6 +467,7 @@ depot_alloc_stack(unsigned long *entries, unsigned int nr_entries, u32 hash, dep
{
struct stack_record *stack = NULL;
size_t record_size;
+ bool compressed = false;
lockdep_assert_held(&pool_lock);
@@ -452,10 +484,13 @@ depot_alloc_stack(unsigned long *entries, unsigned int nr_entries, u32 hash, dep
* Evictable entries have to allocate the max. size so they may
* safely be re-used by differently sized allocations.
*/
- record_size = depot_stack_record_size(stack, CONFIG_STACKDEPOT_MAX_FRAMES);
+ record_size = depot_stack_record_size(stack,
+ CONFIG_STACKDEPOT_MAX_FRAMES,
+ false);
stack = depot_pop_free();
} else {
- record_size = depot_stack_record_size(stack, nr_entries);
+ compressed = depot_entries_compressible(entries, nr_entries);
+ record_size = depot_stack_record_size(stack, nr_entries, compressed);
}
if (!stack) {
@@ -468,7 +503,16 @@ depot_alloc_stack(unsigned long *entries, unsigned int nr_entries, u32 hash, dep
stack->hash = hash;
stack->size = nr_entries;
/* stack->handle is already filled in by depot_pop_free_pool(). */
- memcpy(stack->entries, entries, flex_array_size(stack, entries, nr_entries));
+ if (compressed) {
+ stack->flags = STACK_RECORD_COMPRESSED;
+ for (unsigned int i = 0; i < nr_entries; i++)
+ arch_stack_depot_frame_try_compress(entries[i],
+ &stack->compressed_entries[i]);
+ } else {
+ stack->flags = 0;
+ memcpy(stack->entries, entries,
+ flex_array_size(stack, entries, nr_entries));
+ }
if (flags & STACK_DEPOT_FLAG_GET) {
refcount_set(&stack->count, 1);
@@ -588,6 +632,20 @@ int stackdepot_memcmp(const unsigned long *u1, const unsigned long *u2,
return 0;
}
+static inline bool depot_stack_equal(const struct stack_record *stack,
+ const unsigned long *entries,
+ unsigned int size)
+{
+ if (!(stack->flags & STACK_RECORD_COMPRESSED))
+ return !stackdepot_memcmp(entries, stack->entries, size);
+
+ for (unsigned int i = 0; i < size; i++) {
+ if (depot_stack_entry(stack, i) != entries[i])
+ return false;
+ }
+ return true;
+}
+
/* Finds a stack in a bucket of the hash table. */
static inline struct stack_record *find_stack(struct list_head *bucket,
unsigned long *entries, int size,
@@ -615,7 +673,7 @@ static inline struct stack_record *find_stack(struct list_head *bucket,
* management state unioned with @entries. The refcount is zero
* in that case and the below refcount_inc_not_zero() will fail.
*/
- if (data_race(stackdepot_memcmp(entries, stack->entries, size)))
+ if (!data_race(depot_stack_equal(stack, entries, size)))
continue;
/*
@@ -761,7 +819,8 @@ struct stack_record *__stack_depot_get_stack_record(depot_stack_handle_t handle)
static void depot_copy_entries(const struct stack_record *stack,
unsigned long *entries, unsigned int nr_entries)
{
- memcpy(entries, stack->entries, nr_entries * sizeof(*entries));
+ for (unsigned int i = 0; i < nr_entries; i++)
+ entries[i] = depot_stack_entry(stack, i);
/* stackdepot.c is not instrumented; tell KMSAN the copy is initialized. */
kmsan_unpoison_memory(entries, nr_entries * sizeof(*entries));
}
@@ -833,9 +892,15 @@ EXPORT_SYMBOL_GPL(stack_depot_put);
void stack_depot_print(depot_stack_handle_t handle)
{
struct stack_record *stack = depot_print_fetch(handle);
+ unsigned long frame;
- if (stack)
- stack_trace_print(stack->entries, stack->size, 0);
+ if (!stack)
+ return;
+ for (unsigned int i = 0; i < stack->size; i++) {
+ frame = depot_stack_entry(stack, i);
+ kmsan_unpoison_memory(&frame, sizeof(frame));
+ stack_trace_print(&frame, 1, 0);
+ }
}
EXPORT_SYMBOL_GPL(stack_depot_print);
@@ -843,9 +908,23 @@ int stack_depot_snprint(depot_stack_handle_t handle, char *buf, size_t size,
int spaces)
{
struct stack_record *stack = depot_print_fetch(handle);
+ unsigned int generated, total = 0;
+ unsigned long frame;
- return stack ? stack_trace_snprint(buf, size, stack->entries,
- stack->size, spaces) : 0;
+ if (!stack)
+ return 0;
+ for (unsigned int i = 0; i < stack->size && size; i++) {
+ frame = depot_stack_entry(stack, i);
+ kmsan_unpoison_memory(&frame, sizeof(frame));
+ generated = stack_trace_snprint(buf, size, &frame, 1, spaces);
+ total += generated;
+ /* Stop once @buf is full, as stack_trace_snprint() does. */
+ if (generated >= size)
+ break;
+ buf += generated;
+ size -= generated;
+ }
+ return total;
}
EXPORT_SYMBOL_GPL(stack_depot_snprint);
diff --git a/lib/tests/stackdepot_kunit.c b/lib/tests/stackdepot_kunit.c
index dc3f8e207bdd..abcc34844125 100644
--- a/lib/tests/stackdepot_kunit.c
+++ b/lib/tests/stackdepot_kunit.c
@@ -152,6 +152,98 @@ static void stackdepot_frame_arm64(struct kunit *test)
}
#endif /* CONFIG_ARM64 */
+#if (defined(CONFIG_X86_64) && !defined(CONFIG_UML)) || defined(CONFIG_ARM64)
+#define STACKDEPOT_TEST_COMPRESSED
+
+/* Return a distinct compressible frame for each @i. */
+static unsigned long stackdepot_compressible_frame(unsigned int i)
+{
+#ifdef CONFIG_ARM64
+ return stackdepot_arm64_frame(0x1000L * (i + 1));
+#else
+ return 0xffffffff81000000UL + 0x1000UL * (i + 1);
+#endif
+}
+
+/* Return a frame that cannot be compressed but has the same 32-bit payload. */
+static unsigned long stackdepot_raw_frame(unsigned int i)
+{
+#ifdef CONFIG_ARM64
+ return stackdepot_compressible_frame(i) + BIT_ULL(32);
+#else
+ return stackdepot_compressible_frame(i) & U32_MAX;
+#endif
+}
+
+static void stackdepot_compressed_frames(struct kunit *test)
+{
+ unsigned long compressible[3], mixed[3], fetched[3];
+ depot_stack_handle_t compressed_handle, mixed_handle;
+ struct stack_record *record;
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(compressible); i++)
+ compressible[i] = stackdepot_compressible_frame(0x100 + i);
+ memcpy(mixed, compressible, sizeof(mixed));
+ mixed[1] = stackdepot_raw_frame(0x101);
+
+ KUNIT_ASSERT_EQ(test, stack_depot_init(), 0);
+
+ compressed_handle = stack_depot_save(compressible,
+ ARRAY_SIZE(compressible), GFP_KERNEL);
+ KUNIT_ASSERT_NE(test, compressed_handle, (depot_stack_handle_t)0);
+ mixed_handle = stack_depot_save(mixed, ARRAY_SIZE(mixed), GFP_KERNEL);
+ KUNIT_ASSERT_NE(test, mixed_handle, (depot_stack_handle_t)0);
+ /* Equal payloads must not make different stacks compare equal. */
+ KUNIT_EXPECT_NE(test, compressed_handle, mixed_handle);
+ KUNIT_EXPECT_EQ(test, stack_depot_save(compressible, ARRAY_SIZE(compressible),
+ GFP_KERNEL), compressed_handle);
+
+ record = __stack_depot_get_stack_record(compressed_handle);
+ KUNIT_ASSERT_NOT_NULL(test, record);
+ KUNIT_EXPECT_TRUE(test, record->flags & STACK_RECORD_COMPRESSED);
+ record = __stack_depot_get_stack_record(mixed_handle);
+ KUNIT_ASSERT_NOT_NULL(test, record);
+ KUNIT_EXPECT_FALSE(test, record->flags & STACK_RECORD_COMPRESSED);
+
+ KUNIT_EXPECT_EQ(test, stack_depot_fetch_into(compressed_handle, fetched,
+ ARRAY_SIZE(fetched)),
+ (unsigned int)ARRAY_SIZE(compressible));
+ KUNIT_EXPECT_MEMEQ(test, fetched, compressible, sizeof(compressible));
+ KUNIT_EXPECT_EQ(test, stack_depot_fetch_into(mixed_handle, fetched,
+ ARRAY_SIZE(fetched)),
+ (unsigned int)ARRAY_SIZE(mixed));
+ KUNIT_EXPECT_MEMEQ(test, fetched, mixed, sizeof(mixed));
+}
+
+/* Evictable records are max-size, so their frames are not compressed. */
+static void stackdepot_get_record_not_compressed(struct kunit *test)
+{
+ unsigned long entries[3], fetched[3];
+ depot_stack_handle_t handle;
+ struct stack_record *record;
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(entries); i++)
+ entries[i] = stackdepot_compressible_frame(0x200 + i);
+
+ KUNIT_ASSERT_EQ(test, stack_depot_init(), 0);
+
+ handle = stack_depot_save_flags(entries, ARRAY_SIZE(entries), GFP_KERNEL,
+ STACK_DEPOT_FLAG_CAN_ALLOC |
+ STACK_DEPOT_FLAG_GET);
+ KUNIT_ASSERT_NE(test, handle, (depot_stack_handle_t)0);
+ record = __stack_depot_get_stack_record(handle);
+ KUNIT_ASSERT_NOT_NULL(test, record);
+ KUNIT_EXPECT_FALSE(test, record->flags & STACK_RECORD_COMPRESSED);
+ KUNIT_EXPECT_EQ(test, stack_depot_fetch_into(handle, fetched,
+ ARRAY_SIZE(fetched)),
+ (unsigned int)ARRAY_SIZE(entries));
+ KUNIT_EXPECT_MEMEQ(test, fetched, entries, sizeof(entries));
+ stack_depot_put(handle);
+}
+#endif
+
/* stack_depot_snprint() must match stack_trace_snprint(), with truncation. */
static void stackdepot_snprint_matches_stack_trace(struct kunit *test)
{
@@ -162,8 +254,13 @@ static void stackdepot_snprint_matches_stack_trace(struct kunit *test)
char *got, *want;
unsigned int i;
- for (i = 0; i < nr; i++)
+ for (i = 0; i < nr; i++) {
+#ifdef STACKDEPOT_TEST_COMPRESSED
+ entries[i] = stackdepot_compressible_frame(0x300 + i);
+#else
entries[i] = 0x301000UL + 0x1000UL * i;
+#endif
+ }
got = kunit_kzalloc(test, size, GFP_KERNEL);
want = kunit_kzalloc(test, size, GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, got);
@@ -195,6 +292,10 @@ static struct kunit_case stackdepot_test_cases[] = {
#endif
#ifdef CONFIG_ARM64
KUNIT_CASE(stackdepot_frame_arm64),
+#endif
+#ifdef STACKDEPOT_TEST_COMPRESSED
+ KUNIT_CASE(stackdepot_compressed_frames),
+ KUNIT_CASE(stackdepot_get_record_not_compressed),
#endif
{}
};
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v3 10/10] scripts/gdb: Decode compressed stack depot frames
2026-11-13 18:56 [PATCH v3 00/10] stackdepot: store compressed frames in stack records Matt Fleming
` (8 preceding siblings ...)
2026-11-13 18:57 ` [PATCH v3 09/10] stackdepot: Store compressed frames in stack records Matt Fleming
@ 2026-11-13 18:57 ` Matt Fleming
9 siblings, 0 replies; 11+ messages in thread
From: Matt Fleming @ 2026-11-13 18:57 UTC (permalink / raw)
To: Andrew Morton, Marco Elver
Cc: Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov,
Oscar Salvador, Vlastimil Babka, Caleb Kan, Caleb Kan, linux-mm,
linux-kernel, kasan-dev, kernel-team, Matt Fleming
From: Matt Fleming <mfleming@cloudflare.com>
lx-stack_depot_lookup reads stack_record->entries directly and would
misread compressed frames as full-width addresses. Export
STACK_RECORD_COMPRESSED through constants.py.in and decompress frames
the same way as arch_stack_depot_frame_decompress() on x86-64 and
arm64.
Other architectures never compress frames (the generic hook rejects
compression), but the helper raises an error if it meets compressed
frames there.
Signed-off-by: Matt Fleming <mfleming@cloudflare.com>
---
scripts/gdb/linux/constants.py.in | 5 +++++
scripts/gdb/linux/stackdepot.py | 23 +++++++++++++++++++----
2 files changed, 24 insertions(+), 4 deletions(-)
diff --git a/scripts/gdb/linux/constants.py.in b/scripts/gdb/linux/constants.py.in
index dab8b80bed69..da6b39c3dc13 100644
--- a/scripts/gdb/linux/constants.py.in
+++ b/scripts/gdb/linux/constants.py.in
@@ -22,6 +22,7 @@
#include <linux/radix-tree.h>
#include <linux/maple_tree.h>
#include <linux/slab.h>
+#include <linux/stackdepot.h>
#include <linux/threads.h>
#include <linux/vmalloc.h>
@@ -112,6 +113,10 @@ if IS_BUILTIN(CONFIG_PAGE_OWNER):
LX_GDBPARSED(PAGE_EXT_OWNER)
LX_GDBPARSED(PAGE_EXT_OWNER_ALLOCATED)
+/* linux/stackdepot.h */
+if IS_BUILTIN(CONFIG_STACKDEPOT):
+ LX_GDBPARSED(STACK_RECORD_COMPRESSED)
+
/* linux/slab.h */
LX_GDBPARSED(SLAB_RED_ZONE)
LX_GDBPARSED(SLAB_POISON)
diff --git a/scripts/gdb/linux/stackdepot.py b/scripts/gdb/linux/stackdepot.py
index aaa18af25cd1..f689cacb3cb7 100644
--- a/scripts/gdb/linux/stackdepot.py
+++ b/scripts/gdb/linux/stackdepot.py
@@ -20,6 +20,21 @@ def help():
gdb.write("Unrecognized command\n")
raise gdb.GdbError(t)
+def stack_record_entry(stack, i):
+ if not int(stack['flags']) & constants.LX_STACK_RECORD_COMPRESSED:
+ return int(stack['entries'][i])
+
+ # Mirror arch_stack_depot_frame_decompress().
+ payload = int(stack['compressed_entries'][i])
+ if utils.is_target_arch("x86-64"):
+ return 0xffffffff00000000 | payload
+ if utils.is_target_arch("aarch64"):
+ text = int(gdb.parse_and_eval("(unsigned long)_text"))
+ if payload & 0x80000000:
+ payload -= 1 << 32
+ return (text + payload) & 0xffffffffffffffff
+ raise gdb.GdbError("compressed stack depot frames are not supported on this architecture\n")
+
def stack_depot_fetch(handle):
global DEPOT_STACK_ALIGN
global stack_record_type
@@ -40,7 +55,7 @@ def stack_depot_fetch(handle):
pool_index = parts['pool_index_plus_1'] - 1
if pool_index >= pools_num:
gdb.write("pool index %d out of bounds (%d) for stack id 0x%08x\n" % (pool_index, pools_num, handle))
- return gdb.Value(0), 0
+ return [], 0
stack_pools = gdb.parse_and_eval('stack_pools')
@@ -48,10 +63,10 @@ def stack_depot_fetch(handle):
pool = stack_pools[pool_index]
stack = (pool + gdb.Value(offset).cast(utils.get_size_t_type())).cast(stack_record_type.get_type().pointer())
size = int(stack['size'].cast(utils.get_ulong_type()))
- return stack['entries'], size
+ return [stack_record_entry(stack, i) for i in range(size)], size
except Exception as e:
gdb.write("%s\n" % e)
- return gdb.Value(0), 0
+ return [], 0
def stack_depot_print(handle):
if not constants.LX_CONFIG_STACKDEPOT:
@@ -61,7 +76,7 @@ def stack_depot_print(handle):
if nr_entries > 0:
for i in range(0, nr_entries):
try:
- gdb.execute("x /i 0x%x" % (int(entries[i])))
+ gdb.execute("x /i 0x%x" % entries[i])
except Exception as e:
gdb.write("%s\n" % e)
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-11-13 18:57 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-13 18:56 [PATCH v3 00/10] stackdepot: store compressed frames in stack records Matt Fleming
2026-11-13 18:56 ` [PATCH v3 01/10] stackdepot: add caller-owned stack trace fetching Matt Fleming
2026-11-13 18:56 ` [PATCH v3 02/10] mm/kmemleak: use stack_depot_fetch_into() for leak reports Matt Fleming
2026-11-13 18:56 ` [PATCH v3 03/10] kmsan: use stack_depot_fetch_into() for origin reports Matt Fleming
2026-11-13 18:56 ` [PATCH v3 04/10] mm/slub: use stack_depot_fetch_into() for owner tracking Matt Fleming
2026-11-13 18:56 ` [PATCH v3 05/10] drm/locking: print deadlock backtraces with stack_depot_snprint() Matt Fleming
2026-11-13 18:56 ` [PATCH v3 06/10] mm/page_owner: Read stack traces with stack_depot_fetch_into() Matt Fleming
2026-11-13 18:56 ` [PATCH v3 07/10] stackdepot: Remove stack_depot_fetch() Matt Fleming
2026-11-13 18:57 ` [PATCH v3 08/10] stackdepot: add architecture hooks for compact frame storage Matt Fleming
2026-11-13 18:57 ` [PATCH v3 09/10] stackdepot: Store compressed frames in stack records Matt Fleming
2026-11-13 18:57 ` [PATCH v3 10/10] scripts/gdb: Decode compressed stack depot frames Matt Fleming
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).