* [PATCH v2 1/2] word-at-a-time: check the first byte in load_unaligned_zeropad()
2026-10-23 1:49 [PATCH v2 0/2] word-at-a-time: let KASAN see load_unaligned_zeropad() Qiliang Yuan
@ 2026-10-23 1:49 ` Qiliang Yuan
2026-10-23 1:57 ` sashiko-bot
2026-10-23 1:49 ` [PATCH v2 2/2] kasan/test: cover load_unaligned_zeropad() Qiliang Yuan
1 sibling, 1 reply; 5+ messages in thread
From: Qiliang Yuan @ 2026-10-23 1:49 UTC (permalink / raw)
To: Russell King, Catalin Marinas, Will Deacon, Mark Rutland,
Madhavan Srinivasan, Michael Ellerman, Nicholas Piggin,
Christophe Leroy (CS GROUP), Ritesh Harjani (IBM),
Shrikanth Hegde, Paul Walmsley, Palmer Dabbelt, Albert Ou,
Alexandre Ghiti, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
Christian Borntraeger, Sven Schnelle, Thomas Gleixner,
Ingo Molnar, Borislav Petkov, Dave Hansen, x86, H. Peter Anvin,
Shuah Khan, Marco Elver, Andrey Ryabinin, Alexander Potapenko,
Andrey Konovalov, Dmitry Vyukov, Vincenzo Frascino, Andrew Morton
Cc: linux-arm-kernel, Kees Cook, linux-kernel, linux-hardening,
linuxppc-dev, linux-riscv, linux-s390, linux-kselftest, kasan-dev,
linux-mm, Qiliang Yuan, Peter Collingbourne
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 with instrument_read() 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 a no-op instrument_read() for the powerpc
selftest that builds the header in user space.
Suggested-by: Peter Collingbourne <peter@pcc.me.uk>
Signed-off-by: Qiliang Yuan <odys.yuan@gmail.com>
Reviewed-by: Marco Elver <elver@google.com>
---
arch/arm/include/asm/word-at-a-time.h | 5 +++++
arch/arm64/include/asm/word-at-a-time.h | 4 ++++
arch/powerpc/include/asm/word-at-a-time.h | 4 ++++
arch/riscv/include/asm/word-at-a-time.h | 4 ++++
arch/s390/include/asm/word-at-a-time.h | 4 ++++
arch/x86/include/asm/word-at-a-time.h | 4 ++++
tools/testing/selftests/powerpc/primitives/linux/instrumented.h | 3 +++
7 files changed, 28 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..361771f178699 100644
--- a/arch/arm/include/asm/word-at-a-time.h
+++ b/arch/arm/include/asm/word-at-a-time.h
@@ -56,6 +56,8 @@ static inline unsigned long find_zero(unsigned long mask)
#include <asm-generic/word-at-a-time.h>
#endif
+#include <linux/instrumented.h>
+
#ifdef CONFIG_DCACHE_WORD_ACCESS
/*
@@ -69,6 +71,9 @@ static inline unsigned long load_unaligned_zeropad(const void *addr)
{
unsigned long ret, tmp;
+ /* Check only the first byte, the rest may lie past the object */
+ instrument_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 d7a3b69dc8c6f..37d45e716c11a 100644
--- a/arch/arm64/include/asm/word-at-a-time.h
+++ b/arch/arm64/include/asm/word-at-a-time.h
@@ -8,6 +8,7 @@
#include <linux/uaccess.h>
#include <linux/bitops.h>
+#include <linux/instrumented.h>
#include <linux/wordpart.h>
struct word_at_a_time {
@@ -45,6 +46,9 @@ static inline unsigned long load_unaligned_zeropad(const void *addr)
{
unsigned long ret;
+ /* Check only the first byte, the rest may lie past the object */
+ instrument_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..1660b2e3e3a8f 100644
--- a/arch/powerpc/include/asm/word-at-a-time.h
+++ b/arch/powerpc/include/asm/word-at-a-time.h
@@ -5,6 +5,7 @@
* Word-at-a-time interfaces for PowerPC.
*/
#include <linux/bitops.h>
+#include <linux/instrumented.h>
#include <linux/wordpart.h>
#include <asm/asm-compat.h>
#include <asm/extable.h>
@@ -168,6 +169,9 @@ static inline unsigned long load_unaligned_zeropad(const void *addr)
{
unsigned long ret, offset, tmp;
+ /* Check only the first byte, the rest may lie past the object */
+ instrument_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..761fa2734b7f5 100644
--- a/arch/riscv/include/asm/word-at-a-time.h
+++ b/arch/riscv/include/asm/word-at-a-time.h
@@ -11,6 +11,7 @@
#include <asm/asm-extable.h>
#include <linux/bitops.h>
+#include <linux/instrumented.h>
#include <linux/wordpart.h>
struct word_at_a_time {
@@ -60,6 +61,9 @@ static inline unsigned long load_unaligned_zeropad(const void *addr)
{
unsigned long ret;
+ /* Check only the first byte, the rest may lie past the object */
+ instrument_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 0b7779c8d5a4e..f3caa4959cc87 100644
--- a/arch/s390/include/asm/word-at-a-time.h
+++ b/arch/s390/include/asm/word-at-a-time.h
@@ -3,6 +3,7 @@
#define _ASM_WORD_AT_A_TIME_H
#include <linux/bitops.h>
+#include <linux/instrumented.h>
#include <linux/wordpart.h>
#include <asm/asm-extable.h>
#include <asm/bitsperlong.h>
@@ -52,6 +53,9 @@ static inline unsigned long load_unaligned_zeropad(const void *addr)
{
unsigned long data;
+ /* Check only the first byte, the rest may lie past the object */
+ instrument_read(addr, 1);
+
asm_inline volatile(
"0: lg %[data],0(%[addr])\n"
"1:\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..5374e08372253 100644
--- a/arch/x86/include/asm/word-at-a-time.h
+++ b/arch/x86/include/asm/word-at-a-time.h
@@ -3,6 +3,7 @@
#define _ASM_WORD_AT_A_TIME_H
#include <linux/bitops.h>
+#include <linux/instrumented.h>
#include <linux/wordpart.h>
struct word_at_a_time {
@@ -71,6 +72,9 @@ static inline unsigned long load_unaligned_zeropad(const void *addr)
{
unsigned long ret;
+ /* Check only the first byte, the rest may lie past the object */
+ instrument_read(addr, 1);
+
asm volatile(
"1: mov %[mem], %[ret]\n"
"2:\n"
diff --git a/tools/testing/selftests/powerpc/primitives/linux/instrumented.h b/tools/testing/selftests/powerpc/primitives/linux/instrumented.h
new file mode 100644
index 0000000000000..114ef9284f69b
--- /dev/null
+++ b/tools/testing/selftests/powerpc/primitives/linux/instrumented.h
@@ -0,0 +1,3 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* There is nothing for KASAN or KCSAN to check in user space. */
+#define instrument_read(p, size) ((void)(p), (void)(size))
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread* [PATCH v2 2/2] kasan/test: cover load_unaligned_zeropad()
2026-10-23 1:49 [PATCH v2 0/2] word-at-a-time: let KASAN see load_unaligned_zeropad() Qiliang Yuan
2026-10-23 1:49 ` [PATCH v2 1/2] word-at-a-time: check the first byte in load_unaligned_zeropad() Qiliang Yuan
@ 2026-10-23 1:49 ` Qiliang Yuan
2026-10-23 1:57 ` sashiko-bot
1 sibling, 1 reply; 5+ messages in thread
From: Qiliang Yuan @ 2026-10-23 1:49 UTC (permalink / raw)
To: Russell King, Catalin Marinas, Will Deacon, Mark Rutland,
Madhavan Srinivasan, Michael Ellerman, Nicholas Piggin,
Christophe Leroy (CS GROUP), Ritesh Harjani (IBM),
Shrikanth Hegde, Paul Walmsley, Palmer Dabbelt, Albert Ou,
Alexandre Ghiti, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
Christian Borntraeger, Sven Schnelle, Thomas Gleixner,
Ingo Molnar, Borislav Petkov, Dave Hansen, x86, H. Peter Anvin,
Shuah Khan, Marco Elver, Andrey Ryabinin, Alexander Potapenko,
Andrey Konovalov, Dmitry Vyukov, Vincenzo Frascino, Andrew Morton
Cc: linux-arm-kernel, Kees Cook, linux-kernel, linux-hardening,
linuxppc-dev, linux-riscv, linux-s390, linux-kselftest, kasan-dev,
linux-mm, Qiliang Yuan
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 <odys.yuan@gmail.com>
Reviewed-by: Andrey Ryabinin <ryabinin.a.a@gmail.com>
---
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 <trace/events/printk.h>
#include <asm/page.h>
+#ifdef CONFIG_DCACHE_WORD_ACCESS
+#include <asm/word-at-a-time.h>
+#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
^ permalink raw reply related [flat|nested] 5+ messages in thread