[PATCH v2 1/1] objtool/klp: compute sympos during checksum phase
Joe Lawrence <[email protected]> Fri, 24 Jul 2026 18:17:30 -0400
| Newsgroups | org.kernel.vger.live-patching |
|---|---|
| Message-ID | <[email protected]> |
The sympos value tells livepatch which instance of a duplicate local symbol to target. Previously, klp-diff determined sympos by iterating through the vmlinux.o symbol table and applying heuristics to infer the linked binary's ordering. This was fragile and incorrect for data symbols whose sections get reordered by the linker script. Fix this by computing sympos during the checksum phase, while the original linked objects (vmlinux, .ko) are still intact. Add a --linked-obj option to `objtool klp checksum` which opens the linked binary, determines the correct ordering (by address for vmlinux, by symbol table index for modules), and writes a .discard.sym_order section into the checksummed .o file. Later, `objtool klp diff` reads .discard.sym_order from the orig checksummed file for sympos lookups. This replaces the old heuristic entirely. Signed-off-by: Joe Lawrence <[email protected]> --- scripts/livepatch/klp-build | 36 +++- .../objtool/include/objtool/checksum_types.h | 6 + tools/objtool/klp-checksum.c | 195 +++++++++++++++++- tools/objtool/klp-diff.c | 88 ++++---- 4 files changed, 267 insertions(+), 58 deletions(-) diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build index c4a7acf8edc3..a5ddde6bf7c4 100755 --- a/scripts/livepatch/klp-build +++ b/scripts/livepatch/klp-build @@ -582,7 +582,8 @@ find_objects() { -printf '%P\n' } -# Copy all .o archives to $ORIG_DIR +# Copy all .o archives to $ORIG_DIR, plus the linked vmlinux/.ko binaries +# needed later for address-based (vmlinux) / index-based (.ko) sympos. copy_orig_objects() { local files=() @@ -602,6 +603,16 @@ copy_orig_objects() { mkdir -p "$orig_dir" cp -f "$file" "$orig_dir" + + if [[ "$_file" == "vmlinux.o" ]]; then + [[ ! -f "$PWD/vmlinux" ]] && die "missing vmlinux" + cp -f "$PWD/vmlinux" "$ORIG_DIR/vmlinux" + else + local ko="${_file%.*}.ko" + [[ ! -f "$PWD/$ko" ]] && die "missing $ko" + mkdir -p "$(dirname "$ORIG_DIR/$ko")" + cp -f "$PWD/$ko" "$ORIG_DIR/$ko" + fi done xtrace_restore @@ -650,13 +661,15 @@ copy_patched_objects() { } # Copy .o files to a separate directory and run "objtool klp checksum" on each -# copy. The checksums are written to a .discard.sym_checksum section. +# copy. The checksums are written to a .discard.sym_checksum section, and +# duplicate-symbol sympos values (from --linked-obj) to .discard.sym_order. # # If match_dir is given, only process files which also exist there. generate_checksums() { local src_dir="$1" local dest_dir="$2" - local match_dir="${3:-}" + local linked_root="$3" + local match_dir="${4:-}" local files=() local file @@ -667,12 +680,23 @@ generate_checksums() { for file in "${files[@]}"; do local rel="${file#"$src_dir"/}" local dest="$dest_dir/$rel" + local linked_obj + local linked_obj_arg=() [[ -n "$match_dir" && ! -f "$match_dir/$rel" ]] && continue mkdir -p "$(dirname "$dest")" cp -f "$file" "$dest" - "$OBJTOOL" klp checksum "$dest" + + if [[ "$rel" == "vmlinux.o" ]]; then + linked_obj="$linked_root/vmlinux" + else + linked_obj="$linked_root/${rel%.*}.ko" + fi + [[ ! -f "$linked_obj" ]] && die "missing linked object: $linked_obj" + linked_obj_arg=(--linked-obj "$linked_obj") + + "$OBJTOOL" klp checksum "${linked_obj_arg[@]}" "$dest" done touch "$dest_dir/.complete" @@ -911,9 +935,9 @@ fi if (( SHORT_CIRCUIT <= 3 )); then status "Generating original checksums" - generate_checksums "$ORIG_DIR" "$ORIG_CSUM_DIR" "$PATCHED_DIR" + generate_checksums "$ORIG_DIR" "$ORIG_CSUM_DIR" "$ORIG_DIR" "$PATCHED_DIR" status "Generating patched checksums" - generate_checksums "$PATCHED_DIR" "$PATCHED_CSUM_DIR" + generate_checksums "$PATCHED_DIR" "$PATCHED_CSUM_DIR" "$PWD" fi if (( SHORT_CIRCUIT <= 4 )); then diff --git a/tools/objtool/include/objtool/checksum_types.h b/tools/objtool/include/objtool/checksum_types.h index 507efdd8ab5b..2eb5c1cce0c6 100644 --- a/tools/objtool/include/objtool/checksum_types.h +++ b/tools/objtool/include/objtool/checksum_types.h @@ -7,6 +7,12 @@ struct sym_checksum { u64 checksum; }; +struct sym_sympos { + u64 addr; + u32 sympos; + u32 pad; +}; + #ifdef BUILD_KLP #include <xxhash.h> diff --git a/tools/objtool/klp-checksum.c b/tools/objtool/klp-checksum.c index b8e47f28997e..d16f00a3bb2a 100644 --- a/tools/objtool/klp-checksum.c +++ b/tools/objtool/klp-checksum.c @@ -1,5 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include <string.h> +#include <stdlib.h> +#include <fcntl.h> #include <subcmd/parse-options.h> #include <objtool/arch.h> @@ -11,6 +13,10 @@ #include <objtool/warn.h> #include <objtool/checksum.h> +#include <linux/string.h> + +static const char *linked_obj_name; + static int checksum_debug_init(struct objtool_file *file) { char *dup, *s; @@ -250,6 +256,183 @@ int calculate_checksums(struct objtool_file *file) return 0; } +struct linked_sym_entry { + unsigned long key; + unsigned int sort_idx; + const char *file_name; +}; + +static int create_sym_order_section(struct objtool_file *file) +{ + struct elf *linked_elf; + struct section *sec; + struct symbol *sym, *lsym; + size_t entsize = sizeof(struct sym_sympos); + unsigned int idx = 0; + bool by_address; + int ret = -1; + + sec = find_section_by_name(file->elf, ".discard.sym_order"); + if (sec) { + WARN("file already has .discard.sym_order section, skipping"); + return 0; + } + + linked_elf = elf_open_read(linked_obj_name, O_RDONLY); + if (!linked_elf) + return -1; + + /* + * For vmlinux, index by address as per: + * kernel/kallsyms.c:kallsyms_on_each_match_symbol() + * + * For modules, index by symtab index as per: + * kernel/module/kallsyms.c:module_kallsyms_on_each_symbol() + */ + by_address = str_ends_with(linked_obj_name, "vmlinux"); + + /* + * Count local symbols in file.o that have duplicates in the linked + * object (vmlinux or .ko). Only those need sympos. + */ + for_each_sym(file->elf, sym) { + int nr = 0; + + if (sym->bind != STB_LOCAL) + continue; + if (!is_func_sym(sym) && !is_object_sym(sym)) + continue; + + for_each_sym_by_name(linked_elf, sym->name, lsym) { + if (lsym->bind != STB_LOCAL) + continue; + if (!is_func_sym(lsym) && !is_object_sym(lsym)) + continue; + nr++; + } + + if (nr > 1) + idx++; + } + + if (!idx) { + ret = 0; + goto out; + } + + sec = elf_create_section_pair(file->elf, ".discard.sym_order", entsize, + idx, idx); + if (!sec) + goto out; + + /* + * For each local symbol with homonyms in the linked binary, determine + * its sympos: sort the duplicates by address (vmlinux) or symtab + * index (modules), then find our position by filename. + */ + idx = 0; + for_each_sym(file->elf, sym) { + struct linked_sym_entry *entries; + const char *sym_file_name; + struct sym_sympos *entry; + int nr = 0, i; + long pos = -1; + + if (sym->bind != STB_LOCAL) + continue; + if (!is_func_sym(sym) && !is_object_sym(sym)) + continue; + + /* First pass: count to size entries[] */ + for_each_sym_by_name(linked_elf, sym->name, lsym) { + if (lsym->bind != STB_LOCAL) + continue; + if (!is_func_sym(lsym) && !is_object_sym(lsym)) + continue; + nr++; + } + + if (nr <= 1) + continue; + + entries = calloc(nr, sizeof(*entries)); + if (!entries) { + ERROR_GLIBC("calloc"); + goto out; + } + + /* Second pass: collect address/index and filename for dupes */ + nr = 0; + for_each_sym_by_name(linked_elf, sym->name, lsym) { + if (lsym->bind != STB_LOCAL) + continue; + if (!is_func_sym(lsym) && !is_object_sym(lsym)) + continue; + entries[nr].key = by_address ? lsym->sym.st_value : lsym->idx; + entries[nr].sort_idx = lsym->idx; + entries[nr].file_name = lsym->file ? lsym->file->name : NULL; + nr++; + } + + /* Insertion sort by key, tie-break by symtab index */ + for (i = 1; i < nr; i++) { + struct linked_sym_entry tmp = entries[i]; + int j = i - 1; + + while (j >= 0 && (entries[j].key > tmp.key || + (entries[j].key == tmp.key && + entries[j].sort_idx > tmp.sort_idx))) { + entries[j + 1] = entries[j]; + j--; + } + entries[j + 1] = tmp; + } + + /* Match our compilation unit's filename against sorted entries */ + sym_file_name = sym->file ? sym->file->name : NULL; + for (i = 0; i < nr; i++) { + bool match; + + if (sym_file_name && entries[i].file_name) + match = !strcmp(sym_file_name, entries[i].file_name); + else if (!sym_file_name && !entries[i].file_name) + match = true; + else + match = false; + + if (match) { + pos = i + 1; + break; + } + } + + free(entries); + + if (pos < 0) { + ERROR("can't determine sympos for %s", sym->name); + goto out; + } + + /* Emit reloc pointing to sym, with its sympos in the data entry */ + if (!elf_init_reloc(file->elf, sec->rsec, idx, idx * entsize, + sym, 0, R_TEXT64)) + goto out; + + entry = (struct sym_sympos *)sec->data->d_buf + idx; + entry->addr = 0; /* reloc */ + entry->sympos = (u32)pos; + entry->pad = 0; + + mark_sec_changed(file->elf, sec, true); + idx++; + } + + ret = 0; +out: + elf_close(linked_elf); + return ret; +} + int create_sym_checksum_section(struct objtool_file *file) { struct section *sec; @@ -297,7 +480,7 @@ int create_sym_checksum_section(struct objtool_file *file) } static const char * const klp_checksum_usage[] = { - "objtool klp checksum [<options>] file.o", + "objtool klp checksum --linked-obj <vmlinux|file.ko> [<options>] file.o", NULL, }; @@ -309,6 +492,7 @@ int cmd_klp_checksum(int argc, const char **argv) const struct option options[] = { OPT_STRING(0, "debug-checksum", &opts.debug_checksum, "syms", "enable checksum debug output"), OPT_BOOLEAN(0, "dry-run", &opts.dryrun, "don't write modifications"), + OPT_STRING(0, "linked-obj", &linked_obj_name, "file", "linked binary (vmlinux or .ko) for sympos ordering"), OPT_END(), }; @@ -316,6 +500,11 @@ int cmd_klp_checksum(int argc, const char **argv) if (argc != 1) usage_with_options(klp_checksum_usage, options); + if (!linked_obj_name) { + ERROR("--linked-obj is required"); + usage_with_options(klp_checksum_usage, options); + } + opts.checksum = true; objname = argv[0]; @@ -332,6 +521,10 @@ int cmd_klp_checksum(int argc, const char **argv) if (ret) goto out; + ret = create_sym_order_section(file); + if (ret) + goto out; + ret = create_sym_checksum_section(file); out: diff --git a/tools/objtool/klp-diff.c b/tools/objtool/klp-diff.c index f7a02c4a2429..89fb43eb4e71 100644 --- a/tools/objtool/klp-diff.c +++ b/tools/objtool/klp-diff.c @@ -242,6 +242,41 @@ static int read_sym_checksums(struct elf *elf) return 0; } +/* + * Look up a symbol's sympos from the .discard.sym_order section. + * Returns > 0 for the position among duplicates, or 0 if the symbol is + * unique (either not in the section or the section doesn't exist). + */ +static long find_sympos_from_order_section(struct elf *elf, struct symbol *sym) +{ + struct section *sec; + int nr; + + sec = find_section_by_name(elf, ".discard.sym_order"); + if (!sec) + return 0; + + if (!sec->rsec) + return 0; + + nr = sec_size(sec) / sizeof(struct sym_sympos); + for (int i = 0; i < nr; i++) { + struct sym_sympos *entry; + struct reloc *reloc; + + reloc = find_reloc_by_dest(elf, sec, i * sizeof(struct sym_sympos)); + if (!reloc) + continue; + + if (reloc->sym == sym) { + entry = (struct sym_sympos *)sec->data->d_buf + i; + return (long)entry->sympos; + } + } + + return 0; +} + static struct symbol *first_file_symbol(struct elf *elf) { struct symbol *sym; @@ -344,6 +379,7 @@ static bool is_special_section(struct section *sec) static const char * const non_special_discards[] = { ".discard.addressable", ".discard.sym_checksum", + ".discard.sym_order", }; if (is_text_sec(sec)) @@ -898,60 +934,10 @@ static int correlate_symbols(struct elfs *e) /* "sympos" is used by livepatch to disambiguate duplicate symbol names */ static unsigned long find_sympos(struct elf *elf, struct symbol *sym) { - bool vmlinux = str_ends_with(objname, "vmlinux.o"); - unsigned long sympos = 0, nr_matches = 0; - bool has_dup = false; - struct symbol *s; - if (sym->bind != STB_LOCAL) return 0; - if (vmlinux && is_func_sym(sym)) { - /* - * HACK: Unfortunately, symbol ordering can differ between - * vmlinux.o and vmlinux due to the linker script emitting - * .text.unlikely* before .text*. Count .text.unlikely* first. - * - * TODO: Disambiguate symbols more reliably (checksums?) - */ - for_each_sym(elf, s) { - if (strstarts(s->sec->name, ".text.unlikely") && - !strcmp(s->name, sym->name)) { - nr_matches++; - if (s == sym) - sympos = nr_matches; - else - has_dup = true; - } - } - for_each_sym(elf, s) { - if (!strstarts(s->sec->name, ".text.unlikely") && - !strcmp(s->name, sym->name)) { - nr_matches++; - if (s == sym) - sympos = nr_matches; - else - has_dup = true; - } - } - } else { - 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; + return (unsigned long)find_sympos_from_order_section(elf, sym); } static int clone_sym_relocs(struct elfs *e, struct symbol *patched_sym); -- 2.54.0