| // SPDX-License-Identifier: GPL-2.0-or-later | |
| /* | |
| * Copyright 2002-2007 H. Peter Anvin - All Rights Reserved | |
| * | |
| * Test RAID-6 recovery algorithms. | |
| */ | |
| #include <kunit/test.h> | |
| #include <linux/prandom.h> | |
| #include <linux/vmalloc.h> | |
| #include <linux/raid/pq.h> | |
| #include "../algos.h" | |
| MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING"); | |
| #define RAID6_KUNIT_SEED 42 | |
| #define RAID6_KUNIT_NUM_TEST_ITERS 10 | |
| #define RAID6_KUNIT_MAX_BUFFERS 64 /* Including P and Q */ | |
| #define RAID6_KUNIT_MAX_FAILURES 2 | |
| #define RAID6_KUNIT_MAX_BYTES PAGE_SIZE | |
| #define RAID6_KUNIT_ALLOC_BYTES SZ_16K | |
| static struct rnd_state rng; | |
| static void *test_buffers[RAID6_KUNIT_MAX_BUFFERS]; | |
| static void *aligned_buffers[RAID6_KUNIT_MAX_BUFFERS]; | |
| static void *test_recov_buffers[RAID6_KUNIT_MAX_FAILURES]; | |
| static size_t test_buflen; | |
| struct test_args { | |
| unsigned int recov_idx; | |
| const struct raid6_recov_calls *recov; | |
| unsigned int gen_idx; | |
| const struct raid6_calls *gen; | |
| }; | |
| static struct test_args args; | |
| static u32 rand32(void) | |
| { | |
| return prandom_u32_state(&rng); | |
| } | |
| /* Generate a random length that is a multiple of 512. */ | |
| static unsigned int random_length(unsigned int max_length) | |
| { | |
| return round_up((rand32() % max_length) + 1, 512); | |
| } | |
| static unsigned int random_nr_buffers(void) | |
| { | |
| return (rand32() % (RAID6_KUNIT_MAX_BUFFERS - (RAID6_MIN_DISKS - 1))) + | |
| RAID6_MIN_DISKS; | |
| } | |
| /* Generate a random alignment that is a multiple of 64. */ | |
| static unsigned int random_alignment(unsigned int max_alignment) | |
| { | |
| if (max_alignment == 0) | |
| return 0; | |
| return (rand32() % (max_alignment + 1)) & ~63; | |
| } | |
| static void makedata(int start, int stop) | |
| { | |
| int i; | |
| for (i = start; i <= stop; i++) | |
| prandom_bytes_state(&rng, test_buffers[i], test_buflen); | |
| } | |
| static char member_type(unsigned int nr_buffers, int d) | |
| { | |
| if (d == nr_buffers - 2) | |
| return 'P'; | |
| if (d == nr_buffers - 1) | |
| return 'Q'; | |
| return 'D'; | |
| } | |
| static void test_recover_one(struct kunit *test, unsigned int nr_buffers, | |
| unsigned int len, int faila, int failb) | |
| { | |
| const struct test_args *ta = test->param_value; | |
| void *dataptrs[RAID6_KUNIT_MAX_BUFFERS]; | |
| int i; | |
| if (faila > failb) | |
| swap(faila, failb); | |
| for (i = 0; i < RAID6_KUNIT_MAX_FAILURES; i++) | |
| memset(test_recov_buffers[i], 0xf0, test_buflen); | |
| memcpy(dataptrs, aligned_buffers, sizeof(dataptrs)); | |
| dataptrs[faila] = test_recov_buffers[0]; | |
| dataptrs[failb] = test_recov_buffers[1]; | |
| if (failb == nr_buffers - 1) { | |
| /* | |
| * We don't implement the data+Q failure scenario, since it | |
| * is equivalent to a RAID-5 failure (XOR, then recompute Q). | |
| */ | |
| if (WARN_ON_ONCE(faila != nr_buffers - 2)) | |
| return; | |
| /* P+Q failure. Just rebuild the syndrome. */ | |
| ta->gen->gen_syndrome(nr_buffers, len, dataptrs); | |
| } else if (failb == nr_buffers - 2) { | |
| /* data+P failure. */ | |
| ta->recov->datap(nr_buffers, len, faila, dataptrs); | |
| } else { | |
| /* data+data failure. */ | |
| ta->recov->data2(nr_buffers, len, faila, failb, dataptrs); | |
| } | |
| KUNIT_EXPECT_MEMEQ_MSG(test, aligned_buffers[faila], dataptrs[faila], | |
| len, | |
| "faila miscompared: %3d[%c] buffers %u len %u (failb=%3d[%c])\n", | |
| faila, member_type(nr_buffers, faila), | |
| nr_buffers, len, | |
| failb, member_type(nr_buffers, failb)); | |
| KUNIT_EXPECT_MEMEQ_MSG(test, aligned_buffers[failb], dataptrs[failb], | |
| len, | |
| "failb miscompared: %3d[%c] buffers %u len %u (faila=%3d[%c])\n", | |
| failb, member_type(nr_buffers, failb), | |
| nr_buffers, len, | |
| faila, member_type(nr_buffers, faila)); | |
| } | |
| static void test_recover(struct kunit *test, unsigned int nr_buffers, | |
| unsigned int len) | |
| { | |
| unsigned int nr_data = nr_buffers - 2; | |
| int iterations, i; | |
| /* Test P+Q recovery */ | |
| test_recover_one(test, nr_buffers, len, nr_data, nr_buffers - 1); | |
| /* Test data+P recovery */ | |
| for (i = 0; i < nr_buffers - 2; i++) | |
| test_recover_one(test, nr_buffers, len, i, nr_data); | |
| /* Double data failure is impossible with a single data disk */ | |
| if (nr_data == 1) | |
| return; | |
| /* Test data+data recovery using random sampling */ | |
| iterations = nr_buffers * 2; /* should provide good enough coverage */ | |
| for (i = 0; i < iterations; i++) { | |
| int faila = rand32() % nr_data, failb; | |
| do { | |
| failb = rand32() % nr_data; | |
| } while (failb == faila); | |
| test_recover_one(test, nr_buffers, len, faila, failb); | |
| } | |
| } | |
| /* Simulate rmw run */ | |
| static void test_rmw_one(struct kunit *test, unsigned int nr_buffers, | |
| unsigned int len, int p1, int p2) | |
| { | |
| const struct test_args *ta = test->param_value; | |
| ta->gen->xor_syndrome(nr_buffers, p1, p2, len, aligned_buffers); | |
| makedata(p1, p2); | |
| ta->gen->xor_syndrome(nr_buffers, p1, p2, len, aligned_buffers); | |
| test_recover(test, nr_buffers, len); | |
| } | |
| static void test_rmw(struct kunit *test, unsigned int nr_buffers, | |
| unsigned int len) | |
| { | |
| int iterations = nr_buffers / 2, i; | |
| for (i = 0; i < iterations; i++) { | |
| int p1 = rand32() % (nr_buffers - 2); | |
| int p2 = rand32() % (nr_buffers - 2); | |
| if (p2 < p1) | |
| swap(p1, p2); | |
| test_rmw_one(test, nr_buffers, len, p1, p2); | |
| } | |
| } | |
| static void raid6_test_one(struct kunit *test) | |
| { | |
| const struct test_args *ta = test->param_value; | |
| unsigned int nr_buffers = random_nr_buffers(); | |
| unsigned int len = random_length(RAID6_KUNIT_MAX_BYTES); | |
| unsigned int max_alignment; | |
| int i; | |
| /* Nuke syndromes */ | |
| memset(test_buffers[nr_buffers - 2], 0xee, test_buflen); | |
| memset(test_buffers[nr_buffers - 1], 0xee, test_buflen); | |
| /* | |
| * If we're not using the entire buffer size, inject randomize alignment | |
| * into the buffer. | |
| */ | |
| max_alignment = RAID6_KUNIT_MAX_BYTES - len; | |
| if (rand32() % 2 == 0) { | |
| /* Use random alignments mod 64 */ | |
| for (i = 0; i < nr_buffers; i++) | |
| aligned_buffers[i] = test_buffers[i] + | |
| random_alignment(max_alignment); | |
| } else { | |
| /* Go up to the guard page, to catch buffer overreads */ | |
| unsigned int align = test_buflen - len; | |
| for (i = 0; i < nr_buffers; i++) | |
| aligned_buffers[i] = test_buffers[i] + align; | |
| } | |
| /* Generate assumed good syndrome */ | |
| ta->gen->gen_syndrome(nr_buffers, len, aligned_buffers); | |
| test_recover(test, nr_buffers, len); | |
| if (ta->gen->xor_syndrome) | |
| test_rmw(test, nr_buffers, len); | |
| } | |
| static void raid6_test(struct kunit *test) | |
| { | |
| int i; | |
| for (i = 0; i < RAID6_KUNIT_NUM_TEST_ITERS; i++) | |
| raid6_test_one(test); | |
| } | |
| static void raid6_benchmark(struct kunit *test) | |
| { | |
| static const unsigned int nr_to_test[] = { | |
| 4, 5, 6, 7, 8, 10, 12, 15, 16, 32, | |
| }; | |
| static const unsigned int len_to_test[] = { | |
| SZ_4K, SZ_16K, | |
| }; | |
| unsigned int i, j, l; | |
| u64 t; | |
| if (!IS_ENABLED(CONFIG_RAID6_PQ_KUNIT_BENCHMARK)) | |
| kunit_skip(test, "not enabled"); | |
| /* warm-up */ | |
| for (i = 0; i < ARRAY_SIZE(nr_to_test); i++) { | |
| for (j = 0; j < ARRAY_SIZE(len_to_test); j++) { | |
| for (l = 0; l < 10; l++) { | |
| raid6_gen_syndrome(nr_to_test[i], | |
| len_to_test[j], test_buffers); | |
| } | |
| } | |
| } | |
| /* | |
| * Preferably this would be a loop over len_to_test, but the kunit | |
| * logging always adds a newline to each logged format string. | |
| */ | |
| static_assert(ARRAY_SIZE(len_to_test) == 2); | |
| kunit_info(test, " \t%5u bytes\t%5u bytes\n", | |
| len_to_test[0], len_to_test[1]); | |
| for (i = 0; i < ARRAY_SIZE(nr_to_test); i++) { | |
| unsigned int nr = nr_to_test[i]; | |
| u64 speed[ARRAY_SIZE(len_to_test)]; | |
| KUNIT_ASSERT_LE(test, nr, RAID6_KUNIT_MAX_BUFFERS); | |
| for (j = 0; j < ARRAY_SIZE(len_to_test); j++) { | |
| unsigned int len = len_to_test[j]; | |
| const unsigned long num_iters = 1000; | |
| KUNIT_ASSERT_GT(test, len, 0); | |
| KUNIT_ASSERT_LE(test, len, RAID6_KUNIT_ALLOC_BYTES); | |
| preempt_disable(); | |
| t = ktime_get_ns(); | |
| for (l = 0; l < num_iters; l++) | |
| raid6_gen_syndrome(nr_to_test[i], | |
| len_to_test[j], test_buffers); | |
| t = max(ktime_get_ns() - t, 1); | |
| preempt_enable(); | |
| speed[j] = div64_u64((u64)len * num_iters * nr, t); | |
| } | |
| static_assert(ARRAY_SIZE(len_to_test) == 2); | |
| kunit_info(test, "%3u disks:\t%5llu GB/s\t%5llu GB/s\n", | |
| nr, speed[0], speed[1]); | |
| } | |
| } | |
| static const void *raid6_gen_params(struct kunit *test, const void *prev, | |
| char *desc) | |
| { | |
| if (!prev) { | |
| memset(&args, 0, sizeof(args)); | |
| next_algo: | |
| args.recov_idx = 0; | |
| args.gen = raid6_algo_find(args.gen_idx); | |
| if (!args.gen) | |
| return NULL; | |
| } | |
| if (args.recov) | |
| args.recov_idx++; | |
| args.recov = raid6_recov_algo_find(args.recov_idx); | |
| if (!args.recov) { | |
| args.gen_idx++; | |
| goto next_algo; | |
| } | |
| snprintf(desc, KUNIT_PARAM_DESC_SIZE, "gen=%s recov=%s", | |
| args.gen->name, args.recov->name); | |
| return &args; | |
| } | |
| static struct kunit_case raid6_test_cases[] = { | |
| KUNIT_CASE_PARAM(raid6_test, raid6_gen_params), | |
| KUNIT_CASE(raid6_benchmark), | |
| {}, | |
| }; | |
| static int raid6_suite_init(struct kunit_suite *suite) | |
| { | |
| int i; | |
| prandom_seed_state(&rng, RAID6_KUNIT_SEED); | |
| /* | |
| * Allocate the test buffer using vmalloc() with a page-aligned length | |
| * so that it is immediately followed by a guard page. This allows | |
| * buffer overreads to be detected, even in assembly code. | |
| */ | |
| test_buflen = round_up(RAID6_KUNIT_ALLOC_BYTES, PAGE_SIZE); | |
| for (i = 0; i < RAID6_KUNIT_MAX_FAILURES; i++) { | |
| test_recov_buffers[i] = vmalloc(test_buflen); | |
| if (!test_recov_buffers[i]) | |
| goto out_free_recov_buffers; | |
| } | |
| for (i = 0; i < RAID6_KUNIT_MAX_BUFFERS; i++) { | |
| test_buffers[i] = vmalloc(test_buflen); | |
| if (!test_buffers[i]) | |
| goto out_free_buffers; | |
| } | |
| makedata(0, RAID6_KUNIT_MAX_BUFFERS - 1); | |
| return 0; | |
| out_free_buffers: | |
| for (i = 0; i < RAID6_KUNIT_MAX_BUFFERS; i++) | |
| vfree(test_buffers[i]); | |
| memset(test_buffers, 0, sizeof(test_buffers)); | |
| out_free_recov_buffers: | |
| for (i = 0; i < RAID6_KUNIT_MAX_FAILURES; i++) | |
| vfree(test_recov_buffers[i]); | |
| memset(test_recov_buffers, 0, sizeof(test_recov_buffers)); | |
| return -ENOMEM; | |
| } | |
| static void raid6_suite_exit(struct kunit_suite *suite) | |
| { | |
| int i; | |
| for (i = 0; i < RAID6_KUNIT_MAX_BUFFERS; i++) | |
| vfree(test_buffers[i]); | |
| memset(test_buffers, 0, sizeof(test_buffers)); | |
| for (i = 0; i < RAID6_KUNIT_MAX_FAILURES; i++) | |
| vfree(test_recov_buffers[i]); | |
| memset(test_recov_buffers, 0, sizeof(test_recov_buffers)); | |
| } | |
| static struct kunit_suite raid6_test_suite = { | |
| .name = "raid6", | |
| .test_cases = raid6_test_cases, | |
| .suite_init = raid6_suite_init, | |
| .suite_exit = raid6_suite_exit, | |
| }; | |
| kunit_test_suite(raid6_test_suite); | |
| MODULE_DESCRIPTION("Unit test for the RAID P/Q library functions"); | |
| MODULE_LICENSE("GPL"); |