mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Ravi Jonnalagadda <ravis.opensrc@gmail.com>
To: sj@kernel.org, akinobu.mita@gmail.com, damon@lists.linux.dev,
	linux-mm@kvack.org, linux-kernel@vger.kernel.org,
	linux-doc@vger.kernel.org
Cc: akpm@linux-foundation.org, corbet@lwn.net, bijan311@gmail.com,
	ajayjoshi@micron.com, honggyu.kim@sk.com, yunjeong.mun@sk.com,
	ravis.opensrc@gmail.com, rientjes@google.com, weixugc@google.com,
	jic23@kernel.org, gourry@gourry.net
Subject: [RFC PATCH v2 9/9] mm/damon/tests/drain-kunit: kunit for report rings and unified drain
Date: Thu, 10 Sep 2026 10:16:23 -0700	[thread overview]
Message-ID: <20260910171623.6638-10-ravis.opensrc@gmail.com> (raw)
In-Reply-To: <20260910171623.6638-1-ravis.opensrc@gmail.com>

Add kunit coverage for the report rings (the global page-fault ring and
a per-context perf ring) and the unified drain: ring inject/drain,
overflow safety on wrap, the damon_report_access() producer + probe_idx
routing (page-fault reports to the global pf ring, perf reports to the
reporting ctx's own perf ring), ring partition, the pf-ring owner
-EBUSY guard, vaddr thread-group-id filtering, and paddr crediting left
unfiltered.  Wire the suites into core.c (included after the drain and its
counters are defined) so CONFIG_DAMON_KUNIT_TEST=y builds them.

Cover the page-fault primitive and an event-driven probe sharing one
context: a report on each ring credits the access rate of the region
while only the probe report reaches the probe hits array feeding the
weighted sum.  Cover the address-space match by injecting a report whose
virtual address falls inside a region of a physical address space target
and whose physical address falls outside it, which is not credited.
Cover the hot and cold sides of the page-fault signal by reporting an
access on one of two regions and aging both.

Signed-off-by: Ravi Jonnalagadda <ravis.opensrc@gmail.com>
---
 mm/damon/core.c              |    2 +
 mm/damon/tests/.kunitconfig  |    4 +
 mm/damon/tests/drain-kunit.h | 1091 ++++++++++++++++++++++++++++++++++
 mm/damon/tests/perf-kunit.h  |  133 +++++
 4 files changed, 1230 insertions(+)
 create mode 100644 mm/damon/tests/drain-kunit.h
 create mode 100644 mm/damon/tests/perf-kunit.h

diff --git a/mm/damon/core.c b/mm/damon/core.c
index 37909420910e2..d14795993a888 100644
--- a/mm/damon/core.c
+++ b/mm/damon/core.c
@@ -5354,3 +5354,5 @@ struct damon_region *damon_search(unsigned long addr, struct pid *pid)
 subsys_initcall(damon_init);
 
 #include "tests/core-kunit.h"
+#include "tests/drain-kunit.h"
+#include "tests/perf-kunit.h"
diff --git a/mm/damon/tests/.kunitconfig b/mm/damon/tests/.kunitconfig
index 144d27e6ecc5c..8096aca135807 100644
--- a/mm/damon/tests/.kunitconfig
+++ b/mm/damon/tests/.kunitconfig
@@ -16,3 +16,7 @@ CONFIG_DAMON_SYSFS_KUNIT_TEST=y
 
 # enable DAMON_DEBUG_SANITY to catch any bug
 CONFIG_DAMON_DEBUG_SANITY=y
+
+# for global report rings (pf/perf) and per-PMU exclusivity tests
+CONFIG_PERF_EVENTS=y
+CONFIG_DAMON_PERF_SOURCE=y
diff --git a/mm/damon/tests/drain-kunit.h b/mm/damon/tests/drain-kunit.h
new file mode 100644
index 0000000000000..21ad90027eddb
--- /dev/null
+++ b/mm/damon/tests/drain-kunit.h
@@ -0,0 +1,1091 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * DAMON kunit tests for the unified paddr/vaddr report drain path.
+ *
+ * Included at the bottom of core.c (after kdamond_check_reported_accesses
+ * is defined) so the static function is visible.
+ */
+
+#ifdef CONFIG_DAMON_KUNIT_TEST
+
+#ifndef _DAMON_DRAIN_KUNIT_H
+#define _DAMON_DRAIN_KUNIT_H
+
+#include <kunit/test.h>
+#include <linux/damon.h>
+
+/*
+ * Reports are partitioned by probe_idx: probe_idx == DAMON_PROBE_IDX_NONE (0)
+ * lands in the global page_fault ring; probe_idx >= 1 lands in the owning
+ * context's per-context perf ring (ctx->perf_rings).  The drain dispatcher
+ * kdamond_check_reported_accesses() drains the perf ring only for a ctx that
+ * has event-driven probes.
+ *
+ * Attach a dummy event-driven probe AND allocate the ctx's per-ctx perf ring
+ * so the ctx both drains the perf ring and has ring storage for injected
+ * probe_idx>=1 reports.  In a live run damon_perf_probe_setup() allocates the
+ * ring; kunit has no real perf event, so it allocates directly.  Returns
+ * 0/-ENOMEM.
+ */
+static int damon_test_attach_perf_probe(struct damon_ctx *ctx)
+{
+	struct damon_probe *p = damon_new_probe();
+	int err;
+
+	if (!p)
+		return -ENOMEM;
+	p->event_driven = true;
+	damon_add_probe(ctx, p);
+
+	err = damon_ctx_alloc_perf_ring(ctx);
+	if (err)
+		return err;
+	return 0;
+}
+
+/*
+ * Mark @ctx as monitoring the physical address space.
+ *
+ * The drain matches a report against the address space of the context, which
+ * damon_target_has_pid() derives from ctx->ops.id, so a context whose targets
+ * carry no pid needs the paddr id for its reports to be matched by paddr.
+ * Only the id is set: the drain reads no other operations field, and these
+ * tests call it directly rather than through a kdamond.
+ */
+static void damon_test_set_paddr_ctx(struct damon_ctx *ctx)
+{
+	ctx->ops.id = DAMON_OPS_PADDR;
+}
+
+/*
+ * Test A: vaddr entry with a matching thread group id drains correctly.
+ *
+ * Create a vaddr ctx with target pid=current, region [0x1000, 0x2000).
+ * Inject entry: paddr=0, vaddr=0x1500, tgid=current tgid, probe_idx=1, ctx=ctx.
+ * After drain: probe_hits[0]==1 (probe_idx 1 stored 0-based), samples_drained
+ * increments.
+ */
+static void damon_test_unified_vaddr_match(struct kunit *test)
+{
+	struct damon_ctx *ctx;
+	struct damon_target *t;
+	struct damon_region *r;
+	struct damon_access_report rep = {
+		.paddr     = 0,
+		.vaddr     = 0x1500,
+		.probe_idx = 1,
+		.size      = PAGE_SIZE,
+	};
+	unsigned long before, after;
+	int hits;
+
+	ctx = damon_new_ctx();
+	if (!ctx)
+		kunit_skip(test, "ctx alloc failed");
+	if (damon_test_attach_perf_probe(ctx)) {
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "perf probe alloc failed");
+	}
+
+	t = damon_new_target();
+	if (!t) {
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "target alloc failed");
+	}
+	t->pid = get_pid(task_tgid(current));
+	if (!t->pid) {
+		damon_free_target(t);
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "pid alloc failed");
+	}
+	rep.tgid = task_tgid_vnr(current);
+	rep.ctx = ctx;	/* route to this ctx's per-ctx perf ring */
+
+	/*
+	 * Region must fully contain the report [vaddr, vaddr + size): a report
+	 * straddling the region end is rejected by the drain (correctly).  With
+	 * vaddr=0x1500 and size=PAGE_SIZE the region must reach >= 0x2500.
+	 */
+	r = damon_new_region(0x1000, 0x3000);
+	if (!r) {
+		put_pid(t->pid);
+		damon_free_target(t);
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "region alloc failed");
+	}
+	damon_add_region(r, t);
+	damon_add_target(ctx, t);
+
+	rep.report_jiffies = jiffies;
+	before = damon_get_samples_drained();
+	damon_report_access(&rep);
+	kdamond_check_reported_accesses(ctx);
+	after = damon_get_samples_drained();
+
+	hits = 0;
+	damon_for_each_region(r, t)
+		hits += r->probe_hits[0];
+
+	KUNIT_EXPECT_EQ(test, hits, 1);
+	KUNIT_EXPECT_GT(test, after, before);
+
+	damon_destroy_ctx(ctx);
+}
+
+/*
+ * Test B: a vaddr entry whose thread group id matches no target is dropped.
+ *
+ * Same setup but inject with a thread group id no target carries.
+ * probe_hits[0]==0 (probe_idx 1 stored 0-based), samples_no_region increments.
+ */
+static void damon_test_unified_vaddr_tgid_mismatch(struct kunit *test)
+{
+	struct damon_ctx *ctx;
+	struct damon_target *t;
+	struct damon_region *r;
+	struct damon_access_report rep = {
+		.paddr     = 0,
+		.vaddr     = 0x1500,
+		.tgid      = 9999,	/* matches no target */
+		.probe_idx = 1,
+		.size      = PAGE_SIZE,
+	};
+	unsigned long before, after;
+	int hits;
+
+	ctx = damon_new_ctx();
+	if (!ctx)
+		kunit_skip(test, "ctx alloc failed");
+	if (damon_test_attach_perf_probe(ctx)) {
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "perf probe alloc failed");
+	}
+
+	t = damon_new_target();
+	if (!t) {
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "target alloc failed");
+	}
+	t->pid = get_pid(task_tgid(current));
+	if (!t->pid) {
+		damon_free_target(t);
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "pid alloc failed");
+	}
+	rep.ctx = ctx;
+
+	/* Wide enough to contain the report; the id mismatch is the sole reject reason. */
+	r = damon_new_region(0x1000, 0x3000);
+	if (!r) {
+		put_pid(t->pid);
+		damon_free_target(t);
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "region alloc failed");
+	}
+	damon_add_region(r, t);
+	damon_add_target(ctx, t);
+
+	rep.report_jiffies = jiffies;
+	before = damon_get_samples_no_region();
+	damon_report_access(&rep);
+	kdamond_check_reported_accesses(ctx);
+	after = damon_get_samples_no_region();
+
+	hits = 0;
+	damon_for_each_region(r, t)
+		hits += r->probe_hits[0];
+
+	KUNIT_EXPECT_EQ(test, hits, 0);
+	KUNIT_EXPECT_GT(test, after, before);
+
+	damon_destroy_ctx(ctx);
+}
+
+/*
+ * Test C: paddr entry drains correctly (no id filter for paddr ops).
+ *
+ * Create a paddr ctx (no pid), region [0x10000, 0x20000).
+ * Inject: paddr=0x15000, vaddr=0, probe_idx=1, ctx=ctx.
+ * After drain: probe_hits[0]==1 (probe_idx 1 stored 0-based).
+ */
+static void damon_test_unified_paddr_no_regression(struct kunit *test)
+{
+	struct damon_ctx *ctx;
+	struct damon_target *t;
+	struct damon_region *r;
+	struct damon_access_report rep = {
+		.paddr     = 0x15000,
+		.vaddr     = 0,
+		.tid       = 0,
+		.probe_idx = 1,
+		.size      = PAGE_SIZE,
+	};
+	unsigned long before, after;
+	int hits;
+
+	ctx = damon_new_ctx();
+	if (!ctx)
+		kunit_skip(test, "ctx alloc failed");
+	if (damon_test_attach_perf_probe(ctx)) {
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "perf probe alloc failed");
+	}
+
+	t = damon_new_target();
+	if (!t) {
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "target alloc failed");
+	}
+	t->pid = NULL;	/* paddr target: no pid */
+	damon_test_set_paddr_ctx(ctx);
+	rep.ctx = ctx;
+
+	r = damon_new_region(0x10000, 0x20000);
+	if (!r) {
+		damon_free_target(t);
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "region alloc failed");
+	}
+	damon_add_region(r, t);
+	damon_add_target(ctx, t);
+
+	rep.report_jiffies = jiffies;
+	before = damon_get_samples_drained();
+	damon_report_access(&rep);
+	kdamond_check_reported_accesses(ctx);
+	after = damon_get_samples_drained();
+
+	hits = 0;
+	damon_for_each_region(r, t)
+		hits += r->probe_hits[0];
+
+	KUNIT_EXPECT_EQ(test, hits, 1);
+	KUNIT_EXPECT_GT(test, after, before);
+
+	damon_destroy_ctx(ctx);
+}
+
+/*
+ * Test pf-ring credit: a page_fault ctx drains the global pf ring.
+ *
+ * Create a paddr ctx with page_fault primitive enabled (no probes), region
+ * [0x10000, 0x20000).  Inject probe_idx=0 (DAMON_PROBE_IDX_NONE) paddr entry;
+ * such reports carry no ctx and land in the global pf ring.  After drain via
+ * the dispatcher: samples_drained increments (access rate credited) and
+ * probe_hits stays 0 (no probe attribution for probe_idx 0).
+ */
+static void damon_test_ring0_pagefault_credit(struct kunit *test)
+{
+	struct damon_ctx *ctx;
+	struct damon_target *t;
+	struct damon_region *r;
+	struct damon_access_report rep = {
+		.paddr     = 0x15000,
+		.vaddr     = 0,
+		.tid       = 0,
+		.probe_idx = 0,	/* DAMON_PROBE_IDX_NONE -> global pf ring */
+		.size      = PAGE_SIZE,
+	};
+	unsigned long before, after;
+	int hits;
+
+	ctx = damon_new_ctx();
+	if (!ctx)
+		kunit_skip(test, "ctx alloc failed");
+	/* page_fault primitive: routes drain to the global pf ring. */
+	ctx->sample_control.primitives_enabled.page_table = false;
+	ctx->sample_control.primitives_enabled.page_fault = true;
+
+	t = damon_new_target();
+	if (!t) {
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "target alloc failed");
+	}
+	t->pid = NULL;	/* paddr target: no pid */
+	damon_test_set_paddr_ctx(ctx);
+
+	r = damon_new_region(0x10000, 0x20000);
+	if (!r) {
+		damon_free_target(t);
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "region alloc failed");
+	}
+	damon_add_region(r, t);
+	damon_add_target(ctx, t);
+
+	rep.report_jiffies = jiffies;
+	before = damon_get_samples_drained();
+	damon_report_access(&rep);
+	kdamond_check_reported_accesses(ctx);
+	after = damon_get_samples_drained();
+
+	hits = 0;
+	damon_for_each_region(r, t)
+		hits += r->probe_hits[0];
+
+	KUNIT_EXPECT_EQ(test, hits, 0);	/* probe_idx 0: no probe_hits */
+	KUNIT_EXPECT_GT(test, after, before);
+
+	damon_destroy_ctx(ctx);
+}
+
+/*
+ * Test pf-ring cold-demote signal: complements ring0_pagefault_credit
+ * (which proves the HOT side).  This proves the COLD side.
+ *
+ * Create a paddr ctx with the page_fault primitive enabled (no probes) and one
+ * target with TWO regions: HOT [0x10000, 0x20000) and COLD [0x20000, 0x30000).
+ * Prime both regions to the same nonzero nr_accesses so aging is observable.
+ * Inject a single page_fault report (probe_idx=0 -> pf ring) hitting only the
+ * HOT region, then run the drain + zero-access-report aging pass:
+ *
+ *   kdamond_check_reported_accesses() credits the HOT region: it gets a
+ *   nr_accesses bump and access_reported=true; the COLD region is untouched.
+ *
+ *   kdamond_apply_zero_access_report() clears access_reported on the HOT
+ *   region (keeping its nr_accesses) and, because the COLD region was NOT
+ *   reported this tick, calls damon_update_region_access_rate(r, false) on it
+ *   (no credit) so the COLD region does not advance.
+ *
+ * After the cycle the HOT region's nr_accesses has pulled ahead of the COLD
+ * region's (HOT > COLD) -- the cold-demote signal a migrate_cold scheme acts
+ * on -- and access_reported is false on both regions again.
+ */
+static void damon_test_ring0_pagefault_cold_demote(struct kunit *test)
+{
+	struct damon_ctx *ctx;
+	struct damon_target *t;
+	struct damon_region *hot, *cold;
+	struct damon_access_report rep = {
+		.paddr     = 0x15000,	/* inside HOT region */
+		.vaddr     = 0,
+		.tid       = 0,
+		.probe_idx = 0,	/* DAMON_PROBE_IDX_NONE -> global pf ring */
+		.size      = PAGE_SIZE,
+	};
+
+	ctx = damon_new_ctx();
+	if (!ctx)
+		kunit_skip(test, "ctx alloc failed");
+	/* page_fault primitive: routes drain to the global pf ring. */
+	ctx->sample_control.primitives_enabled.page_table = false;
+	ctx->sample_control.primitives_enabled.page_fault = true;
+
+	t = damon_new_target();
+	if (!t) {
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "target alloc failed");
+	}
+	t->pid = NULL;	/* paddr target: no pid */
+	damon_test_set_paddr_ctx(ctx);
+
+	hot = damon_new_region(0x10000, 0x20000);
+	if (!hot) {
+		damon_free_target(t);
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "hot region alloc failed");
+	}
+	damon_add_region(hot, t);
+
+	cold = damon_new_region(0x20000, 0x30000);
+	if (!cold) {
+		damon_free_target(t);
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "cold region alloc failed");
+	}
+	damon_add_region(cold, t);
+	damon_add_target(ctx, t);
+
+	/* Prime both regions to the same nonzero access rate. */
+	hot->nr_accesses = 4;
+	cold->nr_accesses = 4;
+
+	/* Inject a pf-ring report hitting only the HOT region, then drain. */
+	rep.report_jiffies = jiffies;
+	damon_report_access(&rep);
+	kdamond_check_reported_accesses(ctx);
+
+	/* HOT reported this tick; COLD was not. */
+	KUNIT_EXPECT_TRUE(test, hot->access_reported);
+	KUNIT_EXPECT_FALSE(test, cold->access_reported);
+
+	/* Aging pass: HOT keeps its (credited) rate; COLD is not credited. */
+	kdamond_apply_zero_access_report(ctx);
+
+	/* Cold-demote signal: HOT pulled ahead of COLD. */
+	KUNIT_EXPECT_GT(test, hot->nr_accesses, cold->nr_accesses);
+	/* access_reported cleared on both after the zero-report pass. */
+	KUNIT_EXPECT_FALSE(test, hot->access_reported);
+	KUNIT_EXPECT_FALSE(test, cold->access_reported);
+
+	damon_destroy_ctx(ctx);
+}
+
+/*
+ * Test perf-ring credit: a perf ctx drains its own per-ctx perf ring.
+ *
+ * Create a paddr ctx with an event-driven probe (which allocates the ctx's
+ * per-ctx perf ring), region [0x10000, 0x20000).  Inject a probe_idx=1 paddr
+ * entry tagged with ctx, so it lands in that ctx's perf ring.  After drain:
+ * probe_hits[0]==1.
+ */
+static void damon_test_ring1_perf_credit(struct kunit *test)
+{
+	struct damon_ctx *ctx;
+	struct damon_target *t;
+	struct damon_region *r;
+	struct damon_access_report rep = {
+		.paddr     = 0x15000,
+		.vaddr     = 0,
+		.tid       = 0,
+		.probe_idx = 1,	/* -> per-ctx perf ring */
+		.size      = PAGE_SIZE,
+	};
+	unsigned long before, after;
+	int hits;
+
+	ctx = damon_new_ctx();
+	if (!ctx)
+		kunit_skip(test, "ctx alloc failed");
+	if (damon_test_attach_perf_probe(ctx)) {
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "perf probe alloc failed");
+	}
+
+	t = damon_new_target();
+	if (!t) {
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "target alloc failed");
+	}
+	t->pid = NULL;
+	damon_test_set_paddr_ctx(ctx);
+	rep.ctx = ctx;
+
+	r = damon_new_region(0x10000, 0x20000);
+	if (!r) {
+		damon_free_target(t);
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "region alloc failed");
+	}
+	damon_add_region(r, t);
+	damon_add_target(ctx, t);
+
+	rep.report_jiffies = jiffies;
+	before = damon_get_samples_drained();
+	damon_report_access(&rep);
+	kdamond_check_reported_accesses(ctx);
+	after = damon_get_samples_drained();
+
+	hits = 0;
+	damon_for_each_region(r, t)
+		hits += r->probe_hits[0];
+
+	KUNIT_EXPECT_EQ(test, hits, 1);
+	KUNIT_EXPECT_GT(test, after, before);
+
+	damon_destroy_ctx(ctx);
+}
+
+/*
+ * Test ring partition: a perf-only ctx does NOT consume a pf-ring entry.
+ *
+ * Inject a probe_idx=0 (global pf ring) entry, then drain with a perf-only ctx
+ * (event probe, page_fault disabled).  The perf ctx drains only its per-ctx
+ * perf ring, which is empty, so samples_drained must NOT change.  A subsequent
+ * pf ctx drain consumes the entry (samples_drained increments), proving the
+ * entry was partitioned into the global pf ring and left untouched by the perf
+ * drain.
+ */
+static void damon_test_ring_partition(struct kunit *test)
+{
+	struct damon_ctx *perf_ctx, *pf_ctx;
+	struct damon_target *t_perf, *t_pf;
+	struct damon_region *r;
+	struct damon_access_report rep = {
+		.paddr     = 0x15000,
+		.vaddr     = 0,
+		.tid       = 0,
+		.probe_idx = 0,	/* DAMON_PROBE_IDX_NONE -> global pf ring */
+		.size      = PAGE_SIZE,
+	};
+	unsigned long before, mid, after;
+
+	perf_ctx = damon_new_ctx();
+	if (!perf_ctx)
+		kunit_skip(test, "perf ctx alloc failed");
+	if (damon_test_attach_perf_probe(perf_ctx)) {
+		damon_destroy_ctx(perf_ctx);
+		kunit_skip(test, "perf probe alloc failed");
+	}
+	t_perf = damon_new_target();
+	if (!t_perf) {
+		damon_destroy_ctx(perf_ctx);
+		kunit_skip(test, "perf target alloc failed");
+	}
+	t_perf->pid = NULL;
+	damon_test_set_paddr_ctx(perf_ctx);
+	r = damon_new_region(0x10000, 0x20000);
+	if (!r) {
+		damon_free_target(t_perf);
+		damon_destroy_ctx(perf_ctx);
+		kunit_skip(test, "perf region alloc failed");
+	}
+	damon_add_region(r, t_perf);
+	damon_add_target(perf_ctx, t_perf);
+
+	pf_ctx = damon_new_ctx();
+	if (!pf_ctx) {
+		damon_destroy_ctx(perf_ctx);
+		kunit_skip(test, "pf ctx alloc failed");
+	}
+	pf_ctx->sample_control.primitives_enabled.page_table = false;
+	pf_ctx->sample_control.primitives_enabled.page_fault = true;
+	t_pf = damon_new_target();
+	if (!t_pf) {
+		damon_destroy_ctx(pf_ctx);
+		damon_destroy_ctx(perf_ctx);
+		kunit_skip(test, "pf target alloc failed");
+	}
+	t_pf->pid = NULL;
+	damon_test_set_paddr_ctx(pf_ctx);
+	r = damon_new_region(0x10000, 0x20000);
+	if (!r) {
+		damon_free_target(t_pf);
+		damon_destroy_ctx(pf_ctx);
+		damon_destroy_ctx(perf_ctx);
+		kunit_skip(test, "pf region alloc failed");
+	}
+	damon_add_region(r, t_pf);
+	damon_add_target(pf_ctx, t_pf);
+
+	rep.report_jiffies = jiffies;
+	before = damon_get_samples_drained();
+	damon_report_access(&rep);		/* lands in the global pf ring */
+	kdamond_check_reported_accesses(perf_ctx); /* drains perf ring only */
+	mid = damon_get_samples_drained();
+	kdamond_check_reported_accesses(pf_ctx);   /* drains pf ring */
+	after = damon_get_samples_drained();
+
+	KUNIT_EXPECT_EQ(test, mid, before);	/* perf drain left pf entry */
+	KUNIT_EXPECT_GT(test, after, mid);	/* pf drain consumed it */
+
+	damon_destroy_ctx(pf_ctx);
+	damon_destroy_ctx(perf_ctx);
+}
+
+/*
+ * Test per-context perf ring isolation.
+ *
+ * Two independent perf ctxs (each with its own event-driven probe and its own
+ * per-ctx perf ring) each receive one probe_idx=1 report tagged with their
+ * respective ctx.  Each ctx must credit exactly its own report and see nothing
+ * from the other: proof that perf reports route to the owning ctx's ring, and
+ * that two perf-driven ctxs coexist without a shared ring or a cross-ctx owner
+ * guard (unlike the global perf ring, which allowed only one perf drainer).
+ */
+static void damon_test_perf_per_ctx_isolation(struct kunit *test)
+{
+	struct damon_ctx *ctx_a, *ctx_b;
+	struct damon_target *ta, *tb;
+	struct damon_region *ra, *rb;
+	struct damon_access_report rep_a = {
+		.paddr = 0x15000, .probe_idx = 1, .size = PAGE_SIZE,
+	};
+	struct damon_access_report rep_b = {
+		.paddr = 0x35000, .probe_idx = 1, .size = PAGE_SIZE,
+	};
+	int hits_a, hits_b;
+
+	ctx_a = damon_new_ctx();
+	ctx_b = damon_new_ctx();
+	if (!ctx_a || !ctx_b) {
+		if (ctx_a)
+			damon_destroy_ctx(ctx_a);
+		if (ctx_b)
+			damon_destroy_ctx(ctx_b);
+		kunit_skip(test, "ctx alloc failed");
+	}
+	if (damon_test_attach_perf_probe(ctx_a) ||
+			damon_test_attach_perf_probe(ctx_b)) {
+		damon_destroy_ctx(ctx_a);
+		damon_destroy_ctx(ctx_b);
+		kunit_skip(test, "perf probe alloc failed");
+	}
+
+	ta = damon_new_target();
+	tb = damon_new_target();
+	if (!ta || !tb) {
+		if (ta)
+			damon_free_target(ta);
+		if (tb)
+			damon_free_target(tb);
+		damon_destroy_ctx(ctx_a);
+		damon_destroy_ctx(ctx_b);
+		kunit_skip(test, "target alloc failed");
+	}
+	ta->pid = NULL;
+	tb->pid = NULL;
+	damon_test_set_paddr_ctx(ctx_a);
+	damon_test_set_paddr_ctx(ctx_b);
+
+	ra = damon_new_region(0x10000, 0x20000);	/* holds rep_a paddr */
+	rb = damon_new_region(0x30000, 0x40000);	/* holds rep_b paddr */
+	if (!ra || !rb) {
+		if (ra)
+			damon_free_region(ra);
+		if (rb)
+			damon_free_region(rb);
+		damon_free_target(ta);
+		damon_free_target(tb);
+		damon_destroy_ctx(ctx_a);
+		damon_destroy_ctx(ctx_b);
+		kunit_skip(test, "region alloc failed");
+	}
+	damon_add_region(ra, ta);
+	damon_add_target(ctx_a, ta);
+	damon_add_region(rb, tb);
+	damon_add_target(ctx_b, tb);
+
+	/* Each report is tagged with its owning ctx. */
+	rep_a.ctx = ctx_a;
+	rep_b.ctx = ctx_b;
+	rep_a.report_jiffies = jiffies;
+	rep_b.report_jiffies = jiffies;
+
+	/*
+	 * Report into both ctx rings, then drain each ctx.  ctx_a must credit
+	 * only rep_a; ctx_b must credit only rep_b -- no cross-talk, and no
+	 * -EBUSY from a second perf drainer.
+	 */
+	damon_report_access(&rep_a);
+	damon_report_access(&rep_b);
+	kdamond_check_reported_accesses(ctx_a);
+	kdamond_check_reported_accesses(ctx_b);
+
+	hits_a = 0;
+	damon_for_each_region(ra, ta)
+		hits_a += ra->probe_hits[0];
+	hits_b = 0;
+	damon_for_each_region(rb, tb)
+		hits_b += rb->probe_hits[0];
+
+	KUNIT_EXPECT_EQ(test, hits_a, 1);	/* ctx_a credited its own report */
+	KUNIT_EXPECT_EQ(test, hits_b, 1);	/* ctx_b credited its own report */
+
+	damon_destroy_ctx(ctx_a);
+	damon_destroy_ctx(ctx_b);
+}
+
+/*
+ * Test pf-ring single-owner claim (page_fault only).
+ *
+ * The global pf ring is a destructive SPSC channel with a single owner:
+ * two ctxs draining it in one batch must be rejected with -EBUSY.  The perf
+ * ring has no such batch-claim owner (it is per-ctx), so only the pf owner
+ * helper is exercised here.
+ */
+static void damon_test_pf_ring_owner_ebusy(struct kunit *test)
+{
+	struct damon_ctx *a, *b;
+	struct damon_ctx *arr[2];
+	struct damon_ctx *owner_pf;
+	int err;
+
+	a = damon_new_ctx();
+	b = damon_new_ctx();
+	if (!a || !b) {
+		if (a)
+			damon_destroy_ctx(a);
+		if (b)
+			damon_destroy_ctx(b);
+		kunit_skip(test, "ctx alloc failed");
+	}
+
+	/* Both drain the global pf ring: same-ring collision -> -EBUSY. */
+	a->sample_control.primitives_enabled.page_table = false;
+	a->sample_control.primitives_enabled.page_fault = true;
+	b->sample_control.primitives_enabled.page_table = false;
+	b->sample_control.primitives_enabled.page_fault = true;
+	arr[0] = a;
+	arr[1] = b;
+	owner_pf = NULL;
+	err = damon_claim_ring_owner_start(arr, 2, damon_drains_ring_pf,
+			&owner_pf);
+	KUNIT_EXPECT_EQ(test, err, -EBUSY);
+
+	/* Only a drains pf: single owner, claimed to a. */
+	b->sample_control.primitives_enabled.page_fault = false;
+	owner_pf = NULL;
+	err = damon_claim_ring_owner_start(arr, 2, damon_drains_ring_pf,
+			&owner_pf);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_PTR_EQ(test, owner_pf, a);
+
+	damon_destroy_ctx(a);
+	damon_destroy_ctx(b);
+}
+
+/*
+ * Test the queued/dropped return value, and that a ring-full drop is counted
+ * as ring-full rather than busy-guard.
+ *
+ * A per-context perf ring is private to its ctx, so a freshly created ctx
+ * starts with an empty ring nobody else writes to and the counts are exact.
+ * Preemption is held across the loop so every report targets the same CPU's
+ * ring, per the SPSC invariant damon_report_access() documents.
+ *
+ * A ring holds DAMON_REPORT_RING_SIZE - 1 entries (one slot is kept empty to
+ * distinguish full from empty), so exactly that many reports are queued and
+ * every one after that is dropped.
+ */
+static void damon_test_report_return_value(struct kunit *test)
+{
+	struct damon_ctx *ctx;
+	struct damon_access_report rep = {
+		.paddr = 0x15000, .probe_idx = 1, .size = PAGE_SIZE,
+	};
+	unsigned long full_before, busy_before;
+	unsigned int queued = 0, dropped = 0;
+	int i;
+
+	ctx = damon_new_ctx();
+	if (!ctx)
+		kunit_skip(test, "ctx alloc failed");
+	if (damon_test_attach_perf_probe(ctx)) {
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "perf probe alloc failed");
+	}
+	rep.ctx = ctx;
+
+	preempt_disable();
+	full_before = damon_get_report_ring_full();
+	busy_before = damon_get_report_busy_drop();
+
+	/* One past capacity, so the last iteration must be a drop. */
+	for (i = 0; i < DAMON_REPORT_RING_SIZE; i++) {
+		if (damon_report_access(&rep))
+			queued++;
+		else
+			dropped++;
+	}
+	preempt_enable();
+
+	KUNIT_EXPECT_EQ(test, queued, (unsigned int)DAMON_REPORT_RING_SIZE - 1);
+	KUNIT_EXPECT_EQ(test, dropped, 1u);
+	/* No NMI nests here, so the drop must be the full ring. */
+	KUNIT_EXPECT_GT(test, damon_get_report_ring_full(), full_before);
+	KUNIT_EXPECT_EQ(test, damon_get_report_busy_drop(), busy_before);
+
+	damon_destroy_ctx(ctx);
+}
+
+/*
+ * Test that draining restores capacity: fill the ring, drain it via the
+ * dispatcher, then report again and expect the report to be queued.
+ */
+static void damon_test_report_drain_restores_capacity(struct kunit *test)
+{
+	struct damon_ctx *ctx;
+	struct damon_target *t;
+	struct damon_region *r;
+	struct damon_access_report rep = {
+		.paddr = 0x15000, .probe_idx = 1, .size = PAGE_SIZE,
+	};
+	int i;
+
+	ctx = damon_new_ctx();
+	if (!ctx)
+		kunit_skip(test, "ctx alloc failed");
+	if (damon_test_attach_perf_probe(ctx)) {
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "perf probe alloc failed");
+	}
+
+	t = damon_new_target();
+	if (!t) {
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "target alloc failed");
+	}
+	t->pid = NULL;
+	damon_test_set_paddr_ctx(ctx);
+	rep.ctx = ctx;
+
+	r = damon_new_region(0x10000, 0x20000);
+	if (!r) {
+		damon_free_target(t);
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "region alloc failed");
+	}
+	damon_add_region(r, t);
+	damon_add_target(ctx, t);
+
+	/* Fill the ring: the last report is dropped. */
+	preempt_disable();
+	for (i = 0; i < DAMON_REPORT_RING_SIZE; i++)
+		damon_report_access(&rep);
+	KUNIT_EXPECT_FALSE(test, damon_report_access(&rep));
+	preempt_enable();
+
+	kdamond_check_reported_accesses(ctx);
+
+	/* Capacity is back. */
+	preempt_disable();
+	KUNIT_EXPECT_TRUE(test, damon_report_access(&rep));
+	preempt_enable();
+
+	damon_destroy_ctx(ctx);
+}
+
+/*
+ * Test that a page_fault report and an event-driven probe report credit the
+ * same region in one context.
+ *
+ * Create a paddr ctx with the page_fault primitive enabled AND a weighted
+ * event-driven probe, region [0x10000, 0x20000).  Inject one report on each
+ * ring: probe_idx=0 (pf ring) and probe_idx=1 (perf ring).  The drain
+ * dispatcher drains both, so the access rate is credited twice while only the
+ * probe report lands in probe_hits[].
+ */
+static void damon_test_pf_and_probe_one_ctx(struct kunit *test)
+{
+	struct damon_ctx *ctx;
+	struct damon_target *t;
+	struct damon_region *r;
+	struct damon_probe *p;
+	struct damon_access_report pf_rep = {
+		.paddr     = 0x15000,
+		.probe_idx = 0,	/* DAMON_PROBE_IDX_NONE -> global pf ring */
+		.size      = PAGE_SIZE,
+	};
+	struct damon_access_report probe_rep = {
+		.paddr     = 0x16000,
+		.probe_idx = 1,	/* -> per-ctx perf ring */
+		.size      = PAGE_SIZE,
+	};
+	unsigned long before, after;
+	int hits;
+
+	ctx = damon_new_ctx();
+	if (!ctx)
+		kunit_skip(test, "ctx alloc failed");
+	if (damon_test_attach_perf_probe(ctx)) {
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "perf probe alloc failed");
+	}
+	/* A nonzero weight selects the probe-weighted score. */
+	damon_for_each_probe(p, ctx)
+		p->weight = 1;
+	ctx->sample_control.primitives_enabled.page_table = false;
+	ctx->sample_control.primitives_enabled.page_fault = true;
+	probe_rep.ctx = ctx;
+
+	t = damon_new_target();
+	if (!t) {
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "target alloc failed");
+	}
+	t->pid = NULL;	/* paddr target: no pid */
+	damon_test_set_paddr_ctx(ctx);
+
+	r = damon_new_region(0x10000, 0x20000);
+	if (!r) {
+		damon_free_target(t);
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "region alloc failed");
+	}
+	damon_add_region(r, t);
+	damon_add_target(ctx, t);
+
+	pf_rep.report_jiffies = jiffies;
+	probe_rep.report_jiffies = jiffies;
+	before = damon_get_samples_drained();
+	damon_report_access(&pf_rep);
+	damon_report_access(&probe_rep);
+	kdamond_check_reported_accesses(ctx);
+	after = damon_get_samples_drained();
+
+	hits = 0;
+	damon_for_each_region(r, t)
+		hits += r->probe_hits[0];
+
+	/* Both reports credited the access rate. */
+	KUNIT_EXPECT_EQ(test, after - before, 2ul);
+	/* Only the probe report has a probe_hits[] slot. */
+	KUNIT_EXPECT_EQ(test, hits, 1);
+	/* The weighted sum sees the probe hit. */
+	damon_for_each_region(r, t)
+		KUNIT_EXPECT_EQ(test,
+				damon_probe_hits_wsum(r, false, false, ctx), 1u);
+
+	damon_destroy_ctx(ctx);
+}
+
+/*
+ * Test that a report is matched by the address space of the target rather than
+ * by which address it carries.
+ *
+ * Create a paddr ctx with the page_fault primitive, region
+ * [0x10000, 0x20000).  Inject a report whose vaddr falls inside that region
+ * and whose paddr falls outside it.  A paddr target matches the paddr, so the
+ * report finds no region and is counted as such.
+ */
+static void damon_test_report_addr_space_keyed(struct kunit *test)
+{
+	struct damon_ctx *ctx;
+	struct damon_target *t;
+	struct damon_region *r;
+	struct damon_access_report rep = {
+		.paddr     = 0x95000,	/* outside the region */
+		.vaddr     = 0x15000,	/* inside the region */
+		.tid       = 0,
+		.probe_idx = 0,
+		.size      = PAGE_SIZE,
+	};
+	unsigned long before, after;
+
+	ctx = damon_new_ctx();
+	if (!ctx)
+		kunit_skip(test, "ctx alloc failed");
+	ctx->sample_control.primitives_enabled.page_table = false;
+	ctx->sample_control.primitives_enabled.page_fault = true;
+
+	t = damon_new_target();
+	if (!t) {
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "target alloc failed");
+	}
+	t->pid = NULL;	/* paddr target: no pid */
+	damon_test_set_paddr_ctx(ctx);
+
+	r = damon_new_region(0x10000, 0x20000);
+	if (!r) {
+		damon_free_target(t);
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "region alloc failed");
+	}
+	damon_add_region(r, t);
+	damon_add_target(ctx, t);
+
+	rep.report_jiffies = jiffies;
+	before = damon_get_samples_no_region();
+	damon_report_access(&rep);
+	kdamond_check_reported_accesses(ctx);
+	after = damon_get_samples_no_region();
+
+	/* The vaddr was not used to match a paddr target. */
+	KUNIT_EXPECT_GT(test, after, before);
+	damon_for_each_region(r, t)
+		KUNIT_EXPECT_EQ(test, r->nr_accesses, 0u);
+
+	damon_destroy_ctx(ctx);
+}
+
+/*
+ * Test the hot side of the page-fault signal: a reported region scores hotter
+ * than a region that reported nothing.
+ *
+ * Create a paddr ctx with the page_fault primitive and one target with two
+ * regions.  Report an access on the first region only, over several sampling
+ * ticks.  Each tick credits the reported region and withholds credit from the
+ * other one, so the access rate of the first grows while the second stays at
+ * zero, and the hot score of the first is higher while the cold score is
+ * reversed.
+ */
+static void damon_test_pagefault_hot_promote(struct kunit *test)
+{
+	struct damon_ctx *ctx;
+	struct damon_target *t;
+	struct damon_region *hot, *cold;
+	struct damos scheme = {
+		.quota = { .weight_nr_accesses = 100, .weight_age = 0 },
+	};
+	struct damon_access_report rep = {
+		.paddr     = 0x15000,
+		.probe_idx = 0,
+		.size      = PAGE_SIZE,
+	};
+	int i;
+
+	ctx = damon_new_ctx();
+	if (!ctx)
+		kunit_skip(test, "ctx alloc failed");
+	ctx->sample_control.primitives_enabled.page_table = false;
+	ctx->sample_control.primitives_enabled.page_fault = true;
+
+	t = damon_new_target();
+	if (!t) {
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "target alloc failed");
+	}
+	t->pid = NULL;	/* paddr target: no pid */
+	damon_test_set_paddr_ctx(ctx);
+
+	hot = damon_new_region(0x10000, 0x20000);
+	cold = damon_new_region(0x20000, 0x30000);
+	if (!hot || !cold) {
+		if (hot)
+			damon_free_region(hot);
+		if (cold)
+			damon_free_region(cold);
+		damon_free_target(t);
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "region alloc failed");
+	}
+	damon_add_region(hot, t);
+	damon_add_region(cold, t);
+	damon_add_target(ctx, t);
+
+	/* Both regions start from a zero access rate. */
+	KUNIT_EXPECT_EQ(test, hot->nr_accesses, 0u);
+	KUNIT_EXPECT_EQ(test, cold->nr_accesses, 0u);
+
+	/*
+	 * One sampling tick per iteration: report on the hot region, drain the
+	 * ring, then run the zero-access pass over the regions that reported
+	 * nothing.
+	 */
+	for (i = 0; i < 3; i++) {
+		rep.report_jiffies = jiffies;
+		damon_report_access(&rep);
+		kdamond_check_reported_accesses(ctx);
+		kdamond_apply_zero_access_report(ctx);
+	}
+
+	KUNIT_EXPECT_EQ(test, hot->nr_accesses, 3u);
+	KUNIT_EXPECT_EQ(test, cold->nr_accesses, 0u);
+	KUNIT_EXPECT_GT(test, hot->nr_accesses, cold->nr_accesses);
+	KUNIT_EXPECT_GT(test, damon_hot_score(ctx, hot, &scheme),
+			damon_hot_score(ctx, cold, &scheme));
+	KUNIT_EXPECT_LT(test, damon_cold_score(ctx, hot, &scheme),
+			damon_cold_score(ctx, cold, &scheme));
+
+	damon_destroy_ctx(ctx);
+}
+
+static struct kunit_case damon_drain_test_cases[] = {
+	KUNIT_CASE(damon_test_unified_vaddr_match),
+	KUNIT_CASE(damon_test_unified_vaddr_tgid_mismatch),
+	KUNIT_CASE(damon_test_unified_paddr_no_regression),
+	KUNIT_CASE(damon_test_ring0_pagefault_credit),
+	KUNIT_CASE(damon_test_ring0_pagefault_cold_demote),
+	KUNIT_CASE(damon_test_ring1_perf_credit),
+	KUNIT_CASE(damon_test_ring_partition),
+	KUNIT_CASE(damon_test_perf_per_ctx_isolation),
+	KUNIT_CASE(damon_test_pf_ring_owner_ebusy),
+	KUNIT_CASE(damon_test_report_return_value),
+	KUNIT_CASE(damon_test_report_drain_restores_capacity),
+	KUNIT_CASE(damon_test_pf_and_probe_one_ctx),
+	KUNIT_CASE(damon_test_report_addr_space_keyed),
+	KUNIT_CASE(damon_test_pagefault_hot_promote),
+	{}
+};
+
+static struct kunit_suite damon_drain_test_suite = {
+	.name = "damon_drain",
+	.test_cases = damon_drain_test_cases,
+};
+kunit_test_suite(damon_drain_test_suite);
+
+#endif /* _DAMON_DRAIN_KUNIT_H */
+
+#endif	/* CONFIG_DAMON_KUNIT_TEST */
diff --git a/mm/damon/tests/perf-kunit.h b/mm/damon/tests/perf-kunit.h
new file mode 100644
index 0000000000000..08e8b911f4d94
--- /dev/null
+++ b/mm/damon/tests/perf-kunit.h
@@ -0,0 +1,133 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * DAMON kunit tests for the per-context perf report ring.
+ *
+ * Included at the bottom of core.c, after tests/drain-kunit.h, whose
+ * damon_test_attach_perf_probe() helper these tests reuse.
+ */
+
+#ifdef CONFIG_DAMON_KUNIT_TEST
+
+#ifndef _DAMON_PERF_KUNIT_H
+#define _DAMON_PERF_KUNIT_H
+
+#include <kunit/test.h>
+#include <linux/damon.h>
+
+/*
+ * A report with probe_idx >= 1 is enqueued into the ring of the context named
+ * by report->ctx, so these tests build a context with an allocated perf ring
+ * and point the injected reports at it.  A freshly allocated ring is empty,
+ * which makes the accepted and rejected counts below exact.
+ */
+
+/*
+ * Test A: perf ring basic write
+ *
+ * Inject reports into a context's perf ring via damon_report_access() and
+ * verify each one is accepted.
+ */
+static void damon_test_perf_ring_basic(struct kunit *test)
+{
+	struct damon_ctx *ctx;
+	struct damon_access_report report = {
+		.paddr = 0x1000, .size = PAGE_SIZE, .probe_idx = 1,
+	};
+	int i;
+
+	ctx = damon_new_ctx();
+	if (!ctx)
+		kunit_skip(test, "ctx alloc failed");
+	if (damon_test_attach_perf_probe(ctx)) {
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "perf ring alloc failed");
+	}
+	report.ctx = ctx;
+
+	for (i = 0; i < 3; i++)
+		KUNIT_EXPECT_TRUE(test, damon_report_access(&report));
+
+	damon_destroy_ctx(ctx);
+}
+
+/*
+ * Test B: ring overflow is reported and does not corrupt head/tail
+ *
+ * Fill a context's perf ring to capacity and verify that further writes are
+ * refused and counted rather than overwriting live entries.  The ring holds
+ * DAMON_REPORT_RING_SIZE - 1 entries, one slot being reserved to distinguish
+ * full from empty, so the last two of the writes below must be refused.
+ */
+static void damon_test_perf_ring_overflow_safety(struct kunit *test)
+{
+	struct damon_ctx *ctx;
+	struct damon_access_report report = {
+		.paddr = 0x3000, .size = PAGE_SIZE, .probe_idx = 1,
+	};
+	unsigned long overflow_before, overflow_after;
+	int queued = 0, refused = 0;
+	int i;
+
+	ctx = damon_new_ctx();
+	if (!ctx)
+		kunit_skip(test, "ctx alloc failed");
+	if (damon_test_attach_perf_probe(ctx)) {
+		damon_destroy_ctx(ctx);
+		kunit_skip(test, "perf ring alloc failed");
+	}
+	report.ctx = ctx;
+
+	/* Pinned so every write lands in the same CPU's ring. */
+	preempt_disable();
+	overflow_before = damon_get_report_overflow();
+
+	for (i = 0; i < DAMON_REPORT_RING_SIZE + 1; i++) {
+		if (damon_report_access(&report))
+			queued++;
+		else
+			refused++;
+	}
+
+	overflow_after = damon_get_report_overflow();
+	preempt_enable();
+
+	KUNIT_EXPECT_EQ(test, queued, DAMON_REPORT_RING_SIZE - 1);
+	KUNIT_EXPECT_EQ(test, refused, 2);
+	KUNIT_EXPECT_GT(test, overflow_after, overflow_before);
+
+	damon_destroy_ctx(ctx);
+}
+
+/*
+ * Test C: a perf report without an owning context is refused
+ *
+ * A report with probe_idx >= 1 but no ctx cannot be routed to a ring.  Verify
+ * it is refused instead of dereferenced, which is what an overflow arriving
+ * after its context's ring was freed looks like.
+ */
+static void damon_test_perf_report_requires_ctx(struct kunit *test)
+{
+	struct damon_access_report report = {
+		.paddr = 0x5000, .size = PAGE_SIZE, .probe_idx = 1,
+		.ctx = NULL,
+	};
+
+	KUNIT_EXPECT_FALSE(test, damon_report_access(&report));
+}
+
+static struct kunit_case damon_perf_test_cases[] = {
+	KUNIT_CASE(damon_test_perf_ring_basic),
+	KUNIT_CASE(damon_test_perf_ring_overflow_safety),
+	KUNIT_CASE(damon_test_perf_report_requires_ctx),
+	{}
+};
+
+static struct kunit_suite damon_perf_test_suite = {
+	.name = "damon_perf",
+	.test_cases = damon_perf_test_cases,
+};
+kunit_test_suite(damon_perf_test_suite);
+
+#endif /* _DAMON_PERF_KUNIT_H */
+
+#endif	/* CONFIG_DAMON_KUNIT_TEST */
-- 
2.43.0


  parent reply	other threads:[~2026-09-10 17:16 UTC|newest]

