Thread (15 messages) 15 messages, 1 author, 1d ago
DORMANTno replies

[PATCH v4 14/14] objtool/klp: Rewrite PC-relative data references to GOT on LoongArch

From: George Guo <hidden>
Date: 2026-07-24 11:45:49
Also in: lkml, llvm, loongarch
Subsystem: objtool, the rest · Maintainers: Josh Poimboeuf, Peter Zijlstra, Linus Torvalds

From: George Guo <redacted>

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 <redacted>
---
 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
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help