Skip to content

Commit 913e65a

Browse files
coibyakpm00
authored andcommitted
crash: add KUnit tests for crash_exclude_mem_range
crash_exclude_mem_range seems to be a simple function but there have been multiple attempts to fix it, - commit a2e9a95 ("kexec: Improve & fix crash_exclude_mem_range() to handle overlapping ranges") - commit 6dff315 ("crash_core: fix and simplify the logic of crash_exclude_mem_range()") So add a set of unit tests to verify the correctness of current implementation. Shall we change the function in the future, the unit tests can also help prevent any regression. For example, we may make the function smarter by allocating extra crash_mem range on demand thus there is no need for the caller to foresee any memory range split or address -ENOMEM failure. The testing strategy is to verify the correctness of base case. The base case is there is one to-be-excluded range A and one existing range B. Then we can exhaust all possibilities of the position of A regarding B. For example, here are two combinations, Case: A is completely inside B (causes split) Original: [----B----] Exclude: {--A--} Result: [B1] .. [B2] Case: A overlaps B's left part Original: [----B----] Exclude: {---A---} Result: [..B..] In theory we can prove the correctness by induction, - Base case: crash_exclude_mem_range is correct in the case where n=1 (n is the number of existing ranges). - Inductive step: If crash_exclude_mem_range is correct for n=k existing ranges, then the it's also correct for n=k+1 ranges. But for the sake of simplicity, simply use unit tests to cover the base case together with two regression tests. Note most of the exclude_single_range_test() code is generated by Google Gemini with some small tweaks. The function specification, function body and the exhausting test strategy are presented as prompts. [akpm@linux-foundation.org: export crash_exclude_mem_range() to modules, for kernel/crash_core_test.c] Link: https://lkml.kernel.org/r/20250904093855.1180154-2-coxu@redhat.com Signed-off-by: Coiby Xu <coxu@redhat.com> Assisted-by: Google Gemini Cc: Baoquan He <bhe@redhat.com> Cc: Borislav Betkov <bp@alien8.de> Cc: Dave Young <dyoung@redhat.com> Cc: fuqiang wang <fuqiang.wang@easystack.cn> Cc: "H. Peter Anvin" <hpa@zytor.com> Cc: Ingo Molnar <mingo@redhat.com> Cc: Thomas Gleinxer <tglx@linutronix.de> Cc: Vivek Goyal <vgoyal@redhat.com> Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
1 parent d337f45 commit 913e65a

4 files changed

Lines changed: 370 additions & 0 deletions

File tree

kernel/Kconfig.kexec

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,17 @@ config CRASH_DM_CRYPT_CONFIGS
148148
CRASH_DM_CRYPT cannot directly select CONFIGFS_FS, because that
149149
is required to be built-in.
150150

151+
config CRASH_DUMP_KUNIT_TEST
152+
tristate "Unit Tests for kernel crash dumps" if !KUNIT_ALL_TESTS
153+
depends on CRASH_DUMP && KUNIT
154+
default KUNIT_ALL_TESTS
155+
help
156+
This option builds KUnit unit tests for kernel crash dumps. The unit
157+
tests will be used to verify the correctness of covered functions and
158+
also prevent any regression.
159+
160+
If unsure, say N.
161+
151162
config CRASH_HOTPLUG
152163
bool "Update the crash elfcorehdr on system configuration changes"
153164
default y

kernel/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ obj-$(CONFIG_CRASH_RESERVE) += crash_reserve.o
7878
obj-$(CONFIG_KEXEC_CORE) += kexec_core.o
7979
obj-$(CONFIG_CRASH_DUMP) += crash_core.o
8080
obj-$(CONFIG_CRASH_DM_CRYPT) += crash_dump_dm_crypt.o
81+
obj-$(CONFIG_CRASH_DUMP_KUNIT_TEST) += crash_core_test.o
8182
obj-$(CONFIG_KEXEC) += kexec.o
8283
obj-$(CONFIG_KEXEC_FILE) += kexec_file.o
8384
obj-$(CONFIG_KEXEC_ELF) += kexec_elf.o

kernel/crash_core.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,20 @@ int crash_prepare_elf64_headers(struct crash_mem *mem, int need_kernel_map,
265265
return 0;
266266
}
267267