Thread overview: 12+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-10 17:16 [RFC PATCH v2 0/9] mm/damon: hardware-sampled access reports Ravi Jonnalagadda
2026-09-10 17:16 ` [RFC PATCH v2 1/9] mm/damon/vaddr: support page fault access check primitive Ravi Jonnalagadda
2026-09-10 17:16 ` [RFC PATCH v2 2/9] mm/damon/core: read the CPU number with preemption disabled Ravi Jonnalagadda
2026-09-10 17:16 ` [RFC PATCH v2 3/9] mm/damon/paddr: lock the folio for the page fault primitive rmap walk Ravi Jonnalagadda
2026-09-10 17:16 ` [RFC PATCH v2 4/9] mm/damon: add damos_node_eligible_mem_bp tracepoint Ravi Jonnalagadda
2026-09-10 17:16 ` [RFC PATCH v2 5/9] mm/damon/core: add per-probe-class report rings and unified drain Ravi Jonnalagadda
2026-09-10 17:16 ` [RFC PATCH v2 6/9] mm/damon: add perf-event overflow handler feeding the report ring Ravi Jonnalagadda
2026-09-10 17:16 ` [RFC PATCH v2 7/9] mm/damon/ops-common: use probe-weighted score when probe weights are set Ravi Jonnalagadda
2026-09-10 17:16 ` [RFC PATCH v2 8/9] mm/damon: add perf_event prep for PMU-driven hotness probes Ravi Jonnalagadda
2026-09-10 17:16 ` Ravi Jonnalagadda [this message]
2026-09-11  0:34 ` [RFC PATCH v2 0/9] mm/damon: hardware-sampled access reports SJ Park
2026-09-12  1:38 ` SJ Park

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260910171623.6638-10-ravis.opensrc@gmail.com \
    --to=ravis.opensrc@gmail.com \
    --cc=ajayjoshi@micron.com \
    --cc=akinobu.mita@gmail.com \
    --cc=akpm@linux-foundation.org \
    --cc=bijan311@gmail.com \
    --cc=corbet@lwn.net \
    --cc=damon@lists.linux.dev \
    --cc=gourry@gourry.net \
    --cc=honggyu.kim@sk.com \
    --cc=jic23@kernel.org \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=rientjes@google.com \
    --cc=sj@kernel.org \
    --cc=weixugc@google.com \
    --cc=yunjeong.mun@sk.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®