lib/tests: Add randstruct KUnit test
authorKees Cook <kees@kernel.org>
Sun, 27 Apr 2025 00:21:01 +0000 (17:21 -0700)
committerKees Cook <kees@kernel.org>
Thu, 8 May 2025 16:42:40 +0000 (09:42 -0700)
Perform basic validation about layout randomization and initialization
tracking when using CONFIG_RANDSTRUCT=y. Tested using:

$ ./tools/testing/kunit/kunit.py run \
--kconfig_add CONFIG_RANDSTRUCT_FULL=y \
randstruct
[17:22:30] ================= randstruct (2 subtests) ==================
[17:22:30] [PASSED] randstruct_layout
[17:22:30] [PASSED] randstruct_initializers
[17:22:30] =================== [PASSED] randstruct ====================
[17:22:30] ============================================================
[17:22:30] Testing complete. Ran 2 tests: passed: 2
[17:22:30] Elapsed time: 5.091s total, 0.001s configuring, 4.974s building, 0.086s running

Adding "--make_option LLVM=1" can be used to test Clang, which also
passes.

Acked-by: David Gow <davidgow@google.com>
Signed-off-by: Kees Cook <kees@kernel.org>
MAINTAINERS
lib/Kconfig.debug
lib/tests/Makefile
lib/tests/randstruct_kunit.c [new file with mode: 0644]

index 3cbf9ac0d83f619ef873833b26a3aa3f29ff4161..dc535c67a745eef6dec5c0b5f5272c2985db188e 100644 (file)
@@ -12892,6 +12892,7 @@ F:      include/linux/overflow.h
 F:     include/linux/randomize_kstack.h
 F:     include/linux/ucopysize.h
 F:     kernel/configs/hardening.config
+F:     lib/tests/randstruct_kunit.c
 F:     lib/tests/usercopy_kunit.c
 F:     mm/usercopy.c
 F:     security/Kconfig.hardening
index f9051ab610d54358b21d61c141b737bb345b4cee..6479cec900c732098f00b58e4a8609b1ead48137 100644 (file)
@@ -2863,6 +2863,14 @@ config OVERFLOW_KUNIT_TEST
 
          If unsure, say N.
 
+config RANDSTRUCT_KUNIT_TEST
+       tristate "Test randstruct structure layout randomization at runtime" if !KUNIT_ALL_TESTS
+       depends on KUNIT
+       default KUNIT_ALL_TESTS
+       help
+         Builds unit tests for the checking CONFIG_RANDSTRUCT=y, which
+         randomizes structure layouts.
+
 config STACKINIT_KUNIT_TEST
        tristate "Test level of stack variable initialization" if !KUNIT_ALL_TESTS
        depends on KUNIT
index 5a4794c1826e7edfbf08695a3c8e39adab5bc378..56d6450144828ee7bfe27878d3fca33dc863fd3d 100644 (file)
@@ -35,6 +35,7 @@ obj-$(CONFIG_MEMCPY_KUNIT_TEST) += memcpy_kunit.o
 CFLAGS_overflow_kunit.o = $(call cc-disable-warning, tautological-constant-out-of-range-compare)
 obj-$(CONFIG_OVERFLOW_KUNIT_TEST) += overflow_kunit.o
 obj-$(CONFIG_PRINTF_KUNIT_TEST) += printf_kunit.o
+obj-$(CONFIG_RANDSTRUCT_KUNIT_TEST) += randstruct_kunit.o
 obj-$(CONFIG_SCANF_KUNIT_TEST) += scanf_kunit.o
 obj-$(CONFIG_SIPHASH_KUNIT_TEST) += siphash_kunit.o
 obj-$(CONFIG_SLUB_KUNIT_TEST) += slub_kunit.o
