mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-09-10 15:49:01 -04:00
Explicitly disallow the patching and referencing of init code/data. Otherwise it could potentially introduce some odd edge cases depending on whether the target object's init section has been freed yet (note that the init code still exists in the target module when doing late module patching). Such edge cases include sympos calculation and the patching and/or referencing of non-existent (init-freed) code/data. Not to mention the inherent differences in behavior that occur when the init code is only patched *some* of the time depending on module loading order or kernel config. Acked-by: Joe Lawrence <joe.lawrence@redhat.com> Acked-by: Song Liu <song@kernel.org> Link: https://patch.msgid.link/516e14f84cfbffa27dc19d3dcf35097504097966.1786138493.git.jpoimboe@kernel.org Signed-off-by: Josh Poimboeuf <jpoimboe@kernel.org>
422 lines
9.3 KiB
C
422 lines
9.3 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*
|
|
* Compute "sympos", the position used by livepatch to disambiguate
|
|
* duplicate symbol names in the patched object.
|
|
*/
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <fcntl.h>
|
|
|
|
#include <objtool/objtool.h>
|
|
#include <objtool/warn.h>
|
|
#include <objtool/endianness.h>
|
|
#include <objtool/klp.h>
|
|
|
|
#include <linux/string.h>
|
|
|
|
struct vmlinux_sym {
|
|
struct hlist_node hash;
|
|
const char *name;
|
|
u64 addr;
|
|
};
|
|
|
|
struct vmlinux_symid {
|
|
struct hlist_node hash;
|
|
u64 id;
|
|
u64 addr;
|
|
};
|
|
|
|
struct vmlinux_o_symid {
|
|
struct hlist_node hash;
|
|
u64 id;
|
|
unsigned int sym_idx;
|
|
};
|
|
|
|
static DEFINE_HASHTABLE(vmlinux_o_symids, 16);
|
|
|
|
/*
|
|
* The original linked kernel, found next to the orig vmlinux.o. Read with raw
|
|
* libelf rather than elf_open_read(): only the symbol table and the resolved
|
|
* .klp.symid table are needed, not the (huge) instruction/reloc machinery.
|
|
*
|
|
* Both tables are built once by read_orig_vmlinux(). The Elf handle stays
|
|
* open because the hashed names point into its mmapped string table.
|
|
*/
|
|
static struct {
|
|
Elf *elf;
|
|
DECLARE_HASHTABLE(syms, 16); /* name -> address */
|
|
DECLARE_HASHTABLE(symids, 16); /* .klp.symid id -> address */
|
|
} vmlinux;
|
|
|
|
/*
|
|
* Would the symbol be visible to the runtime's kallsyms-based symbol lookup?
|
|
*/
|
|
static bool vmlinux_sym_in_kallsyms(Elf *elf, GElf_Sym *sym)
|
|
{
|
|
unsigned int type = GELF_ST_TYPE(sym->st_info);
|
|
GElf_Shdr shdr;
|
|
Elf_Scn *scn;
|
|
|
|
if (sym->st_shndx == SHN_UNDEF || sym->st_shndx >= SHN_LORESERVE)
|
|
return false;
|
|
|
|
if (type == STT_SECTION || type == STT_FILE)
|
|
return false;
|
|
|
|
scn = elf_getscn(elf, sym->st_shndx);
|
|
if (!scn || !gelf_getshdr(scn, &shdr))
|
|
return false;
|
|
|
|
return shdr.sh_flags & SHF_ALLOC;
|
|
}
|
|
|
|
static int read_orig_vmlinux(const char *filename)
|
|
{
|
|
size_t shstrndx, nr_syms = 0, nr_symids = 0, strtab_idx = 0;
|
|
Elf_Data *symtab_data = NULL, *symid_data = NULL;
|
|
struct klp_symid *symids;
|
|
Elf_Scn *scn = NULL;
|
|
GElf_Ehdr ehdr;
|
|
int fd;
|
|
|
|
fd = open(filename, O_RDONLY);
|
|
if (fd == -1) {
|
|
ERROR_GLIBC("can't open '%s'", filename);
|
|
return -1;
|
|
}
|
|
|
|
if (elf_version(EV_CURRENT) == EV_NONE) {
|
|
ERROR_ELF("elf_version");
|
|
return -1;
|
|
}
|
|
|
|
vmlinux.elf = elf_begin(fd, ELF_C_READ_MMAP, NULL);
|
|
if (!vmlinux.elf) {
|
|
ERROR_ELF("elf_begin");
|
|
return -1;
|
|
}
|
|
|
|
if (!gelf_getehdr(vmlinux.elf, &ehdr)) {
|
|
ERROR_ELF("gelf_getehdr");
|
|
return -1;
|
|
}
|
|
|
|
if (elf_getshdrstrndx(vmlinux.elf, &shstrndx)) {
|
|
ERROR_ELF("elf_getshdrstrndx");
|
|
return -1;
|
|
}
|
|
|
|
while ((scn = elf_nextscn(vmlinux.elf, scn))) {
|
|
const char *name;
|
|
GElf_Shdr shdr;
|
|
|
|
if (!gelf_getshdr(scn, &shdr)) {
|
|
ERROR_ELF("gelf_getshdr");
|
|
return -1;
|
|
}
|
|
|
|
if (shdr.sh_type == SHT_SYMTAB) {
|
|
symtab_data = elf_getdata(scn, NULL);
|
|
if (!symtab_data) {
|
|
ERROR_ELF("elf_getdata");
|
|
return -1;
|
|
}
|
|
nr_syms = shdr.sh_size / shdr.sh_entsize;
|
|
strtab_idx = shdr.sh_link;
|
|
continue;
|
|
}
|
|
|
|
name = elf_strptr(vmlinux.elf, shstrndx, shdr.sh_name);
|
|
if (name && !strcmp(name, KLP_SYMID_SEC)) {
|
|
if (shdr.sh_size % sizeof(struct klp_symid)) {
|
|
ERROR("%s: %s: struct klp_symid size mismatch",
|
|
filename, KLP_SYMID_SEC);
|
|
return -1;
|
|
}
|
|
symid_data = elf_getdata(scn, NULL);
|
|
if (!symid_data) {
|
|
ERROR_ELF("elf_getdata");
|
|
return -1;
|
|
}
|
|
nr_symids = shdr.sh_size / sizeof(struct klp_symid);
|
|
}
|
|
}
|
|
|
|
if (!symtab_data) {
|
|
ERROR("%s: missing symbol table", filename);
|
|
return -1;
|
|
}
|
|
|
|
if (!symid_data) {
|
|
ERROR("%s: missing %s section, kernel not built with CONFIG_KLP_BUILD?",
|
|
filename, KLP_SYMID_SEC);
|
|
return -1;
|
|
}
|
|
|
|
for (size_t i = 0; i < nr_syms; i++) {
|
|
struct vmlinux_sym *vsym;
|
|
const char *name;
|
|
GElf_Sym s;
|
|
|
|
if (!gelf_getsym(symtab_data, i, &s)) {
|
|
ERROR_ELF("gelf_getsym");
|
|
return -1;
|
|
}
|
|
|
|
if (!vmlinux_sym_in_kallsyms(vmlinux.elf, &s))
|
|
continue;
|
|
|
|
name = elf_strptr(vmlinux.elf, strtab_idx, s.st_name);
|
|
if (!name)
|
|
continue;
|
|
|
|
vsym = calloc(1, sizeof(*vsym));
|
|
if (!vsym) {
|
|
ERROR_GLIBC("calloc");
|
|
return -1;
|
|
}
|
|
|
|
vsym->name = name;
|
|
vsym->addr = s.st_value;
|
|
hash_add(vmlinux.syms, &vsym->hash, str_hash(name));
|
|
}
|
|
|
|
symids = symid_data->d_buf;
|
|
|
|
for (size_t i = 0; i < nr_symids; i++) {
|
|
struct vmlinux_symid *vsymid;
|
|
|
|
vsymid = calloc(1, sizeof(*vsymid));
|
|
if (!vsymid) {
|
|
ERROR_GLIBC("calloc");
|
|
return -1;
|
|
}
|
|
|
|
vsymid->id = __bswap_if_needed(&ehdr, symids[i].id);
|
|
vsymid->addr = __bswap_if_needed(&ehdr, symids[i].addr);
|
|
hash_add(vmlinux.symids, &vsymid->hash, vsymid->id);
|
|
}
|
|
|
|
/* the fd and Elf handle stay open, the hashed names live in the mmap */
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Read the orig vmlinux.o's .klp.symid table, an array of entries whose 'addr'
|
|
* fields have relocs to the symbols they describe.
|
|
*/
|
|
static int read_vmlinux_o_symids(struct elf *vmlinux_o)
|
|
{
|
|
struct section *sec;
|
|
|
|
for_each_sec(vmlinux_o, sec) {
|
|
unsigned long nr;
|
|
|
|
if (strcmp(sec->name, KLP_SYMID_SEC))
|
|
continue;
|
|
|
|
if (sec_size(sec) % sizeof(struct klp_symid)) {
|
|
ERROR("%s: %s: struct klp_symid size mismatch",
|
|
vmlinux_o->name, KLP_SYMID_SEC);
|
|
return -1;
|
|
}
|
|
|
|
nr = sec_size(sec) / sizeof(struct klp_symid);
|
|
|
|
for (unsigned long i = 0; i < nr; i++) {
|
|
unsigned long offset = i * sizeof(struct klp_symid);
|
|
struct vmlinux_o_symid *entry;
|
|
struct klp_symid *symid;
|
|
struct reloc *reloc;
|
|
|
|
entry = calloc(1, sizeof(*entry));
|
|
if (!entry) {
|
|
ERROR_GLIBC("calloc");
|
|
return -1;
|
|
}
|
|
|
|
symid = sec->data->d_buf + offset;
|
|
entry->id = bswap_if_needed(vmlinux_o, symid->id);
|
|
|
|
reloc = find_reloc_by_dest(vmlinux_o, sec,
|
|
offset + offsetof(struct klp_symid, addr));
|
|
if (!reloc) {
|
|
ERROR("%s: missing reloc for %s entry",
|
|
vmlinux_o->name, KLP_SYMID_SEC);
|
|
return -1;
|
|
}
|
|
entry->sym_idx = reloc->sym->idx;
|
|
|
|
hash_add(vmlinux_o_symids, &entry->hash, entry->sym_idx);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int klp_sympos_init(struct elf *orig)
|
|
{
|
|
char *filename;
|
|
int ret;
|
|
|
|
if (!str_ends_with(objname, "vmlinux.o"))
|
|
return 0;
|
|
|
|
if (read_vmlinux_o_symids(orig))
|
|
return -1;
|
|
|
|
filename = strndup(objname, strlen(objname) - 2);
|
|
if (!filename) {
|
|
ERROR_GLIBC("strndup");
|
|
return -1;
|
|
}
|
|
|
|
ret = read_orig_vmlinux(filename);
|
|
free(filename);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/* Find the symbol's id in the orig vmlinux.o's .klp.symid table */
|
|
static int find_vmlinux_o_symid(struct symbol *sym, u64 *id)
|
|
{
|
|
struct vmlinux_o_symid *entry;
|
|
|
|
hash_for_each_possible(vmlinux_o_symids, entry, hash, sym->idx) {
|
|
if (entry->sym_idx == sym->idx) {
|
|
*id = entry->id;
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
ERROR("no %s entry for symbol %s in orig vmlinux.o", KLP_SYMID_SEC,
|
|
sym->name);
|
|
return -1;
|
|
}
|
|
|
|
/* Find the symbol's final address in the orig vmlinux's .klp.symid table */
|
|
static int find_vmlinux_symid_addr(u64 id, u64 *addr)
|
|
{
|
|
struct vmlinux_symid *symid;
|
|
|
|
hash_for_each_possible(vmlinux.symids, symid, hash, id) {
|
|
if (symid->id == id) {
|
|
*addr = symid->addr;
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* Find the sympos of a vmlinux-local symbol by ranking its final address
|
|
* among the duplicately named symbols in the linked orig vmlinux, replicating
|
|
* the order in which kallsyms_on_each_match_symbol() counts them.
|
|
*/
|
|
static unsigned long find_vmlinux_sympos(struct symbol *sym)
|
|
{
|
|
unsigned long nr_matches = 0, sympos = 1;
|
|
u32 key = str_hash(sym->name);
|
|
struct vmlinux_sym *vsym;
|
|
bool found = false;
|
|
u64 id, addr;
|
|
|
|
hash_for_each_possible(vmlinux.syms, vsym, hash, key)
|
|
if (!strcmp(vsym->name, sym->name))
|
|
nr_matches++;
|
|
|
|
if (!nr_matches) {
|
|
ERROR("can't find symbol %s in orig vmlinux", sym->name);
|
|
return ULONG_MAX;
|
|
}
|
|
|
|
/*
|
|
* Unique symbols don't need disambiguating. They also have no
|
|
* .klp.symid entry, which is only emitted for names duplicated in
|
|
* vmlinux.o, so the lookups below would fail.
|
|
*/
|
|
if (nr_matches == 1)
|
|
return 0;
|
|
|
|
if (find_vmlinux_o_symid(sym, &id))
|
|
return ULONG_MAX;
|
|
|
|
if (find_vmlinux_symid_addr(id, &addr)) {
|
|
ERROR("no %s entry for symbol %s in orig vmlinux", KLP_SYMID_SEC,
|
|
sym->name);
|
|
return ULONG_MAX;
|
|
}
|
|
|
|
hash_for_each_possible(vmlinux.syms, vsym, hash, key) {
|
|
if (strcmp(vsym->name, sym->name))
|
|
continue;
|
|
|
|
if (vsym->addr < addr)
|
|
sympos++;
|
|
else if (vsym->addr == addr)
|
|
found = true;
|
|
}
|
|
|
|
if (!found) {
|
|
ERROR("%s address mismatch for symbol %s, stale orig vmlinux?",
|
|
KLP_SYMID_SEC, sym->name);
|
|
return ULONG_MAX;
|
|
}
|
|
|
|
return sympos;
|
|
}
|
|
|
|
static bool is_init_sym(struct symbol *sym)
|
|
{
|
|
return strstarts(sym->sec->name, ".init");
|
|
}
|
|
|
|
/*
|
|
* "sympos" is used by livepatch to disambiguate duplicate symbol names.
|
|
*/
|
|
unsigned long klp_find_sympos(struct elf *elf, struct symbol *sym)
|
|
{
|
|
unsigned long sympos = 0, nr_matches = 0;
|
|
bool has_dup = false;
|
|
struct symbol *s;
|
|
|
|
if (is_init_sym(sym)) {
|
|
ERROR("%s: can't patch or reference init code/data", sym->name);
|
|
return ULONG_MAX;
|
|
}
|
|
|
|
if (sym->bind != STB_LOCAL)
|
|
return 0;
|
|
|
|
/*
|
|
* vmlinux: the final link reorders symbols relative to vmlinux.o,
|
|
* so the position needs to be derived from the linked orig vmlinux via
|
|
* the .klp.symid table.
|
|
*/
|
|
if (vmlinux.elf)
|
|
return find_vmlinux_sympos(sym);
|
|
|
|
/*
|
|
* modules: the final .ko preserves symbol table order, so a
|
|
* symtab-order count here matches the runtime count done by
|
|
* module_kallsyms_on_each_symbol().
|
|
*/
|
|
for_each_sym(elf, s) {
|
|
if (!strcmp(s->name, sym->name)) {
|
|
nr_matches++;
|
|
if (s == sym)
|
|
sympos = nr_matches;
|
|
else
|
|
has_dup = true;
|
|
}
|
|
}
|
|
|
|
if (!sympos) {
|
|
ERROR("can't find sympos for %s", sym->name);
|
|
return ULONG_MAX;
|
|
}
|
|
|
|
return has_dup ? sympos : 0;
|
|
}
|