[RFCv2] xen/arm64: livepatch: enable attaching callbacks

Ryo Takakura posted 1 patch 1 day, 6 hours ago
Patches applied successfully (tree, apply log)
git fetch https://gitlab.com/xen-project/patchew/xen tags/patchew/20260812013318.21860-1-takakura@valinux.co.jp
xen/arch/arm/Kconfig                  | 12 ++++
xen/arch/arm/arch.mk                  |  2 +
xen/arch/arm/arm64/Makefile           |  1 +
xen/arch/arm/arm64/insn.c             |  5 ++
xen/arch/arm/arm64/livepatch-asm.S    | 81 +++++++++++++++++++++++++++
xen/arch/arm/arm64/livepatch.c        | 30 ++++++++++
xen/arch/arm/include/asm/arm64/insn.h |  1 +
xen/common/livepatch.c                | 58 +++++++++++++++++--
xen/include/xen/livepatch.h           |  8 +++
xen/include/xen/livepatch_payload.h   |  7 ++-
10 files changed, 198 insertions(+), 7 deletions(-)
create mode 100644 xen/arch/arm/arm64/livepatch-asm.S
[RFCv2] xen/arm64: livepatch: enable attaching callbacks
Posted by Ryo Takakura 1 day, 6 hours ago
Linux ftrace allows registering callbacks which is useful
for debugging and tracing events. On Linux, it is done by
reserving NOPs at function entry points at compile time
which can later be patched to branch to a trampoline.

This patch introduces similar callback feature adopting
linux-like approach of reserving 2 NOPs at function entry
points. When user requests the feature, the NOPs will be
livepatched as:

     mov     x9, lr
     bl      xen_livepatch_trace_caller

where xen_livepatch_trace_caller serves as a trampoline
for saving original lr of the traced function and other
relevant registers before calling the registered tracing
functions.

The tracing functions are called with arguments of:
ip: ip of the traced function
parent_ip: ip where the traced function was called

One can request the feature by specifying the section
for struct livepatch_func as .livepatch.traces.

Signed-off-by: Ryo Takakura <takakura@valinux.co.jp>
---

Hi,

This is a continuation of the previously sent RFC[1].
Thank you Andrew and Roger for the feedback!!

Here are the major updates:
1. Reserve function preamble like Linux using -fpatchable-function-entry=2.
2. No per-callback trampoline generation.
3. Let common code decide whether a patch is a replacement or a preface addition,
   and let arch code provide a handler for each case.

For 2., Andrew suggested avoiding manual generation of trampolines
using the attribute "no_caller_saved_registers". I considered the option
but figured out we still need to save lr before jumping to tracer
function and it requires a trampoline (as far as I understand).
Moreover, the function attribute seems to be only supported on x86...
So I decided to make a common trampoline like linux[2].

Example payload file:

#include <xen/lib.h>
#include <xen/livepatch.h>

static void my_tracer(unsigned long ip, unsigned long parent_ip)
{
    printk("livepatch: do_domctl was called "
           "(ip=%lx, parent_ip=%lx)\n",
           ip, parent_ip);
}

static struct livepatch_func traces[]
    __attribute__((section(".livepatch.traces"))) =
{
    {
        .name = "do_domctl",
        .old_size = 4580,
        .new_addr = my_tracer,
        .version = LIVEPATCH_PAYLOAD_VERSION,
    },
};

I would appreciate any advice or suggestion, Thanks!

Sincerely,
Ryo Takakura

[1] https://lists.xen.org/archives/html/xen-devel/2026-06/msg01442.html
[2] https://web.git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/arm64/kernel/entry-ftrace.S#n36

---
 xen/arch/arm/Kconfig                  | 12 ++++
 xen/arch/arm/arch.mk                  |  2 +
 xen/arch/arm/arm64/Makefile           |  1 +
 xen/arch/arm/arm64/insn.c             |  5 ++
 xen/arch/arm/arm64/livepatch-asm.S    | 81 +++++++++++++++++++++++++++
 xen/arch/arm/arm64/livepatch.c        | 30 ++++++++++
 xen/arch/arm/include/asm/arm64/insn.h |  1 +
 xen/common/livepatch.c                | 58 +++++++++++++++++--
 xen/include/xen/livepatch.h           |  8 +++
 xen/include/xen/livepatch_payload.h   |  7 ++-
 10 files changed, 198 insertions(+), 7 deletions(-)
 create mode 100644 xen/arch/arm/arm64/livepatch-asm.S

