]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
x86_64/bug: Implement __WARN_printf()
authorPeter Zijlstra <peterz@infradead.org>
Mon, 2 Jun 2025 13:08:32 +0000 (15:08 +0200)
committerPeter Zijlstra <peterz@infradead.org>
Mon, 24 Nov 2025 19:23:05 +0000 (20:23 +0100)
The basic idea is to have __WARN_printf() be a vararg function such
that the compiler can do the optimal calling convention for us. This
function body will be a #UD and then set up a va_list in the exception
from pt_regs.

But because the trap will be in a called function, the bug_entry must
be passed in. Have that be the first argument, with the format tucked
away inside the bug_entry.

The comments should clarify the real fun details.

The big downside is that all WARNs will now show:

 RIP: 0010:__WARN_trap:+0

One possible solution is to simply discard the top frame when
unwinding. A follow up patch takes care of this slightly differently
by abusing the x86 static_call implementation.

This changes (with the next patches):

WARN_ONCE(preempt_count() != 2*PREEMPT_DISABLE_OFFSET,
  "corrupted preempt_count: %s/%d/0x%x\n",

from:

cmpl    $2, %ecx #, _7
        jne     .L1472
...

  .L1472:
        cmpb    $0, __already_done.11(%rip)
        je      .L1513
...

  .L1513
movb    $1, __already_done.11(%rip)
movl    1424(%r14), %edx        # _15->pid, _15->pid
        leaq    1912(%r14), %rsi        #, _17
        movq    $.LC43, %rdi    #,
        call    __warn_printk   #
ud2
  .pushsection __bug_table,"aw"
        2:
        .long 1b - .    # bug_entry::bug_addr
        .long .LC1 - .  # bug_entry::file
        .word 5093      # bug_entry::line
        .word 2313      # bug_entry::flags
        .org 2b + 12
  .popsection
  .pushsection .discard.annotate_insn,"M", @progbits, 8
        .long 1b - .
        .long 8         # ANNOTYPE_REACHABLE
  .popsection

into:

cmpl    $2, %ecx        #, _7
        jne     .L1442  #,
...

  .L1442:
        lea (2f)(%rip), %rdi
  1:
  .pushsection __bug_table,"aw"
        2:
        .long 1b - .    # bug_entry::bug_addr
        .long .LC43 - . # bug_entry::format
        .long .LC1 - .  # bug_entry::file
        .word 5093      # bug_entry::line
        .word 2323      # bug_entry::flags
        .org 2b + 16
  .popsection
        movl    1424(%r14), %edx        # _19->pid, _19->pid
        leaq    1912(%r14), %rsi        #, _13
ud1 (%edx), %rdi

Notably, by pushing everything into the exception handler it can take
care of the ONCE thing.

Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>
Link: https://patch.msgid.link/20251110115758.213813530@infradead.org
arch/x86/entry/entry.S
arch/x86/include/asm/bug.h
arch/x86/kernel/traps.c

index 8e9a0cc20a4ab7f7511170d0ecfec7d7c91c7992..772c64ed4523e48adfe49a250060c8ad92f40260 100644 (file)
@@ -32,6 +32,14 @@ SYM_FUNC_END(write_ibpb)
 /* For KVM */
 EXPORT_SYMBOL_GPL(write_ibpb);
 
+SYM_FUNC_START(__WARN_trap)
+       ANNOTATE_NOENDBR
+       ANNOTATE_REACHABLE
+       ud1 (%edx), %_ASM_ARG1
+       RET
+SYM_FUNC_END(__WARN_trap)
+EXPORT_SYMBOL(__WARN_trap)
+
 .popsection
 
 /*
index 50b802169a4b40014cf9ecf85279cc9c79714e3b..b5474603460b48e57f0b53805241e8df300b9f53 100644 (file)
@@ -7,6 +7,11 @@
 #include <linux/objtool.h>
 #include <asm/asm.h>
 
+#ifndef __ASSEMBLY__
+struct bug_entry;
+extern void __WARN_trap(struct bug_entry *bug, ...);
+#endif
+
 /*
  * Despite that some emulators terminate on UD2, we use it for WARN().
  */
@@ -31,6 +36,7 @@
 #define BUG_UD2                        0xfffe
 #define BUG_UD1                        0xfffd
 #define BUG_UD1_UBSAN          0xfffc
+#define BUG_UD1_WARN           0xfffb
 #define BUG_UDB                        0xffd6
 #define BUG_LOCK               0xfff0
 
 #define __BUG_ENTRY_FORMAT(format)
 #endif
 
+#ifdef CONFIG_X86_64
+#define HAVE_ARCH_BUG_FORMAT_ARGS
+#endif
+
 #define __BUG_ENTRY(format, file, line, flags)                         \
        __BUG_REL("1b")         "\t# bug_entry::bug_addr\n"             \
        __BUG_ENTRY_FORMAT(format)                                      \
        __BUG_ENTRY_VERBOSE(file, line)                                 \
        "\t.word " flags        "\t# bug_entry::flags\n"
 
-#define _BUG_FLAGS_ASM(ins, format, file, line, flags, size, extra)    \
-       "1:\t" ins "\n"                                                 \
+#define _BUG_FLAGS_ASM(format, file, line, flags, size, extra)         \
        ".pushsection __bug_table,\"aw\"\n\t"                           \
        ANNOTATE_DATA_SPECIAL                                           \
        "2:\n\t"                                                        \
@@ -82,7 +91,8 @@
 
 #define _BUG_FLAGS(cond_str, ins, flags, extra)                                \
 do {                                                                   \
-       asm_inline volatile(_BUG_FLAGS_ASM(ins, "%c[fmt]", "%c[file]",  \
+       asm_inline volatile("1:\t" ins "\n"                             \
+                           _BUG_FLAGS_ASM("%c[fmt]", "%c[file]",       \
                                           "%c[line]", "%c[fl]",        \
                                           "%c[size]", extra)           \
                   : : [fmt] "i" (WARN_CONDITION_STR(cond_str)),        \
@@ -93,7 +103,8 @@ do {                                                                 \
 } while (0)
 
 #define ARCH_WARN_ASM(file, line, flags, size)                         \
-       _BUG_FLAGS_ASM(ASM_UD2, "0", file, line, flags, size, "")
+       "1:\t " ASM_UD2 "\n"                                            \
+       _BUG_FLAGS_ASM("0", file, line, flags, size, "")
 
 #else
 
@@ -126,6 +137,49 @@ do {                                                                       \
        instrumentation_end();                                          \
 } while (0)
 
+#ifdef HAVE_ARCH_BUG_FORMAT_ARGS
+
+#ifndef __ASSEMBLY__
+struct pt_regs;
+struct sysv_va_list { /* from AMD64 System V ABI */
+       unsigned int gp_offset;
+       unsigned int fp_offset;
+       void *overflow_arg_area;
+       void *reg_save_area;
+};
+struct arch_va_list {
+       unsigned long regs[6];
+       struct sysv_va_list args;
+};
+extern void *__warn_args(struct arch_va_list *args, struct pt_regs *regs);
+#endif /* __ASSEMBLY__ */
+
+#define __WARN_bug_entry(flags, format) ({                             \
+       struct bug_entry *bug;                                          \
+       asm_inline volatile("lea (2f)(%%rip), %[addr]\n1:\n"            \
+                           _BUG_FLAGS_ASM("%c[fmt]", "%c[file]",       \
+                                          "%c[line]", "%c[fl]",        \
+                                          "%c[size]", "")              \
+                  : [addr] "=r" (bug)                                  \
+                  : [fmt] "i" (format),                                \
+                    [file] "i" (__FILE__),                             \
+                    [line] "i" (__LINE__),                             \
+                    [fl] "i" (flags),                                  \
+                    [size] "i" (sizeof(struct bug_entry)));            \
+       bug; })
+
+#define __WARN_print_arg(flags, format, arg...)                                \
+do {                                                                   \
+       int __flags = (flags) | BUGFLAG_WARNING | BUGFLAG_ARGS ;        \
+       __WARN_trap(__WARN_bug_entry(__flags, format), ## arg);         \
+       asm (""); /* inhibit tail-call optimization */                  \
+} while (0)
+
+#define __WARN_printf(taint, fmt, arg...) \
+       __WARN_print_arg(BUGFLAG_TAINT(taint), fmt, ## arg)
+
+#endif /* HAVE_ARCH_BUG_FORMAT_ARGS */
+
 #include <asm-generic/bug.h>
 
 #endif /* _ASM_X86_BUG_H */
index 6b22611e69cc8aa87716c3dcdaf44ba33db0fc2b..fbbe3ab145a71952817ecc624f89984393e76ea0 100644 (file)
@@ -102,25 +102,37 @@ __always_inline int is_valid_bugaddr(unsigned long addr)
  * UBSan{0}:     67 0f b9 00             ud1    (%eax),%eax
  * UBSan{10}:    67 0f b9 40 10          ud1    0x10(%eax),%eax
  * static_call:  0f b9 cc                ud1    %esp,%ecx
+ * __WARN_trap:  67 48 0f b9 3a          ud1    (%edx),%reg
  *
- * Notably UBSAN uses EAX, static_call uses ECX.
+ * Notable, since __WARN_trap can use all registers, the distinction between
+ * UD1 users is through R/M.
  */
 __always_inline int decode_bug(unsigned long addr, s32 *imm, int *len)
 {
        unsigned long start = addr;
+       u8 v, reg, rm, rex = 0;
+       int type = BUG_UD1;
        bool lock = false;
-       u8 v;
 
        if (addr < TASK_SIZE_MAX)
                return BUG_NONE;
 
-       v = *(u8 *)(addr++);
-       if (v == INSN_ASOP)
+       for (;;) {
                v = *(u8 *)(addr++);
+               if (v == INSN_ASOP)
+                       continue;
 
-       if (v == INSN_LOCK) {
-               lock = true;
-               v = *(u8 *)(addr++);
+               if (v == INSN_LOCK) {
+                       lock = true;
+                       continue;
+               }
+
+               if ((v & 0xf0) == 0x40) {
+                       rex = v;
+                       continue;
+               }
+
+               break;
        }
 
        switch (v) {
@@ -156,18 +168,33 @@ __always_inline int decode_bug(unsigned long addr, s32 *imm, int *len)
        if (X86_MODRM_MOD(v) != 3 && X86_MODRM_RM(v) == 4)
                addr++;                 /* SIB */
 
+       reg = X86_MODRM_REG(v) + 8*!!X86_REX_R(rex);
+       rm  = X86_MODRM_RM(v)  + 8*!!X86_REX_B(rex);
+
        /* Decode immediate, if present */
        switch (X86_MODRM_MOD(v)) {
        case 0: if (X86_MODRM_RM(v) == 5)
-                       addr += 4; /* RIP + disp32 */
+                       addr += 4;      /* RIP + disp32 */
+
+               if (rm == 0)            /* (%eax) */
+                       type = BUG_UD1_UBSAN;
+
+               if (rm == 2) {          /* (%edx) */
+                       *imm = reg;
+                       type = BUG_UD1_WARN;
+               }
                break;
 
        case 1: *imm = *(s8 *)addr;
                addr += 1;
+               if (rm == 0)            /* (%eax) */
+                       type = BUG_UD1_UBSAN;
                break;
 
        case 2: *imm = *(s32 *)addr;
                addr += 4;
+               if (rm == 0)            /* (%eax) */
+                       type = BUG_UD1_UBSAN;
                break;
 
        case 3: break;
@@ -176,12 +203,73 @@ __always_inline int decode_bug(unsigned long addr, s32 *imm, int *len)
        /* record instruction length */
        *len = addr - start;
 
-       if (X86_MODRM_REG(v) == 0)      /* EAX */
-               return BUG_UD1_UBSAN;
+       return type;
+}
 
-       return BUG_UD1;
+static inline unsigned long pt_regs_val(struct pt_regs *regs, int nr)
+{
+       int offset = pt_regs_offset(regs, nr);
+       if (WARN_ON_ONCE(offset < -0))
+               return 0;
+       return *((unsigned long *)((void *)regs + offset));
 }
 
+#ifdef HAVE_ARCH_BUG_FORMAT_ARGS
+/*
+ * Create a va_list from an exception context.
+ */
+void *__warn_args(struct arch_va_list *args, struct pt_regs *regs)
+{
+       /*
+        * Register save area; populate with function call argument registers
+        */
+       args->regs[0] = regs->di;
+       args->regs[1] = regs->si;
+       args->regs[2] = regs->dx;
+       args->regs[3] = regs->cx;
+       args->regs[4] = regs->r8;
+       args->regs[5] = regs->r9;
+
+       /*
+        * From the ABI document:
+        *
+        * @gp_offset - the element holds the offset in bytes from
+        * reg_save_area to the place where the next available general purpose
+        * argument register is saved. In case all argument registers have
+        * been exhausted, it is set to the value 48 (6*8).
+        *
+        * @fp_offset - the element holds the offset in bytes from
+        * reg_save_area to the place where the next available floating point
+        * argument is saved. In case all argument registers have been
+        * exhausted, it is set to the value 176 (6*8 + 8*16)
+        *
+        * @overflow_arg_area - this pointer is used to fetch arguments passed
+        * on the stack. It is initialized with the address of the first
+        * argument passed on the stack, if any, and then always updated to
+        * point to the start of the next argument on the stack.
+        *
+        * @reg_save_area - the element points to the start of the register
+        * save area.
+        *
+        * Notably the vararg starts with the second argument and there are no
+        * floating point arguments in the kernel.
+        */
+       args->args.gp_offset = 1*8;
+       args->args.fp_offset = 6*8 + 8*16;
+       args->args.reg_save_area = &args->regs;
+       args->args.overflow_arg_area = (void *)regs->sp;
+
+       /*
+        * If the exception came from __WARN_trap, there is a return
+        * address on the stack, skip that. This is why any __WARN_trap()
+        * caller must inhibit tail-call optimization.
+        */
+       if ((void *)regs->ip == &__WARN_trap)
+               args->args.overflow_arg_area += 8;
+
+       return &args->args;
+}
+#endif /* HAVE_ARCH_BUG_FORMAT */
 
 static nokprobe_inline int
 do_trap_no_signal(struct task_struct *tsk, int trapnr, const char *str,
@@ -334,6 +422,11 @@ static noinstr bool handle_bug(struct pt_regs *regs)
                raw_local_irq_enable();
 
        switch (ud_type) {
+       case BUG_UD1_WARN:
+               if (report_bug_entry((void *)pt_regs_val(regs, ud_imm), regs) == BUG_TRAP_TYPE_WARN)
+                       handled = true;
+               break;
+
        case BUG_UD2:
                if (report_bug(regs->ip, regs) == BUG_TRAP_TYPE_WARN) {
                        handled = true;