From: George Guo <[email protected]>

A livepatch module is mapped far more than the +-2GB reachable by a
pcalau12i/addi.d pair, so a klp relocation that resolves to a symbol in
vmlinux cannot be reached PC-relatively: the relocation overflows and
the patched function computes a wild pointer.  Far calls are already
handled by the module loader, which redirects them through a PLT stub,
but there is no such indirection for data, whose address is
materialized inline.

Compilers emit the direct PC-relative form for any symbol they consider
local.  In particular file-local 'static' data is never interposable, so
-fPIC does not route it through the GOT.  This is correct for vmlinux and
for ordinary modules; it only breaks once klp-diff extracts the function
into the far livepatch module while its data stays behind.  It is not
Clang-specific: GCC emits the same PC-relative form.

Add an arch hook, arch_klp_convert_reloc_to_got(), invoked when a klp
relocation is created, and implement it for LoongArch: rewrite the
reference to its GOT-indirect equivalent, which loads the full 64-bit
address from a GOT slot the module loader fills in:

        pcalau12i rd, %pc_hi20(sym)      ->  pcalau12i rd, %got_pc_hi20(sym)
        addi.d    rd, rj, %pc_lo12(sym)  ->  ld.d      rd, rj, %got_pc_lo12(sym)

The loader already sizes the module GOT from every SHT_RELA section that
targets executable code, including .klp.rela.*, so the slot is reserved
at load time and filled when the patch is enabled.

Only an adjacent, register-consistent pcalau12i/addi.d pair that
materializes an address can be converted.  A PCALA_LO12 on any other
instruction is a load or store that reads the symbol directly; going
through the GOT there needs an extra instruction to dereference the
slot, and the function cannot grow without shifting every offset,
relocation and ORC entry.  A pcalau12i shared by several addi.d with
different addends would likewise need one GOT slot per addend, which a
single GOT_PC_HI20 cannot express.  Reject both rather than emit a
livepatch that is quietly broken.

The LoongArch decoder in tools/ lacks the reg1i20 instruction format
used to identify pcalau12i; add it to the tools/ copy of asm/inst.h.

Reproduced by a livepatch for proc_pid_status() (the shadow-newpid
test), which reads the file-local task_state_array[].

Signed-off-by: George Guo <[email protected]>
---
 tools/arch/loongarch/include/asm/inst.h       |  11 ++
 tools/objtool/arch/loongarch/decode.c         | 104 ++++++++++++++++++
 .../objtool/arch/loongarch/include/arch/elf.h |  16 +++
 tools/objtool/klp-diff.c                      |  23 +++-
 4 files changed, 152 insertions(+), 2 deletions(-)

diff --git a/tools/arch/loongarch/include/asm/inst.h 
b/tools/arch/loongarch/include/asm/inst.h
index d68fad63c8b7..0d4caa6522f1 100644
--- a/tools/arch/loongarch/include/asm/inst.h
+++ b/tools/arch/loongarch/include/asm/inst.h
@@ -25,6 +25,10 @@ enum reg1i21_op {
        bcnez_op        = 0x12, /* bits[9:8] = 0x01 */
 };
 
+enum reg1i20_op {
+       pcalau12i_op    = 0x0d,
+};
+
 enum reg2_op {
        ertn_op         = 0x1920e,
 };
@@ -66,6 +70,12 @@ struct reg0i26_format {
        unsigned int opcode : 6;
 };
 