diff --git a/xen/arch/arm/Kconfig b/xen/arch/arm/Kconfig
index 843a43897e..8d94232248 100644
--- a/xen/arch/arm/Kconfig
+++ b/xen/arch/arm/Kconfig
@@ -276,6 +276,18 @@ config PCI_PASSTHROUGH
 	help
 	  This option enables PCI device passthrough
 
+config ARM64_PATCHABLE_FUNCTION_ENTRY
+    bool "Reserve two NOPs at function entry for tracing"
+    depends on ARM_64 && LIVEPATCH && $(cc-option,-fpatchable-function-entry=2)
+    default n
+    help
+      This option compiles Xen with -fpatchable-function-entry=2, which
+      reserves two NOP instructions at the beginning of functions. The NOPs
+      will be livepatched with a branch instruction to tracing function
+      registered by user.
+
+      Enable this to use livepatch tracing feature.
+
 endmenu
 
 menu "ARM errata workaround via the alternative framework"
diff --git a/xen/arch/arm/arch.mk b/xen/arch/arm/arch.mk
index dea8dbd18a..e3cad65bef 100644
--- a/xen/arch/arm/arch.mk
+++ b/xen/arch/arm/arch.mk
@@ -19,6 +19,8 @@ endif
 CFLAGS-$(CONFIG_ARM_64) += -mgeneral-regs-only # No fp registers etc
 $(call cc-option-add,CFLAGS-$(CONFIG_ARM_64),CC,-mno-outline-atomics)
 
+CFLAGS-$(CONFIG_ARM64_PATCHABLE_FUNCTION_ENTRY) += -fpatchable-function-entry=2
+
 ifneq ($(filter command line environment,$(origin CONFIG_EARLY_PRINTK)),)
     $(error You must use 'make menuconfig' to enable/disable early printk now)
 endif
diff --git a/xen/arch/arm/arm64/Makefile b/xen/arch/arm/arm64/Makefile
index 6491c5350b..7f52b10e66 100644
--- a/xen/arch/arm/arm64/Makefile
+++ b/xen/arch/arm/arm64/Makefile
@@ -12,6 +12,7 @@ obj-y += entry.o
 obj-y += head.o
 obj-y += insn.o
 obj-$(CONFIG_LIVEPATCH) += livepatch.o
+obj-$(CONFIG_ARM64_PATCHABLE_FUNCTION_ENTRY) += livepatch-asm.o
 obj-y += smc.o
 obj-y += smpboot.o
 obj-$(CONFIG_ARM64_SVE) += sve.o sve-asm.o
diff --git a/xen/arch/arm/arm64/insn.c b/xen/arch/arm/arm64/insn.c
index 6b97a84ba7..dc3ddc6351 100644
--- a/xen/arch/arm/arm64/insn.c
+++ b/xen/arch/arm/arm64/insn.c
@@ -218,6 +218,11 @@ u32 __kprobes aarch64_insn_gen_nop(void)
 	return aarch64_insn_gen_hint(AARCH64_INSN_HINT_NOP);
 }
 
