load_unaligned_zeropad() reads a word that may run into an unmapped page and recovers through an exception fixup. It does the word loads of sized_strscpy() and of the name compare in dentry_string_cmp(), path components in hash_name() and hashlen_string(), and the trailing bytes in x86's csum_partial(). It is inline assembly on every architecture that implements it, so KASAN and KCSAN never see these loads, while read_word_at_a_time() at least checks the first byte of each word. A use-after-free or out-of-bounds read of a name handed to path lookup goes unreported, and so does strscpy() reading from a bad source, which the kasan_strings KUnit case expects to be reported. On x86_64 with generic KASAN: KASAN and string KUnit suites failures before after CONFIG_FORTIFY_SOURCE=n 1 0 CONFIG_FORTIFY_SOURCE=y 1 0 Check the first byte at the start of load_unaligned_zeropad() on every architecture, as read_word_at_a_time() does. All callers only load a word whose first byte belongs to the string or buffer they read, so the check does not flag their intended reads past the end. Add no-op stubs for the powerpc selftest that builds the header in user space. Suggested-by: Peter Collingbourne Signed-off-by: Qiliang Yuan --- arch/arm/include/asm/word-at-a-time.h | 7 +++++++ arch/arm64/include/asm/word-at-a-time.h | 7 +++++++ arch/powerpc/include/asm/word-at-a-time.h | 6 ++++++ arch/riscv/include/asm/word-at-a-time.h | 6 ++++++ arch/s390/include/asm/word-at-a-time.h | 6 ++++++ arch/x86/include/asm/word-at-a-time.h | 6 ++++++ tools/testing/selftests/powerpc/primitives/linux/kasan-checks.h | 3 +++ tools/testing/selftests/powerpc/primitives/linux/kcsan-checks.h | 3 +++ 8 files changed, 44 insertions(+) diff --git a/arch/arm/include/asm/word-at-a-time.h b/arch/arm/include/asm/word-at-a-time.h index 5023f98d8293d..f672aad2e30e6 100644 --- a/arch/arm/include/asm/word-at-a-time.h +++ b/arch/arm/include/asm/word-at-a-time.h @@ -56,6 +56,9 @@ static inline unsigned long find_zero(unsigned long mask) #include #endif +#include +#include + #ifdef CONFIG_DCACHE_WORD_ACCESS /* @@ -69,6 +72,10 @@ static inline unsigned long load_unaligned_zeropad(const void *addr) { unsigned long ret, tmp; + /* open-coded instrument_read(addr, 1), as in read_word_at_a_time() */ + kasan_check_read(addr, 1); + kcsan_check_read(addr, 1); + /* Load word from unaligned pointer addr */ asm( "1: ldr %0, [%2]\n" diff --git a/arch/arm64/include/asm/word-at-a-time.h b/arch/arm64/include/asm/word-at-a-time.h index 824ca6987a516..11ecca99057aa 100644 --- a/arch/arm64/include/asm/word-at-a-time.h +++ b/arch/arm64/include/asm/word-at-a-time.h @@ -40,6 +40,9 @@ static inline unsigned long zero_bytemask(unsigned long bits) #include #endif +#include +#include + /* * Load an unaligned word from kernel space. * @@ -51,6 +54,10 @@ static inline unsigned long load_unaligned_zeropad(const void *addr) { unsigned long ret; + /* open-coded instrument_read(addr, 1), as in read_word_at_a_time() */ + kasan_check_read(addr, 1); + kcsan_check_read(addr, 1); + __mte_enable_tco_async(); /* Load word from unaligned pointer addr */ diff --git a/arch/powerpc/include/asm/word-at-a-time.h b/arch/powerpc/include/asm/word-at-a-time.h index 54653a8634149..49c6a1f65c493 100644 --- a/arch/powerpc/include/asm/word-at-a-time.h +++ b/arch/powerpc/include/asm/word-at-a-time.h @@ -5,6 +5,8 @@ * Word-at-a-time interfaces for PowerPC. */ #include +#include +#include #include #include #include @@ -168,6 +170,10 @@ static inline unsigned long load_unaligned_zeropad(const void *addr) { unsigned long ret, offset, tmp; + /* open-coded instrument_read(addr, 1), as in read_word_at_a_time() */ + kasan_check_read(addr, 1); + kcsan_check_read(addr, 1); + asm( "1: " PPC_LL "%[ret], 0(%[addr])\n" "2:\n" diff --git a/arch/riscv/include/asm/word-at-a-time.h b/arch/riscv/include/asm/word-at-a-time.h index 3802cda71ab7e..fb7e2904dd7fd 100644 --- a/arch/riscv/include/asm/word-at-a-time.h +++ b/arch/riscv/include/asm/word-at-a-time.h @@ -11,6 +11,8 @@ #include #include +#include +#include #include struct word_at_a_time { @@ -60,6 +62,10 @@ static inline unsigned long load_unaligned_zeropad(const void *addr) { unsigned long ret; + /* open-coded instrument_read(addr, 1), as in read_word_at_a_time() */ + kasan_check_read(addr, 1); + kcsan_check_read(addr, 1); + /* Load word from unaligned pointer addr */ asm( "1: " REG_L " %0, %2\n" diff --git a/arch/s390/include/asm/word-at-a-time.h b/arch/s390/include/asm/word-at-a-time.h index eaa19dee76994..59393cf065964 100644 --- a/arch/s390/include/asm/word-at-a-time.h +++ b/arch/s390/include/asm/word-at-a-time.h @@ -3,6 +3,8 @@ #define _ASM_WORD_AT_A_TIME_H #include +#include +#include #include #include #include @@ -52,6 +54,10 @@ static inline unsigned long load_unaligned_zeropad(const void *addr) { unsigned long data; + /* open-coded instrument_read(addr, 1), as in read_word_at_a_time() */ + kasan_check_read(addr, 1); + kcsan_check_read(addr, 1); + asm_inline volatile( "0: lg %[data],0(%[addr])\n" "1: nopr %%r7\n" diff --git a/arch/x86/include/asm/word-at-a-time.h b/arch/x86/include/asm/word-at-a-time.h index 422a47746657a..1319e1d87a9c4 100644 --- a/arch/x86/include/asm/word-at-a-time.h +++ b/arch/x86/include/asm/word-at-a-time.h @@ -3,6 +3,8 @@ #define _ASM_WORD_AT_A_TIME_H #include +#include +#include #include struct word_at_a_time { @@ -71,6 +73,10 @@ static inline unsigned long load_unaligned_zeropad(const void *addr) { unsigned long ret; + /* open-coded instrument_read(addr, 1), as in read_word_at_a_time() */ + kasan_check_read(addr, 1); + kcsan_check_read(addr, 1); + asm volatile( "1: mov %[mem], %[ret]\n" "2:\n" diff --git a/tools/testing/selftests/powerpc/primitives/linux/kasan-checks.h b/tools/testing/selftests/powerpc/primitives/linux/kasan-checks.h new file mode 100644 index 0000000000000..000c92de6baf2 --- /dev/null +++ b/tools/testing/selftests/powerpc/primitives/linux/kasan-checks.h @@ -0,0 +1,3 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* There is nothing for KASAN to check in user space. */ +#define kasan_check_read(p, size) ((void)(p), (void)(size)) diff --git a/tools/testing/selftests/powerpc/primitives/linux/kcsan-checks.h b/tools/testing/selftests/powerpc/primitives/linux/kcsan-checks.h new file mode 100644 index 0000000000000..39c97bc15531e --- /dev/null +++ b/tools/testing/selftests/powerpc/primitives/linux/kcsan-checks.h @@ -0,0 +1,3 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* There is nothing for KCSAN to check in user space. */ +#define kcsan_check_read(p, size) ((void)(p), (void)(size)) -- 2.43.0 On architectures with CONFIG_DCACHE_WORD_ACCESS, load_unaligned_zeropad() does the word loads of the dcache name compare and of path name hashing. None of the KASAN test cases reads memory through it, so nothing noticed that KASAN did not check these loads at all. Add a case that reads a freed object through load_unaligned_zeropad() and expects a report. Skip it with hardware tag-based KASAN, since arm64 turns tag checking off for this load: kasan_load_unaligned_zeropad, x86_64, generic KASAN without "word-at-a-time: check the first byte in load_unaligned_zeropad()" FAILED with it PASSED Signed-off-by: Qiliang Yuan --- mm/kasan/kasan_test_c.c | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/mm/kasan/kasan_test_c.c b/mm/kasan/kasan_test_c.c index b9e167ed5be32..c791512f6fc49 100644 --- a/mm/kasan/kasan_test_c.c +++ b/mm/kasan/kasan_test_c.c @@ -28,6 +28,9 @@ #include #include +#ifdef CONFIG_DCACHE_WORD_ACCESS +#include +#endif #include "kasan.h" @@ -1673,6 +1676,26 @@ static void kasan_memcmp(struct kunit *test) kfree(ptr); } +static void kasan_load_unaligned_zeropad(struct kunit *test) +{ +#ifdef CONFIG_DCACHE_WORD_ACCESS + char *ptr; + + /* The tag check is turned off for this load on arm64 with MTE. */ + KASAN_TEST_NEEDS_CONFIG_OFF(test, CONFIG_KASAN_HW_TAGS); + + ptr = kmalloc(64, GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ptr); + kfree(ptr); + + OPTIMIZER_HIDE_VAR(ptr); + KUNIT_EXPECT_KASAN_FAIL_READ(test, + kasan_int_result = load_unaligned_zeropad(ptr)); +#else + kunit_skip(test, "Test requires CONFIG_DCACHE_WORD_ACCESS=y"); +#endif +} + static void kasan_strings(struct kunit *test) { char *ptr; @@ -2264,6 +2287,7 @@ static struct kunit_case kasan_kunit_test_cases[] = { KUNIT_CASE(kasan_alloca_oob_right), KUNIT_CASE(kasan_memchr), KUNIT_CASE(kasan_memcmp), + KUNIT_CASE(kasan_load_unaligned_zeropad), KUNIT_CASE(kasan_strings), KUNIT_CASE(kasan_bitops_generic), KUNIT_CASE(kasan_bitops_tags), -- 2.43.0