diff --git a/lib/tests/randstruct_kunit.c b/lib/tests/randstruct_kunit.c
new file mode 100644 (file)
index 0000000..c796c8f
--- /dev/null
@@ -0,0 +1,283 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Test cases for struct randomization, i.e. CONFIG_RANDSTRUCT=y.
+ *
+ * For example, see:
+ * "Running tests with kunit_tool" at Documentation/dev-tools/kunit/start.rst
+ *     ./tools/testing/kunit/kunit.py run randstruct [--raw_output] \
+ *             [--make_option LLVM=1] \
+ *             --kconfig_add CONFIG_RANDSTRUCT_FULL=y
+ *
+ */
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <kunit/test.h>
+#include <linux/init.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/string.h>
+
+#define DO_MANY_MEMBERS(macro, args...)        \
+       macro(a, args)                  \
+       macro(b, args)                  \
+       macro(c, args)                  \
+       macro(d, args)                  \
+       macro(e, args)                  \
+       macro(f, args)                  \
+       macro(g, args)                  \
+       macro(h, args)
+
+#define do_enum(x, ignored)    MEMBER_NAME_ ## x,
+enum randstruct_member_names {
+       DO_MANY_MEMBERS(do_enum)
+       MEMBER_NAME_MAX,
+};
+/* Make sure the macros are working: want 8 test members. */
+_Static_assert(MEMBER_NAME_MAX == 8, "Number of test members changed?!");
+
+/* This is an unsigned long member to match the function pointer size */
+#define unsigned_long_member(x, ignored)       unsigned long x;
+struct randstruct_untouched {
+       DO_MANY_MEMBERS(unsigned_long_member)
+};
+
+/* Struct explicitly marked with __randomize_layout. */
+struct randstruct_shuffled {
+       DO_MANY_MEMBERS(unsigned_long_member)
+} __randomize_layout;
+#undef unsigned_long_member
+
+/* Struct implicitly randomized from being all func ptrs. */
+#define func_member(x, ignored)        size_t (*x)(int);
+struct randstruct_funcs_untouched {
+       DO_MANY_MEMBERS(func_member)
+} __no_randomize_layout;
+
+struct randstruct_funcs_shuffled {
+       DO_MANY_MEMBERS(func_member)
+};
+#undef func_member
+
+#define func_body(x, ignored)                                  \
+static noinline size_t func_##x(int arg)                       \
+{                                                              \
+       return offsetof(struct randstruct_funcs_untouched, x);  \
+}
+DO_MANY_MEMBERS(func_body)
+
+/* Various mixed types. */
+#define mixed_members                                  \
+       bool a;                                         \
+       short b;                                        \
+       unsigned int c __aligned(16);                   \
+       size_t d;                                       \
+       char e;                                         \
+       u64 f;                                          \
+       union {                                         \
+               struct randstruct_shuffled shuffled;    \
+               uintptr_t g;                            \
+       };                                              \
+       union {                                         \
+               void *ptr;                              \
+               char h;                                 \
+       };
+
+struct randstruct_mixed_untouched {
+       mixed_members
+};
+
+struct randstruct_mixed_shuffled {
+       mixed_members
+} __randomize_layout;
+#undef mixed_members
+
+struct contains_randstruct_untouched {
+       int before;
+       struct randstruct_untouched untouched;
+       int after;
+};
+
+struct contains_randstruct_shuffled {
+       int before;
+       struct randstruct_shuffled shuffled;
+       int after;
+};
+
+static void randstruct_layout(struct kunit *test)
+{
+       int mismatches;
+
+#define check_mismatch(x, untouched, shuffled) \
+       if (offsetof(untouched, x) != offsetof(shuffled, x))    \
+               mismatches++;                                   \
+       kunit_info(test, #shuffled "::" #x " @ %zu (vs %zu)\n", \
+                  offsetof(shuffled, x),                       \
+                  offsetof(untouched, x));                     \
+
+#define check_pair(outcome, untouched, shuffled)               \
+       mismatches = 0;                                         \
+       DO_MANY_MEMBERS(check_mismatch, untouched, shuffled)    \
+       kunit_info(test, "Differing " #untouched " vs " #shuffled " member positions: %d\n", \
+                  mismatches);                                 \
+       KUNIT_##outcome##_MSG(test, mismatches, 0,              \
+                             #untouched " vs " #shuffled " layouts: unlucky or broken?\n");
+
+       check_pair(EXPECT_EQ, struct randstruct_untouched, struct randstruct_untouched)
+       check_pair(EXPECT_GT, struct randstruct_untouched, struct randstruct_shuffled)
+       check_pair(EXPECT_GT, struct randstruct_untouched, struct randstruct_funcs_shuffled)
+       check_pair(EXPECT_GT, struct randstruct_funcs_untouched, struct randstruct_funcs_shuffled)
+       check_pair(EXPECT_GT, struct randstruct_mixed_untouched, struct randstruct_mixed_shuffled)
+#undef check_pair
+
+#undef check_mismatch
+}
+
+#define check_mismatch(x, ignore)                              \
+       KUNIT_EXPECT_EQ_MSG(test, untouched->x, shuffled->x,    \
+                           "Mismatched member value in %s initializer\n", \
+                           name);
+
+static void test_check_init(struct kunit *test, const char *name,
+                           struct randstruct_untouched *untouched,
+                           struct randstruct_shuffled *shuffled)
+{
+       DO_MANY_MEMBERS(check_mismatch)
+}
+
+static void test_check_mixed_init(struct kunit *test, const char *name,
+                                 struct randstruct_mixed_untouched *untouched,
+                                 struct randstruct_mixed_shuffled *shuffled)
+{
+       DO_MANY_MEMBERS(check_mismatch)
+}
+#undef check_mismatch
+
+#define check_mismatch(x, ignore)                              \
+       KUNIT_EXPECT_EQ_MSG(test, untouched->untouched.x,       \
+                           shuffled->shuffled.x,               \
+                           "Mismatched member value in %s initializer\n", \
+                           name);
+static void test_check_contained_init(struct kunit *test, const char *name,
+                                     struct contains_randstruct_untouched *untouched,
+                                     struct contains_randstruct_shuffled *shuffled)
+{
+       DO_MANY_MEMBERS(check_mismatch)
+}
+#undef check_mismatch
+
+#define check_mismatch(x, ignore)                                      \
+       KUNIT_EXPECT_PTR_EQ_MSG(test, untouched->x, shuffled->x,        \
+                           "Mismatched member value in %s initializer\n", \
+                           name);
+
+static void test_check_funcs_init(struct kunit *test, const char *name,
+                                 struct randstruct_funcs_untouched *untouched,
+                                 struct randstruct_funcs_shuffled *shuffled)
+{
+       DO_MANY_MEMBERS(check_mismatch)
+}
+#undef check_mismatch
+
+static void randstruct_initializers(struct kunit *test)
+{
+#define init_members           \
+               .a = 1,         \
+               .b = 3,         \
+               .c = 5,         \
+               .d = 7,         \
+               .e = 11,        \
+               .f = 13,        \
+               .g = 17,        \
+               .h = 19,
+       struct randstruct_untouched untouched = {
+               init_members
+       };
+       struct randstruct_shuffled shuffled = {
+               init_members
+       };
+       struct randstruct_mixed_untouched mixed_untouched = {
+               init_members
+       };
+       struct randstruct_mixed_shuffled mixed_shuffled = {
+               init_members
+       };
+       struct contains_randstruct_untouched contains_untouched = {
+               .untouched = {
+                       init_members
+               },
+       };
+       struct contains_randstruct_shuffled contains_shuffled = {
+               .shuffled = {
+                       init_members
+               },
+       };
+#define func_member(x, ignored)        \
+               .x = func_##x,
+       struct randstruct_funcs_untouched funcs_untouched = {
+               DO_MANY_MEMBERS(func_member)
+       };
+       struct randstruct_funcs_shuffled funcs_shuffled = {
+               DO_MANY_MEMBERS(func_member)
+       };
+
+       test_check_init(test, "named", &untouched, &shuffled);
+       test_check_init(test, "unnamed", &untouched,
+               &(struct randstruct_shuffled){
+                       init_members
+               });
+
+       test_check_contained_init(test, "named", &contains_untouched, &contains_shuffled);
+       test_check_contained_init(test, "unnamed", &contains_untouched,
+               &(struct contains_randstruct_shuffled){
+                       .shuffled = (struct randstruct_shuffled){
+                               init_members
+                       },
+               });
+
+       test_check_contained_init(test, "named", &contains_untouched, &contains_shuffled);
+       test_check_contained_init(test, "unnamed copy", &contains_untouched,
+               &(struct contains_randstruct_shuffled){
+                       /* full struct copy initializer */
+                       .shuffled = shuffled,
+               });
+
+       test_check_mixed_init(test, "named", &mixed_untouched, &mixed_shuffled);
+       test_check_mixed_init(test, "unnamed", &mixed_untouched,
+               &(struct randstruct_mixed_shuffled){
+                       init_members
+               });
+
+       test_check_funcs_init(test, "named", &funcs_untouched, &funcs_shuffled);
+       test_check_funcs_init(test, "unnamed", &funcs_untouched,
+               &(struct randstruct_funcs_shuffled){
+                       DO_MANY_MEMBERS(func_member)
+               });
+
+#undef func_member
+#undef init_members
+}
+
+static int randstruct_test_init(struct kunit *test)
+{
+       if (!IS_ENABLED(CONFIG_RANDSTRUCT))
+               kunit_skip(test, "Not built with CONFIG_RANDSTRUCT=y");
+
+       return 0;
+}
+
+static struct kunit_case randstruct_test_cases[] = {
+       KUNIT_CASE(randstruct_layout),
+       KUNIT_CASE(randstruct_initializers),
+       {}
+};
+
+static struct kunit_suite randstruct_test_suite = {
+       .name = "randstruct",
+       .init = randstruct_test_init,
+       .test_cases = randstruct_test_cases,
+};
+
+kunit_test_suites(&randstruct_test_suite);
+
+MODULE_DESCRIPTION("Test cases for struct randomization");
+MODULE_LICENSE("GPL");