+u32 aarch64_insn_gen_move_reg(uint32_t rd, uint32_t rm)
+{
+    return 0xaa0003e0 | (rm << 16) | rd;
+}
+
 /*
  * Decode the imm field of a branch, and return the byte offset as a
  * signed value (so it can be used when computing a new branch
diff --git a/xen/arch/arm/arm64/livepatch-asm.S b/xen/arch/arm/arm64/livepatch-asm.S
new file mode 100644
index 0000000000..92858bcaa9
--- /dev/null
+++ b/xen/arch/arm/arm64/livepatch-asm.S
@@ -0,0 +1,81 @@
+/*
+ * xen/arch/arm/arm64/livepatch-asm.S
+ *
+ * Trampoline for livepatch tracer.
+ *
+ * Ryo Takakura <takakura@valinux.co.jp>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+/*
+ * If livepatch tracer is enabled, function entries are placed
+ * with two NOPs. Once user requests a functions to be traced,
+ * arch_livepatch_apply_trace() will livepatch those NOPs to:
+ *
+ *     mov     x9, x30
+ *     bl      xen_livepatch_trace_caller
+ *
+ * x9  = traced function's original LR
+ * x30 = traced function + 8
+ */
+FUNC(xen_livepatch_trace_caller)
+        /*
+         * Registers to save/restore:
+         *
+         *   x0-x8          Function arguments
+         *   x9             Original LR
+         *   x29            Original FP
+         *   x30            PC to return
+         */
+        sub     sp, sp, #96
+
+        /* Save function arguments */
+        stp     x0, x1,   [sp, #0]
+        stp     x2, x3,   [sp, #16]
+        stp     x4, x5,   [sp, #32]
+        stp     x6, x7,   [sp, #48]
+        str     x8,       [sp, #64]
+
+        /* Save LR, FP, PC */
+        str     x9,       [sp, #72]
+        str     x29,      [sp, #80]
+        str     x30,      [sp, #88]
+
+        /*
+         * xen_livepatch_trace_dispatcher(ip, parent_ip)
+         *
+         * ip: Instruction pointer of the function being traced.
+         * parent_ip: Instruction pointer of the function which
+         *            called the function being traced.
+         */
+        sub     x0, x30, #8
+        mov     x1, x9
+        bl      xen_livepatch_trace_dispatcher
+
+        /* Restore function arguments */
+        ldp     x0, x1, [sp, #0]
+        ldp     x2, x3, [sp, #16]
+        ldp     x4, x5, [sp, #32]
+        ldp     x6, x7, [sp, #48]
+        ldr     x8,     [sp, #64]
+
+        /* Restore FP, LR, PC */
+        ldr     x29,    [sp, #80]
+        ldr     x30,    [sp, #72]
+        ldr     x9,     [sp, #88]
+
+        /* Restore SP */
+        add     sp, sp, #96
+
+        /* ret x9 */
+        .inst   0xd65f0120
+        sb
+END(xen_livepatch_trace_caller)
diff --git a/xen/arch/arm/arm64/livepatch.c b/xen/arch/arm/arm64/livepatch.c
index 39159ba8b5..bb6d8affa2 100644
--- a/xen/arch/arm/arm64/livepatch.c
+++ b/xen/arch/arm/arm64/livepatch.c
@@ -14,6 +14,8 @@
 #include <asm/bitops.h>
 #include <asm/insn.h>
 
+extern void xen_livepatch_trace_caller(void);
+
 void arch_livepatch_apply(const struct livepatch_func *func,
                           struct livepatch_fstate *state)
 {
@@ -492,6 +494,34 @@ int arch_livepatch_perform_rela(struct livepatch_elf *elf,
     return -EINVAL;
 }
 
+void arch_livepatch_apply_trace(const struct livepatch_func *trace)
+{
+    uint32_t *new_ptr;
+    uint32_t insns[2];
+
+    new_ptr = trace->old_addr - (void *)_start + vmap_of_xen_text;
+
+    insns[0] = aarch64_insn_gen_move_reg(9, 30); /* mov x9, lr */
+    insns[1] = aarch64_insn_gen_branch_imm((unsigned long)trace->old_addr + ARCH_PATCH_INSN_SIZE,
+                                           (unsigned long)xen_livepatch_trace_caller,
+                                           AARCH64_INSN_BRANCH_LINK);
+
+    memcpy(new_ptr, insns, sizeof(insns));
+    clean_and_invalidate_dcache_va_range(new_ptr, sizeof(insns));
+}
+
+void arch_livepatch_revert_trace(const struct livepatch_func *trace)
+{
+    void *new_ptr = trace->old_addr - (void *)_start + vmap_of_xen_text;
+    uint32_t insns[2] = {
+        aarch64_insn_gen_nop(),
+        aarch64_insn_gen_nop(),
+    };
+
+    memcpy(new_ptr, insns, sizeof(insns));
+    clean_and_invalidate_dcache_va_range(new_ptr, sizeof(insns));
+}
+
 /*
  * Local variables:
  * mode: C
diff --git a/xen/arch/arm/include/asm/arm64/insn.h b/xen/arch/arm/include/asm/arm64/insn.h
index ab290030ab..a83d8de0e4 100644
--- a/xen/arch/arm/include/asm/arm64/insn.h
+++ b/xen/arch/arm/include/asm/arm64/insn.h
@@ -82,6 +82,7 @@ u32 aarch64_insn_gen_branch_imm(unsigned long pc, unsigned long addr,
 				enum aarch64_insn_branch_type type);
 u32 aarch64_insn_gen_hint(enum aarch64_insn_hint_op op);
 u32 aarch64_insn_gen_nop(void);
+u32 aarch64_insn_gen_move_reg(uint32_t rd, uint32_t rm);
 
 /* Wrapper for common code */
 static inline bool insn_is_branch_imm(u32 insn)
diff --git a/xen/common/livepatch.c b/xen/common/livepatch.c
index 7515a040ad..6f3c3274ab 100644
--- a/xen/common/livepatch.c
+++ b/xen/common/livepatch.c
@@ -198,6 +198,37 @@ static const char *cf_check livepatch_symbols_lookup(
     return n;
 }
 
+void xen_livepatch_trace_dispatcher(unsigned long ip, unsigned long parent_ip)
+{
+    const struct payload *data;
+    unsigned int i;
+    const struct livepatch_func *f;
+
+    rcu_read_lock(&rcu_payload_lock);
+
+    list_for_each_entry_rcu ( data, &payload_list, list )
+    {
+        if ( data->state != LIVEPATCH_STATE_APPLIED || !data->is_trace )
+            continue;
+
+        for ( i = 0; i < data->nfuncs; ++i )
+        {
+            f = &data->funcs[i];
+
+            if ( (unsigned long)f->old_addr != ip )
+                continue;
+
+            ASSERT(f->new_addr);
+            ((livepatch_trace_func_t)f->new_addr)(ip, parent_ip);
+
+            goto out;
+        }
+    }
+
+out:
+    rcu_read_unlock(&rcu_payload_lock);
+}
+
 /* Lookup function's old address if not already resolved. */
 static int resolve_old_address(struct livepatch_func *f,
                                const struct livepatch_elf *elf)
@@ -551,6 +582,7 @@ static int check_patching_sections(const struct livepatch_elf *elf)
 {
     unsigned int i;
     static const char *const names[] = { ELF_LIVEPATCH_FUNC,
+                                         ELF_LIVEPATCH_TRACES,
                                          ELF_LIVEPATCH_LOAD_HOOKS,
                                          ELF_LIVEPATCH_UNLOAD_HOOKS,
                                          ELF_LIVEPATCH_PREAPPLY_HOOK,
@@ -688,7 +720,7 @@ static inline int livepatch_check_expectations(const struct payload *payload)
 static int prepare_payload(struct payload *payload,
                            struct livepatch_elf *elf)
 {
-    const struct livepatch_elf_sec *sec;
+    const struct livepatch_elf_sec *func_sec, *trace_sec, *sec;
     const struct payload *data;
     unsigned int i;
     struct livepatch_func *funcs;
@@ -696,7 +728,12 @@ static int prepare_payload(struct payload *payload,
     struct virtual_region *region;
     int rc;
 
-    sec = livepatch_elf_sec_by_name(elf, ELF_LIVEPATCH_FUNC);
+    func_sec = livepatch_elf_sec_by_name(elf, ELF_LIVEPATCH_FUNC);
+    trace_sec = livepatch_elf_sec_by_name(elf, ELF_LIVEPATCH_TRACES);
+
+    sec = trace_sec ? trace_sec : func_sec;
+    payload->is_trace = !!trace_sec;
+
     if ( sec )
     {
         if ( !section_ok(elf, sec, sizeof(*payload->funcs)) )
@@ -721,6 +758,13 @@ static int prepare_payload(struct payload *payload,
                 return -EOPNOTSUPP;
             }
 
+            if ( payload->is_trace && !f->new_addr )
+            {
+                printk(XENLOG_ERR LIVEPATCH "%s: Trace new_addr is NULL\n",
+                       elf->name);
+                return -EINVAL;
+            }
+
             /* 'old_addr', 'new_addr', 'new_size' can all be zero. */
             if ( !f->old_size )
             {
@@ -1460,7 +1504,10 @@ static int apply_payload(struct payload *data)
             continue;
         }
 
-        arch_livepatch_apply(func, state);
+        if ( data->is_trace )
+            arch_livepatch_apply_trace(func);
+        else
+            arch_livepatch_apply(func, state);
         state->applied = LIVEPATCH_FUNC_APPLIED;
     }
 
@@ -1509,7 +1556,10 @@ int revert_payload(struct payload *data)
             continue;
         }
 
-        arch_livepatch_revert(func, state);
+        if ( data->is_trace )
+            arch_livepatch_revert_trace(func);
+        else
+            arch_livepatch_revert(func, state);
         state->applied = LIVEPATCH_FUNC_NOT_APPLIED;
     }
 
diff --git a/xen/include/xen/livepatch.h b/xen/include/xen/livepatch.h
index 5dc8c61d37..a893f134e0 100644
--- a/xen/include/xen/livepatch.h
+++ b/xen/include/xen/livepatch.h
@@ -26,6 +26,7 @@ struct xen_sysctl_livepatch_op;
 #define LIVEPATCH             "livepatch: "
 /* ELF payload special section names. */
 #define ELF_LIVEPATCH_FUNC        ".livepatch.funcs"
+#define ELF_LIVEPATCH_TRACES      ".livepatch.traces"
 #define ELF_LIVEPATCH_DEPENDS     ".livepatch.depends"
 #define ELF_LIVEPATCH_XEN_DEPENDS ".livepatch.xen_depends"
 #define ELF_BUILD_ID_NOTE         ".note.gnu.build-id"
@@ -86,6 +87,11 @@ void arch_livepatch_init(void);
 
 int arch_livepatch_verify_func(const struct livepatch_func *func);
 
+typedef void (*livepatch_trace_func_t)(unsigned long ip,
+                                        unsigned long parent_ip);
+void xen_livepatch_trace_dispatcher(unsigned long ip,
+                                    unsigned long parent_ip);
+
 static inline
 unsigned int livepatch_insn_len(const struct livepatch_func *func,
                                 const struct livepatch_fstate *state)
@@ -120,8 +126,10 @@ void arch_livepatch_revive(void);
 
 void arch_livepatch_apply(const struct livepatch_func *func,
                           struct livepatch_fstate *state);
+void arch_livepatch_apply_trace(const struct livepatch_func *trace);
 void arch_livepatch_revert(const struct livepatch_func *func,
                            struct livepatch_fstate *state);
+void arch_livepatch_revert_trace(const struct livepatch_func *trace);
 void arch_livepatch_post_action(void);
 
 void arch_livepatch_mask(void);
diff --git a/xen/include/xen/livepatch_payload.h b/xen/include/xen/livepatch_payload.h
index c6dc7cb5fa..3a81abe0de 100644
--- a/xen/include/xen/livepatch_payload.h
+++ b/xen/include/xen/livepatch_payload.h
@@ -52,9 +52,10 @@ struct payload {
     size_t ro_size;                      /* .. and its size (if any). */
     unsigned int pages;                  /* Total pages for [text,rw,ro]_addr */
     struct list_head applied_list;       /* Linked to 'applied_list'. */
-    const struct livepatch_func *funcs;  /* The array of functions to patch. */
-    struct livepatch_fstate *fstate;     /* State of patched functions. */
-    unsigned int nfuncs;                 /* Nr of functions to patch. */
+    const struct livepatch_func *funcs;  /* The array of functions to patch or trace. */
+    struct livepatch_fstate *fstate;     /* State of patched/traced functions. */
+    unsigned int nfuncs;                 /* Nr of functions to patch or trace. */
+    bool is_trace;                       /* Is a trace payload. */
     const struct livepatch_symbol *symtab; /* All symbols. */
     const char *strtab;                  /* Pointer to .strtab. */
     struct virtual_region region;        /* symbol, bug.frame patching and
-- 
2.34.1