Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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 a2e9a95d2190 ("kexec: Improve & fix crash_exclude_mem_range()
to handle overlapping ranges")
- commit 6dff31597264 ("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>

authored by

Coiby Xu and committed by
Andrew Morton
913e65a2 d337f452

+370
+11
kernel/Kconfig.kexec
··· 148 148 CRASH_DM_CRYPT cannot directly select CONFIGFS_FS, because that 149 149 is required to be built-in. 150 150 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 + 151 162 config CRASH_HOTPLUG 152 163 bool "Update the crash elfcorehdr on system configuration changes" 153 164 default y
+1
kernel/Makefile
··· 78 78 obj-$(CONFIG_KEXEC_CORE) += kexec_core.o 79 79 obj-$(CONFIG_CRASH_DUMP) += crash_core.o 80 80 obj-$(CONFIG_CRASH_DM_CRYPT) += crash_dump_dm_crypt.o 81 + obj-$(CONFIG_CRASH_DUMP_KUNIT_TEST) += crash_core_test.o 81 82 obj-$(CONFIG_KEXEC) += kexec.o 82 83 obj-$(CONFIG_KEXEC_FILE) += kexec_file.o 83 84 obj-$(CONFIG_KEXEC_ELF) += kexec_elf.o
+15
kernel/crash_core.c
··· 265 265 return 0; 266 266 } 267 267 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 + */ 268 282 int crash_exclude_mem_range(struct crash_mem *mem, 269 283 unsigned long long mstart, unsigned long long mend) 270 284 { ··· 338 324 339 325 return 0; 340 326 } 327 + EXPORT_SYMBOL_GPL(crash_exclude_mem_range); 341 328 342 329 ssize_t crash_get_memory_size(void) 343 330 {
+343
kernel/crash_core_test.c
··· 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");