[PATCH 2/4] mm/slub: preserve previous object lifetime in user tracking

From: Pengpeng Hou

Date: Tue Jun 16 2026 - 10:23:37 EST


SLAB_STORE_USER stores one allocation track and one free track for an
object. When that object is reused, the next allocation overwrites the
allocation track. If a stale pointer from the previous lifetime is later
freed or otherwise reported, the free/check report can contain the victim
allocation and the stale operation while the previous completed alloc/free
pair has already been overwritten.

Keep one previous completed lifetime in the existing user tracking
metadata. When an object is allocated and the current allocation/free
tracks both exist, copy that completed lifetime to the previous-lifetime
slots before recording the new allocation. Clear the current free track
when the new allocation begins so the current lifetime does not continue
to display a free from the old lifetime.

Print the previous object lifetime when it is available. This is
diagnostic information only; it does not infer semantic ownership or
identify the root cause of a use-after-free.

Signed-off-by: Pengpeng Hou <pengpeng@xxxxxxxxxxx>
---
mm/slub.c | 66 +++++++++++++++++++++++++++++++++++++++++++++----------
1 file changed, 55 insertions(+), 11 deletions(-)

diff --git a/mm/slub.c b/mm/slub.c
index 43d4febd5bf2..358f42e92207 100644
--- a/mm/slub.c
+++ b/mm/slub.c
@@ -327,7 +327,13 @@ struct track {
unsigned long when; /* When did the operation occur */
};

-enum track_item { TRACK_ALLOC, TRACK_FREE, TRACK_NR };
+enum track_item {
+ TRACK_ALLOC,
+ TRACK_FREE,
+ TRACK_PREV_ALLOC,
+ TRACK_PREV_FREE,
+ TRACK_NR,
+};

static inline unsigned int user_tracking_size(slab_flags_t flags)
{
@@ -1080,12 +1086,37 @@ static void set_track_update(struct kmem_cache *s, void *object,
p->when = jiffies;
}

-static __always_inline void set_track(struct kmem_cache *s, void *object,
- enum track_item alloc, unsigned long addr, gfp_t gfp_flags)
+static bool track_has_record(const struct track *t)
+{
+ return t->addr;
+}
+
+static void clear_track(struct kmem_cache *s, void *object,
+ enum track_item track)
+{
+ memset(get_track(s, object, track), 0, sizeof(struct track));
+}
+
+static void save_previous_lifetime(struct kmem_cache *s, void *object)
+{
+ struct track *alloc = get_track(s, object, TRACK_ALLOC);
+ struct track *free = get_track(s, object, TRACK_FREE);
+
+ if (!track_has_record(alloc) || !track_has_record(free))
+ return;
+
+ *get_track(s, object, TRACK_PREV_ALLOC) = *alloc;
+ *get_track(s, object, TRACK_PREV_FREE) = *free;
+}
+
+static __always_inline void set_alloc_track(struct kmem_cache *s, void *object,
+ unsigned long addr, gfp_t gfp_flags)
{
depot_stack_handle_t handle = set_track_prepare(gfp_flags);

- set_track_update(s, object, alloc, addr, handle);
+ save_previous_lifetime(s, object);
+ set_track_update(s, object, TRACK_ALLOC, addr, handle);
+ clear_track(s, object, TRACK_FREE);
}

static void init_tracking(struct kmem_cache *s, void *object)
@@ -1120,11 +1151,22 @@ static void print_track(const char *s, struct track *t, unsigned long pr_time)
void print_tracking(struct kmem_cache *s, void *object)
{
unsigned long pr_time = jiffies;
+ struct track *prev_alloc;
+ struct track *prev_free;
+
if (!(s->flags & SLAB_STORE_USER))
return;

print_track("Allocated", get_track(s, object, TRACK_ALLOC), pr_time);
print_track("Freed", get_track(s, object, TRACK_FREE), pr_time);
+
+ prev_alloc = get_track(s, object, TRACK_PREV_ALLOC);
+ prev_free = get_track(s, object, TRACK_PREV_FREE);
+ if (track_has_record(prev_alloc) || track_has_record(prev_free)) {
+ pr_err("Previous object lifetime:\n");
+ print_track("Previously allocated", prev_alloc, pr_time);
+ print_track("Previously freed", prev_free, pr_time);
+ }
}

static void print_slab_info(const struct slab *slab)
@@ -1371,10 +1413,12 @@ check_bytes_and_report(struct kmem_cache *s, struct slab *slab,
*
* [Metadata starts at object + s->inuse]
* - A. freelist pointer (if freeptr_outside_object)
- * - B. alloc tracking (SLAB_STORE_USER)
- * - C. free tracking (SLAB_STORE_USER)
- * - D. original request size (SLAB_KMALLOC && SLAB_STORE_USER)
- * - E. KASAN metadata (if enabled)
+ * - B. current alloc tracking (SLAB_STORE_USER)
+ * - C. current free tracking (SLAB_STORE_USER)
+ * - D. previous alloc tracking (SLAB_STORE_USER)
+ * - E. previous free tracking (SLAB_STORE_USER)
+ * - F. original request size (SLAB_KMALLOC && SLAB_STORE_USER)
+ * - G. KASAN metadata (if enabled)
*
* [Mandatory padding] (if CONFIG_SLUB_DEBUG && SLAB_RED_ZONE)
* - One mandatory debug word to guarantee a minimum poisoned gap
@@ -2029,8 +2073,8 @@ static inline void slab_pad_check(struct kmem_cache *s, struct slab *slab) {}
static inline int check_object(struct kmem_cache *s, struct slab *slab,
void *object, u8 val) { return 1; }
static inline depot_stack_handle_t set_track_prepare(gfp_t gfp_flags) { return 0; }
-static inline void set_track(struct kmem_cache *s, void *object,
- enum track_item alloc, unsigned long addr, gfp_t gfp_flags) {}
+static inline void set_alloc_track(struct kmem_cache *s, void *object,
+ unsigned long addr, gfp_t gfp_flags) {}
static inline void add_full(struct kmem_cache *s, struct kmem_cache_node *n,
struct slab *slab) {}
static inline void remove_full(struct kmem_cache *s, struct kmem_cache_node *n,
@@ -4522,7 +4566,7 @@ static void *___slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,

success:
if (kmem_cache_debug_flags(s, SLAB_STORE_USER))
- set_track(s, object, TRACK_ALLOC, addr, gfpflags);
+ set_alloc_track(s, object, addr, gfpflags);

return object;
}
--
2.43.0