+struct reg1i20_format {
+       unsigned int rd : 5;
+       unsigned int immediate : 20;
+       unsigned int opcode : 7;
+};
+
 struct reg1i21_format {
        unsigned int immediate_h  : 5;
        unsigned int rj : 5;
@@ -111,6 +121,7 @@ union loongarch_instruction {
        unsigned int word;
        struct reg0i15_format   reg0i15_format;
        struct reg0i26_format   reg0i26_format;
+       struct reg1i20_format   reg1i20_format;
        struct reg1i21_format   reg1i21_format;
        struct reg2_format      reg2_format;
        struct reg2i12_format   reg2i12_format;
diff --git a/tools/objtool/arch/loongarch/decode.c 
b/tools/objtool/arch/loongarch/decode.c
index 273398ea4486..99765b13058c 100644
--- a/tools/objtool/arch/loongarch/decode.c
+++ b/tools/objtool/arch/loongarch/decode.c
@@ -101,6 +101,110 @@ int arch_normalize_paired_reloc(struct elf *elf, struct 
reloc *reloc)
        return 0;
 }
 
+static bool klp_read_insn(struct section *sec, unsigned long offset,
+                         union loongarch_instruction *insn)
+{
+       if (offset + sizeof(*insn) > sec_size(sec))
+               return false;
+
+       memcpy(insn, sec->data->d_buf + offset, sizeof(*insn));
+       return true;
+}
+
+/*
+ * A livepatch module is mapped far more than the +-2GB reachable by
+ * pcalau12i/addi.d, so a klp reloc which resolves to a symbol in vmlinux 
cannot
+ * be reached PC-relatively: the relocation overflows and the patched function
+ * computes a wild pointer.  Far calls are already handled by the module 
loader,
+ * which redirects them through a PLT stub, but there is no such indirection 
for
+ * data: the address is materialized inline.
+ *
+ * Compilers emit the direct PC-relative form for any symbol they consider 
local
+ * (in particular file-local 'static' data, which is never interposable, so
+ * -fPIC does not route it through the GOT).  This is correct for vmlinux and
+ * for ordinary modules; it only breaks once klp-diff extracts the function 
into
+ * a far away livepatch module while its data stays behind.
+ *
+ * Convert the reference to its GOT-indirect equivalent, which loads the full
+ * 64-bit address from a GOT slot the module loader fills in and so works at 
any
+ * distance:
+ *
+ *     pcalau12i rd, %pc_hi20(sym)     ->  pcalau12i rd, %got_pc_hi20(sym)
+ *     addi.d    rd, rj, %pc_lo12(sym) ->  ld.d      rd, rj, %got_pc_lo12(sym)
+ *
+ * Only an adjacent pcalau12i/addi.d pair which materializes an address can be
+ * converted, and only that exact shape is accepted:
+ *
+ *  - A PCALA_LO12 on anything but addi.d is a load or store reading the symbol
+ *    directly.  Going through the GOT would need an extra instruction to
+ *    dereference the slot, and the function cannot grow without shifting every
+ *    offset, relocation and ORC entry.
+ *
+ *  - The pair must be adjacent and register-consistent.  If one pcalau12i is
+ *    shared by several addi.d with different addends, each needs its own GOT
+ *    slot, but the single GOT_PC_HI20 only spans one page: the result would be
+ *    correct only if those slots happened to share a page.  Requiring
+ *    adjacency rejects that (the second addi.d is not preceded by a 
pcalau12i),
+ *    as well as a pair the compiler scheduled apart.
+ *
+ * Bail out on anything else instead of emitting a livepatch which is quietly
+ * broken.
+ */
+int arch_klp_convert_reloc_to_got(struct elf *elf, struct section *sec,
+                                 unsigned long offset, unsigned int *type)
+{
+       union loongarch_instruction hi, lo;
+       unsigned long hi_offset, lo_offset;
+
+       switch (*type) {
+       case R_LARCH_PCALA_HI20:
+               hi_offset = offset;
+               lo_offset = offset + LOONGARCH_INSN_SIZE;
+               break;
+
+       case R_LARCH_PCALA_LO12:
+               if (offset < LOONGARCH_INSN_SIZE) {
+                       ERROR("%s+0x%lx: PCALA_LO12 with no preceding 
instruction",
+                             sec->name, offset);
+                       return -1;
+               }
+               hi_offset = offset - LOONGARCH_INSN_SIZE;
+               lo_offset = offset;
+               break;
+
+       default:
+               return 0;
+       }
+
+       if (!klp_read_insn(sec, hi_offset, &hi) ||
+           !klp_read_insn(sec, lo_offset, &lo)) {
+               ERROR("%s+0x%lx: PCALA pair runs past end of section",
+                     sec->name, offset);
+               return -1;
+       }
+
+       if (hi.reg1i20_format.opcode != pcalau12i_op ||
+           lo.reg2i12_format.opcode != addid_op ||
+           lo.reg2i12_format.rj != hi.reg1i20_format.rd) {
+               ERROR("%s+0x%lx: cannot make klp reference GOT-indirect: 
expected an adjacent pcalau12i/addi.d pair, found 0x%08x/0x%08x",
+                     sec->name, offset, hi.word, lo.word);
+               return -1;
+       }
+
+       if (*type == R_LARCH_PCALA_HI20) {
+               *type = R_LARCH_GOT_PC_HI20;
+               return 0;
+       }
+
+       /* addi.d rd, rj, %pc_lo12(sym) -> ld.d rd, rj, %got_pc_lo12(sym) */
+       lo.reg2i12_format.opcode = ldd_op;
+       if (elf_write_insn(elf, sec, lo_offset, sizeof(lo), (const char *)&lo))
+               return -1;
+
+       *type = R_LARCH_GOT_PC_LO12;
+       return 0;
+}
+
 bool arch_pc_relative_reloc(struct reloc *reloc)
 {
        return false;
diff --git a/tools/objtool/arch/loongarch/include/arch/elf.h 
b/tools/objtool/arch/loongarch/include/arch/elf.h
index e2843679f24e..7a97cf69dc86 100644
--- a/tools/objtool/arch/loongarch/include/arch/elf.h
+++ b/tools/objtool/arch/loongarch/include/arch/elf.h
@@ -27,6 +27,18 @@
 #ifndef R_LARCH_SUB64
 #define R_LARCH_SUB64          56
 #endif
+#ifndef R_LARCH_PCALA_HI20
+#define R_LARCH_PCALA_HI20     71
+#endif
+#ifndef R_LARCH_PCALA_LO12
+#define R_LARCH_PCALA_LO12     72
+#endif
+#ifndef R_LARCH_GOT_PC_HI20
+#define R_LARCH_GOT_PC_HI20    75
+#endif
+#ifndef R_LARCH_GOT_PC_LO12
+#define R_LARCH_GOT_PC_LO12    76
+#endif
 #ifndef R_LARCH_32_PCREL
 #define R_LARCH_32_PCREL       99
 #endif
@@ -48,9 +60,13 @@
 
 #define ARCH_HAS_INLINE_ALTS   1
 #define ARCH_HAS_PAIRED_RELOCS 1
+#define ARCH_HAS_KLP_GOT_RELOCS        1
 
 struct elf;
 struct reloc;
+struct section;
 int arch_normalize_paired_reloc(struct elf *elf, struct reloc *reloc);
+int arch_klp_convert_reloc_to_got(struct elf *elf, struct section *sec,
+                                 unsigned long offset, unsigned int *type);
 
 #endif /* _OBJTOOL_ARCH_ELF_H */
diff --git a/tools/objtool/klp-diff.c b/tools/objtool/klp-diff.c
index f151ffc71184..1538f8f92f1f 100644
--- a/tools/objtool/klp-diff.c
+++ b/tools/objtool/klp-diff.c
@@ -1525,6 +1525,14 @@ static inline int arch_normalize_paired_reloc(struct elf 
*elf, struct reloc *rel
 }
 #endif
 
+#ifndef ARCH_HAS_KLP_GOT_RELOCS
+static inline int arch_klp_convert_reloc_to_got(struct elf *elf, struct 
section *sec,
+                                               unsigned long offset, unsigned 
int *type)
+{
+       return 0;
+}
+#endif
+
 /*
  * Convert a relocation symbol reference to the needed format: either a section
  * symbol or the underlying symbol itself.  Return -1 error, 0 success, 1 skip.
@@ -1565,12 +1573,23 @@ static int clone_reloc_klp(struct elfs *e, struct reloc 
*patched_reloc,
        char sym_name[SYM_NAME_LEN];
        struct klp_reloc klp_reloc;
        unsigned long sympos;
+       unsigned int type;
 
        if (!patched_sym->twin) {
                ERROR("unexpected klp reloc for new symbol %s", 
patched_sym->name);
                return -1;
        }
 
+       /*
+        * This reference resolves to a symbol which stays in the patched
+        * object, arbitrarily far from the livepatch module.  Give the arch a
+        * chance to convert a PC-relative reference into an indirect one which
+        * can reach it (LoongArch: PCALA -> GOT).
+        */
+       type = reloc_type(patched_reloc);
+       if (arch_klp_convert_reloc_to_got(e->out, sec, offset, &type))
+               return -1;
+
        /*
         * Keep the original reloc intact for now to avoid breaking objtool run
         * which relies on proper relocations for many of its features.  This
@@ -1591,7 +1610,7 @@ static int clone_reloc_klp(struct elfs *e, struct reloc 
*patched_reloc,
                sym->clone = patched_sym;
        }
 
-       if (!elf_create_reloc(e->out, sec, offset, sym, addend, 
reloc_type(patched_reloc)))
+       if (!elf_create_reloc(e->out, sec, offset, sym, addend, type))
                return -1;
 
        /*
@@ -1648,7 +1667,7 @@ static int clone_reloc_klp(struct elfs *e, struct reloc 
*patched_reloc,
        klp_reloc_off = sec_size(klp_relocs);
        memset(&klp_reloc, 0, sizeof(klp_reloc));
 
-       klp_reloc.type = reloc_type(patched_reloc);
+       klp_reloc.type = type;
        if (!elf_add_data(e->out, klp_relocs, &klp_reloc, sizeof(klp_reloc), 
true))
                return -1;
 
-- 
2.53.0


Reply via email to