268+
/**
269+
* crash_exclude_mem_range - exclude a mem range for existing ranges
270+
* @mem: mem->range contains an array of ranges sorted in ascending order
271+
* @mstart: the start of to-be-excluded range
272+
* @mend: the start of to-be-excluded range
273+
*
274+
* If you are unsure if a range split will happen, to avoid function call
275+
* failure because of -ENOMEM, always make sure
276+
* mem->max_nr_ranges == mem->nr_ranges + 1
277+
* before calling the function each time.
278+
*
279+
* returns 0 if a memory range is excluded successfully
280+
* return -ENOMEM if mem->ranges doesn't have space to hold split ranges
281+
*/
268282
int crash_exclude_mem_range(struct crash_mem *mem,
269283
unsigned long long mstart, unsigned long long mend)
270284
{
@@ -324,6 +338,7 @@ int crash_exclude_mem_range(struct crash_mem *mem,
324338

325339
return 0;
326340
}
341+
EXPORT_SYMBOL_GPL(crash_exclude_mem_range);
327342

328343
ssize_t crash_get_memory_size(void)
329344
{

kernel/crash_core_test.c

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
// SPDX-License-Identifier: GPL-2.0-only
2+
#include <kunit/test.h>
3+
#include <linux/crash_core.h> // For struct crash_mem and struct range if defined there
4+
5+
// Helper to create and initialize crash_mem
6+
static struct crash_mem *create_crash_mem(struct kunit *test, unsigned int max_ranges,
7+
unsigned int nr_initial_ranges,
8+
const struct range *initial_ranges)
9+
{
10+
struct crash_mem *mem;
11+
size_t alloc_size;
12+
13+
// Check if max_ranges can even hold initial_ranges
14+
if (max_ranges < nr_initial_ranges) {
15+
kunit_err(test, "max_ranges (%u) < nr_initial_ranges (%u)\n",
16+
max_ranges, nr_initial_ranges);
17+
return NULL;
18+
}
19+
20+
alloc_size = sizeof(struct crash_mem) + (size_t)max_ranges * sizeof(struct range);
21+
mem = kunit_kzalloc(test, alloc_size, GFP_KERNEL);
22+
if (!mem) {
23+
kunit_err(test, "Failed to allocate crash_mem\n");
24+
return NULL;
25+
}
26+
27+
mem->max_nr_ranges = max_ranges;
28+
mem->nr_ranges = nr_initial_ranges;
29+
if (initial_ranges && nr_initial_ranges > 0) {
30+
memcpy(mem->ranges, initial_ranges,
31+
nr_initial_ranges * sizeof(struct range));
32+
}
33+
34+
return mem;
35+
}
36+
37+
// Helper to compare ranges for assertions
38+
static void assert_ranges_equal(struct kunit *test,
39+
const struct range *actual_ranges,
40+
unsigned int actual_nr_ranges,
41+
const struct range *expected_ranges,
42+
unsigned int expected_nr_ranges,
43+
const char *case_name)
44+
{
45+
unsigned int i;
46+
47+
KUNIT_ASSERT_EQ_MSG(test, expected_nr_ranges, actual_nr_ranges,
48+
"%s: Number of ranges mismatch.", case_name);
49+
50+
for (i = 0; i < expected_nr_ranges; i++) {
51+
KUNIT_ASSERT_EQ_MSG(test, expected_ranges[i].start, actual_ranges[i].start,
52+
"%s: Range %u start mismatch.", case_name, i);
53+
KUNIT_ASSERT_EQ_MSG(test, expected_ranges[i].end, actual_ranges[i].end,
54+
"%s: Range %u end mismatch.", case_name, i);
55+
}
56+
}
57+
58+
// Structure for test parameters
59+
struct exclude_test_param {
60+
const char *description;
61+
unsigned long long exclude_start;
62+
unsigned long long exclude_end;
63+
unsigned int initial_max_ranges;
64+
const struct range *initial_ranges;
65+
unsigned int initial_nr_ranges;
66+
const struct range *expected_ranges;
67+
unsigned int expected_nr_ranges;
68+
int expected_ret;
69+
};
70+
71+
static void run_exclude_test_case(struct kunit *test, const struct exclude_test_param *params)
72+
{
73+
struct crash_mem *mem;
74+
int ret;
75+
76+
kunit_info(test, "%s", params->description);
77+
78+
mem = create_crash_mem(test, params->initial_max_ranges,
79+
params->initial_nr_ranges, params->initial_ranges);
80+
if (!mem)
81+
return; // Error already logged by create_crash_mem or kunit_kzalloc
82+
83+
ret = crash_exclude_mem_range(mem, params->exclude_start, params->exclude_end);
84+
85+
KUNIT_ASSERT_EQ_MSG(test, params->expected_ret, ret,
86+
"%s: Return value mismatch.", params->description);
87+
88+
if (params->expected_ret == 0) {
89+
assert_ranges_equal(test, mem->ranges, mem->nr_ranges,
90+
params->expected_ranges, params->expected_nr_ranges,
91+
params->description);
92+
} else {
93+
// If an error is expected, nr_ranges might still be relevant to check
94+
// depending on the exact point of failure. For ENOMEM on split,
95+
// nr_ranges shouldn't have changed.
96+
KUNIT_ASSERT_EQ_MSG(test, params->initial_nr_ranges,
97+
mem->nr_ranges,
98+
"%s: Number of ranges mismatch on error.",
99+
params->description);
100+
}
101+
}
102+
103+
/*
104+
* Test Strategy 1: One to-be-excluded range A and one existing range B.
105+
*
106+
* Exhaust all possibilities of the position of A regarding B.
107+
*/
108+
109+
static const struct range single_range_b = { .start = 100, .end = 199 };
110+
111+
static const struct exclude_test_param exclude_single_range_test_data[] = {
112+
{
113+
.description = "1.1: A is left of B, no overlap",
114+
.exclude_start = 10, .exclude_end = 50,
115+
.initial_max_ranges = 1,
116+
.initial_ranges = &single_range_b, .initial_nr_ranges = 1,
117+
.expected_ranges = &single_range_b, .expected_nr_ranges = 1,
118+
.expected_ret = 0,
119+
},
120+
{
121+
.description = "1.2: A's right boundary touches B's left boundary",
122+
.exclude_start = 10, .exclude_end = 99,
123+
.initial_max_ranges = 1,
124+
.initial_ranges = &single_range_b, .initial_nr_ranges = 1,
125+
.expected_ranges = &single_range_b, .expected_nr_ranges = 1,
126+
.expected_ret = 0,
127+
},
128+
{
129+
.description = "1.3: A overlaps B's left part",
130+
.exclude_start = 50, .exclude_end = 149,
131+
.initial_max_ranges = 1,
132+
.initial_ranges = &single_range_b, .initial_nr_ranges = 1,
133+
.expected_ranges = (const struct range[]){{ .start = 150, .end = 199 }},
134+
.expected_nr_ranges = 1,
135+
.expected_ret = 0,
136+
},
137+
{
138+
.description = "1.4: A is completely inside B",
139+
.exclude_start = 120, .exclude_end = 179,
140+
.initial_max_ranges = 2, // Needs space for split
141+
.initial_ranges = &single_range_b, .initial_nr_ranges = 1,
142+
.expected_ranges = (const struct range[]){
143+
{ .start = 100, .end = 119 },
144+
{ .start = 180, .end = 199 }
145+
},
146+
.expected_nr_ranges = 2,
147+
.expected_ret = 0,
148+
},
149+
{
150+
.description = "1.5: A overlaps B's right part",
151+
.exclude_start = 150, .exclude_end = 249,
152+
.initial_max_ranges = 1,
153+
.initial_ranges = &single_range_b, .initial_nr_ranges = 1,
154+
.expected_ranges = (const struct range[]){{ .start = 100, .end = 149 }},
155+
.expected_nr_ranges = 1,
156+
.expected_ret = 0,
157+
},
158+
{
159+
.description = "1.6: A's left boundary touches B's right boundary",
160+
.exclude_start = 200, .exclude_end = 250,
161+
.initial_max_ranges = 1,
162+
.initial_ranges = &single_range_b, .initial_nr_ranges = 1,
163+
.expected_ranges = &single_range_b, .expected_nr_ranges = 1,
164+
.expected_ret = 0,
165+
},
166+
{
167+
.description = "1.7: A is right of B, no overlap",
168+
.exclude_start = 250, .exclude_end = 300,
169+
.initial_max_ranges = 1,
170+
.initial_ranges = &single_range_b, .initial_nr_ranges = 1,
171+
.expected_ranges = &single_range_b, .expected_nr_ranges = 1,
172+
.expected_ret = 0,
173+
},
174+
{
175+
.description = "1.8: A completely covers B and extends beyond",
176+
.exclude_start = 50, .exclude_end = 250,
177+
.initial_max_ranges = 1,
178+
.initial_ranges = &single_range_b, .initial_nr_ranges = 1,
179+
.expected_ranges = NULL, .expected_nr_ranges = 0,
180+
.expected_ret = 0,
181+
},
182+
{
183+
.description = "1.9: A covers B and extends to the left",
184+
.exclude_start = 50, .exclude_end = 199, // A ends exactly where B ends
185+
.initial_max_ranges = 1,
186+
.initial_ranges = &single_range_b, .initial_nr_ranges = 1,
187+
.expected_ranges = NULL, .expected_nr_ranges = 0,
188+
.expected_ret = 0,
189+
},
190+
{
191+
.description = "1.10: A covers B and extends to the right",
192+
.exclude_start = 100, .exclude_end = 250, // A starts exactly where B starts
193+
.initial_max_ranges = 1,
194+
.initial_ranges = &single_range_b, .initial_nr_ranges = 1,
195+
.expected_ranges = NULL, .expected_nr_ranges = 0,
196+
.expected_ret = 0,
197+
},
198+
{
199+
.description = "1.11: A is identical to B",
200+
.exclude_start = 100, .exclude_end = 199,
201+
.initial_max_ranges = 1,
202+
.initial_ranges = &single_range_b, .initial_nr_ranges = 1,
203+
.expected_ranges = NULL, .expected_nr_ranges = 0,
204+
.expected_ret = 0,
205+
},
206+
{
207+
.description = "1.12: A is a point, left of B, no overlap",
208+
.exclude_start = 10, .exclude_end = 10,
209+
.initial_max_ranges = 1,
210+
.initial_ranges = &single_range_b, .initial_nr_ranges = 1,
211+
.expected_ranges = &single_range_b, .expected_nr_ranges = 1,
212+
.expected_ret = 0,
213+
},
214+
{
215+
.description = "1.13: A is a point, at start of B",
216+
.exclude_start = 100, .exclude_end = 100,
217+
.initial_max_ranges = 1,
218+
.initial_ranges = &single_range_b, .initial_nr_ranges = 1,
219+
.expected_ranges = (const struct range[]){{ .start = 101, .end = 199 }},
220+
.expected_nr_ranges = 1,
221+
.expected_ret = 0,
222+
},
223+
{
224+
.description = "1.14: A is a point, in middle of B (causes split)",
225+
.exclude_start = 150, .exclude_end = 150,
226+
.initial_max_ranges = 2, // Needs space for split
227+
.initial_ranges = &single_range_b, .initial_nr_ranges = 1,
228+
.expected_ranges = (const struct range[]){
229+
{ .start = 100, .end = 149 },
230+
{ .start = 151, .end = 199 }
231+
},
232+
.expected_nr_ranges = 2,
233+
.expected_ret = 0,
234+
},
235+
{
236+
.description = "1.15: A is a point, at end of B",
237+
.exclude_start = 199, .exclude_end = 199,
238+
.initial_max_ranges = 1,
239+
.initial_ranges = &single_range_b, .initial_nr_ranges = 1,
240+
.expected_ranges = (const struct range[]){{ .start = 100, .end = 198 }},
241+
.expected_nr_ranges = 1,
242+
.expected_ret = 0,
243+
},
244+
{
245+
.description = "1.16: A is a point, right of B, no overlap",
246+
.exclude_start = 250, .exclude_end = 250,
247+
.initial_max_ranges = 1,
248+
.initial_ranges = &single_range_b, .initial_nr_ranges = 1,
249+
.expected_ranges = &single_range_b, .expected_nr_ranges = 1,
250+
.expected_ret = 0,
251+
},
252+
// ENOMEM case for single range split
253+
{
254+
.description = "1.17: A completely inside B (split), no space (ENOMEM)",
255+
.exclude_start = 120, .exclude_end = 179,
256+
.initial_max_ranges = 1, // Not enough for split
257+
.initial_ranges = &single_range_b, .initial_nr_ranges = 1,
258+
.expected_ranges = NULL, // Not checked on error by assert_ranges_equal for content
259+
.expected_nr_ranges = 1, // Should remain unchanged
260+
.expected_ret = -ENOMEM,
261+
},
262+
};
263+
264+
265+
static void exclude_single_range_test(struct kunit *test)
266+
{
267+
size_t i;
268+
269+
for (i = 0; i < ARRAY_SIZE(exclude_single_range_test_data); i++) {
270+
kunit_log(KERN_INFO, test, "Running: %s", exclude_single_range_test_data[i].description);
271+
run_exclude_test_case(test, &exclude_single_range_test_data[i]);
272+
// KUnit will stop on first KUNIT_ASSERT failure within run_exclude_test_case
273+
}
274+
}
275+
276+
/*
277+
* Test Strategy 2: Regression test.
278+
*/
279+
280+
static const struct exclude_test_param exclude_range_regression_test_data[] = {
281+
// Test data from commit a2e9a95d2190
282+
{
283+
.description = "2.1: exclude low 1M",
284+
.exclude_start = 0, .exclude_end = (1 << 20) - 1,
285+
.initial_max_ranges = 3,
286+
.initial_ranges = (const struct range[]){
287+
{ .start = 0, .end = 0x3efff },
288+
{ .start = 0x3f000, .end = 0x3ffff },
289+
{ .start = 0x40000, .end = 0x9ffff }
290+
},
291+
.initial_nr_ranges = 3,
292+
.expected_nr_ranges = 0,
293+
.expected_ret = 0,
294+
},
295+
// Test data from https://lore.kernel.org/all/ZXrY7QbXAlxydsSC@MiWiFi-R3L-srv/T/#u
296+
{
297+
.description = "2.2: when range out of bound",
298+
.exclude_start = 100, .exclude_end = 200,
299+
.initial_max_ranges = 3,
300+
.initial_ranges = (const struct range[]){
301+
{ .start = 1, .end = 299 },
302+
{ .start = 401, .end = 1000 },
303+
{ .start = 1001, .end = 2000 }
304+
},
305+
.initial_nr_ranges = 3,
306+
.expected_ranges = NULL, // Not checked on error by assert_ranges_equal for content
307+
.expected_nr_ranges = 3, // Should remain unchanged
308+
.expected_ret = -ENOMEM
309+
},
310+
311+
};
312+
313+
314+
static void exclude_range_regression_test(struct kunit *test)
315+
{
316+
size_t i;
317+
318+
for (i = 0; i < ARRAY_SIZE(exclude_range_regression_test_data); i++) {
319+
kunit_log(KERN_INFO, test, "Running: %s", exclude_range_regression_test_data[i].description);
320+
run_exclude_test_case(test, &exclude_range_regression_test_data[i]);
321+
// KUnit will stop on first KUNIT_ASSERT failure within run_exclude_test_case
322+
}
323+
}
324+
325+
/*
326+
* KUnit Test Suite
327+
*/
328+
static struct kunit_case crash_exclude_mem_range_test_cases[] = {
329+
KUNIT_CASE(exclude_single_range_test),
330+
KUNIT_CASE(exclude_range_regression_test),
331+
{}
332+
};
333+
334+
static struct kunit_suite crash_exclude_mem_range_suite = {
335+
.name = "crash_exclude_mem_range_tests",
336+
.test_cases = crash_exclude_mem_range_test_cases,
337+
// .init and .exit can be NULL if not needed globally for the suite
338+
};
339+
340+
kunit_test_suite(crash_exclude_mem_range_suite);
341+
342+
MODULE_DESCRIPTION("crash dump KUnit test suite");
343+
MODULE_LICENSE("GPL");

0 commit comments

Comments
 (0)