From: sashiko-bot@kernel.org
To: "Longjun Luo" <luolongjuna@gmail.com>,
jpoimboe@kernel.org, peterz@infradead.org
Cc: mbenes@suse.cz, pmladek@suse.com, joe.lawrence@redhat.com,
song@kernel.org, puranjay@kernel.org, nathan@kernel.org,
ndesaulniers@google.com, morbo@google.com,
justinstitt@google.com, live-patching@vger.kernel.org
Subject: Re: [RFC PATCH v2 07/11] objtool/klp: Correlate symbols using DWARF identities
Date: Wed, 04 Nov 2026 14:09:23 +0000 [thread overview]
Message-ID: <sashiko-outbox-197397@kernel.org> (raw)
In-Reply-To: <20261104133158.3187525-8-luolongjuna@gmail.com>
Thank you for your contribution! Sashiko AI review found 9 potential issue(s) to consider:
- [High] objtool/klp: broken checksum and twin resolution for aliased symbols
- [High] objtool/klp: mark_changed_functions() treats ambiguous functions as new
- [Medium] objtool/klp: build_symtab_hash() uses sym->file->name instead of DWARF source
- [Medium] objtool/klp: check_linkage_changes() matches unrelated symbols across TUs
- [Medium] objtool/klp: __LINE__ shifts break __UNIQUE_ID_ddebug_* correlation
- [Medium] objtool/klp: unreachable .lto_priv matching in skip_lto_priv()
- [Medium] objtool/klp: broken .llvm and counter matching in compatible_symbol_name()
- [Medium] objtool/klp: zero-length symbols skipped by correlation are cloned as new
- [Medium] objtool/klp: adding -g breaks sympos, linkage-flip, and multi-TU tests
--
commit 4e3aabc6f0126e260b43d87b5c01364b588687eb
Author: Longjun Luo <luolongjuna@gmail.com>
objtool/klp: Correlate symbols using DWARF identities
This commit replaces positional and suffix-based symbol correlation in
objtool klp diff with DWARF identity matching, checksum disambiguation, and
a restricted FILE/name fallback.
> diff --git a/tools/objtool/klp-diff.c b/tools/objtool/klp-diff.c
> index cd5e683d6468b..468b8654b6871 100644
> --- a/tools/objtool/klp-diff.c
> +++ b/tools/objtool/klp-diff.c
[ ... ]
> @@ -486,361 +473,357 @@ static bool dont_correlate(struct symbol *sym)
> is_anonymous_rodata(sym) ||
> is_initcall_sym(sym) ||
> is_addressable_sym(sym) ||
> + is_metadata_section(sym->sec) ||
> is_special_section(sym->sec) ||
> is_special_section_aux(sym->sec);
> }
>
> -static const char *llvm_suffix(const char *name)
> +static size_t llvm_name_len(const char *name)
> {
> - return strstr(name, ".llvm.");
> + const char *suffix = strstr(name, ".llvm."), *p;
> +
> + if (!suffix || !isdigit((unsigned char)suffix[6]))
> + return strlen(name);
> + for (p = suffix + 6; isdigit((unsigned char)*p); p++)
> + ;
> + return *p ? strlen(name) : (size_t)(suffix - name);
> }
>
> -static bool is_llvm_sym(struct symbol *sym)
> +/* Keep object-fragment numbers, e.g. amd_northbridges.0. */
> +static bool same_llvm_name(const char *a, const char *b)
> {
> - return llvm_suffix(sym->name);
> + size_t a_len = llvm_name_len(a), b_len = llvm_name_len(b);
> +
> + return a_len == b_len && !strncmp(a, b, a_len);
> }
>
> -/*
> - * Determine if two symbols have compatible source file origins:
> - *
> - * - If both symbols are local, only return true if they belong to the same
> - * ELF file symbol.
> - *
> - * - If both symbols are global, always return true, as globals don't have
> - * file associations.
> - *
> - * - If they have different scopes, also return true, as the patch might have
> - * changed the symbol's scope.
> - *
> - * Works for both same-ELF (direct pointer compare) and cross-ELF
> - * (compare via file->twin) cases.
> - */
> -static bool maybe_same_file(struct symbol *sym1, struct symbol *sym2)
> +static const char *skip_lto_priv(const char *name)
> {
> - if (!sym1->file || !sym2->file)
> - return true;
> - if (sym1->file == sym2->file)
> - return true;
> - return sym1->file->twin == sym2->file;
> + if (!strstarts(name, ".lto_priv.") || !isdigit((unsigned char)name[10]))
> + return name;
> + name += 10;
> + while (isdigit((unsigned char)*name))
> + name++;
> + return name;
> }
>
> -/*
> - * Similar to maybe_same_file(), but strict: no scope changes allowed.
> - *
> - * Works for both same-ELF (direct pointer compare) and cross-ELF
> - * (compare via file->twin) cases.
> - */
> -static bool same_file(struct symbol *sym1, struct symbol *sym2)
> +static bool is_numeric_suffix(const char *name)
> {
> - if (llvm_suffix(sym1->name) && llvm_suffix(sym2->name))
> - return true;
> - if (!sym1->file && !sym2->file)
> - return true;
> - if (!sym1->file || !sym2->file)
> + if (*name++ != '.' || !isdigit((unsigned char)*name))
> return false;
> - if (sym1->file == sym2->file)
> + while (isdigit((unsigned char)*name))
> + name++;
> + return !*name;
> +}
> +
> +/* Allow compiler counters and GCC partition renames to change. */
> +static bool same_compiler_name(const char *a, const char *b)
> +{
> + for (;;) {
> + const char *next_a = skip_lto_priv(a), *next_b = skip_lto_priv(b);
> +
> + if (next_a != a || next_b != b) {
> + a = next_a;
> + b = next_b;
> + continue;
> + }
> + if (*a != *b)
> + break;
> + if (!*a)
> + return true;
> + if (*a == '.' && isdigit((unsigned char)a[1])) {
> + if (!isdigit((unsigned char)b[1]))
> + return false;
> + for (a++; isdigit((unsigned char)*a); a++)
> + ;
> + for (b++; isdigit((unsigned char)*b); b++)
> + ;
> + } else {
> + a++;
> + b++;
> + }
> + }
> +
> + /* A trailing counter may be absent on one side. */
> + return (!*a && is_numeric_suffix(b)) || (!*b && is_numeric_suffix(a));
> +}
[Severity: Medium]
Can skip_lto_priv() in same_compiler_name() ever match when .lto_priv.<N> is
added or removed on one side, or when an internal counter changes before
another suffix?
In tools/objtool/elf.c, demangled_name_len() only strips .llvm.<hash> and
trailing .<digits> suffixes, stopping at .lto_priv because 'v' is not a
digit:
tools/objtool/elf.c:demangled_name_len() {
...
while (p > name) {
while (p > name && isdigit((unsigned char)p[-1]))
p--;
if (p == name || p[-1] != '.')
break;
p--;
}
...
}
So demangle_name() produces "foo" for "foo", "foo.lto_priv" for
"foo.lto_priv.0", "foo.lto_priv.0.part" for "foo.lto_priv.0.part.0", and
"var.0.lto_priv" for "var.0.lto_priv.0".
Because find_twin() and find_twin_by_identity_and_checksum() look up
candidates via identity_hash(&sym1->identity) (which hashes
identity.demangled_name) and is_twin_candidate() checks
same_identity(&sym1->identity, &sym2->identity) before calling
compatible_symbol_name(), any pair where .lto_priv.<N> is present on only
one side or where an internal .lto_priv.<N> or .<N> changes before another
suffix will have different demangled_name strings and be rejected before
same_compiler_name() is reached.
> +
> +/* Keep clone tags such as .cold, .isra and .part distinct. */
> +static bool compatible_symbol_name(const char *a, const char *b)
> +{
> + if (same_llvm_name(a, b))
> return true;
> - return sym1->file->twin == sym2->file;
> + if (strchr(a, '@') || strchr(b, '@') || strstr(a, ".str1."))
> + return !strcmp(a, b);
> + return same_compiler_name(a, b);
> }
[Severity: Medium]
What happens in compatible_symbol_name() when a symbol has both a
.llvm.<digits> suffix (or .lto_priv.<digits> suffix) and a compiler counter
suffix that was added or changed?
First, same_llvm_name(a, b) strips .llvm.<digits> via llvm_name_len(), but
requires the remaining prefixes to be byte-for-byte identical. When a
counter suffix was added or changed (for example, target.done.llvm.111 vs
target.done.1.llvm.222, or target.done vs target.done.1.llvm.222 when
ThinLTO promotion changes), same_llvm_name() returns false and
compatible_symbol_name() falls through to same_compiler_name(a, b) with the
unstripped strings.
Because same_compiler_name() does not strip or skip .llvm.<digits>, it
compares ".llvm.111" against ".1.llvm.222" (or "" against ".1.llvm.222")
and returns false. Similarly, for a pair like foo.lto_priv.0 vs
foo.0.lto_priv.0, skip_lto_priv() consumes .lto_priv.0 on the first string,
breaks out of the loop at the '.' of .0.lto_priv.0, and calls
is_numeric_suffix(".0.lto_priv.0"), which returns false because
is_numeric_suffix() does not skip .lto_priv.<digits>.
Second, same_llvm_name() has the comment "Keep object-fragment numbers, e.g.
amd_northbridges.0.", but when same_llvm_name() returns false for two
different fragment numbers (such as amd_northbridges.0 vs
amd_northbridges.1), compatible_symbol_name() falls through to
same_compiler_name(a, b), which treats .0 and .1 as matching compiler
counters unless one side has a .llvm.<digits> suffix.
Should .llvm.<digits> be stripped before comparing prefixes in
same_compiler_name(), or how are object-fragment numbers meant to be
distinguished from compiler counters?
>
> -/*
> - * Is it a local symbol, or at least was it local in the translation unit
> - * before LLVM promoted it?
> - */
> -static bool is_tu_local_sym(struct symbol *sym)
> +static bool is_twin_candidate(struct symbol *sym1, struct symbol *sym2)
> {
> - return is_local_sym(sym) || is_llvm_sym(sym);
> + if (sym1->type != sym2->type)
> + return false;
> + /* Relocations can also reach targets without DWARF identities. */
> + if (has_dwarf_identity(sym1) && has_dwarf_identity(sym2) &&
> + !same_identity(&sym1->identity, &sym2->identity))
> + return false;
> + /* Don't match zero-length labels with sized symbols. */
> + if (!is_undef_sym(sym1) && !is_undef_sym(sym2) &&
> + (!sym1->len != !sym2->len))
> + return false;
> + /* __UNIQUE_ID counters shift when an earlier macro is added. */
> + if (is_object_sym(sym1) &&
> + (strstarts(sym1->name, "__UNIQUE_ID_") || strstr(sym1->name, ".__UNIQUE_ID_")) &&
> + (strstarts(sym2->name, "__UNIQUE_ID_") || strstr(sym2->name, ".__UNIQUE_ID_")))
> + return !strcmp(sym1->identity.demangled_name, sym2->identity.demangled_name);
> + return compatible_symbol_name(sym1->name, sym2->name);
> }
[Severity: Medium]
Does matching __UNIQUE_ID_ddebug_* symbols here work when a patch adds or
removes lines earlier in the source file?
Both DEFINE_DYNAMIC_DEBUG_METADATA_CLS() in include/linux/dynamic_debug.h
(.lineno = __LINE__) and DEBUG_CALL() in
tools/objtool/tests/generic/fixtures/identity/unique_id.c embed __LINE__ in
each static descriptor in section __dyndbg, and checksum_update_object() in
tools/objtool/klp-checksum.c hashes the raw object bytes including that line
number field.
In unique_id.c, the #ifdef PATCHED and #else branches have the same number
of lines, so __LINE__ stays identical. However, if a patch inserts or
deletes lines above a function containing pr_debug():
1. When a function has a single pr_debug() descriptor,
correlate_by_identity() pairs the original and patched
__UNIQUE_ID_ddebug_* symbols by their unique DWARF identity, and
mark_changed_functions() then sees orig_sym->csum.checksum !=
patched_sym->csum.checksum (due to the changed line number) and aborts
with ERROR("changed data: __UNIQUE_ID_ddebug_...") even if the function
containing pr_debug() was not modified.
2. When a changed function has multiple pr_debug() descriptors (which share
the same scope and demangled name __UNIQUE_ID_ddebug),
correlate_by_identity() defers to extend_same_twins_by_checksum() ->
find_twin_by_identity_and_checksum(), which requires
sym1->csum.checksum == sym2->csum.checksum and fails to correlate any
descriptor whose __LINE__ shifted.
>
> -/*
> - * Try to find sym1's twin in patched using deterministic matching.
> - *
> - * Multiple symbols can share a demangled name (e.g., static functions in
> - * different TUs). This function counts same-named candidates through a
> - * funnel of progressively tighter filters. Each level is a strict subset
> - * of the previous one.
> - *
> - * The widest level that yields a 1:1 match wins. Narrower levels are only
> - * needed when the wider level is ambiguous (count > 1).
> - *
> - * Candidates are pre-filtered by maybe_same_file(), which narrows most
> - * local symbols to their own TU. For example, 19 different static
> - * type_show() functions across vmlinux.o each see only one candidate after
> - * pre-filtering, so they match immediately at Level 1.
> - *
> - * Level 1 (name): Works when the demangled name is unique after
> - * pre-filtering. Handles most symbols: unique globals like copy_signal(),
> - * or per-TU locals like pcspkr_probe().
> - *
> - * Level 2 (scope): Filters by local-vs-global (TU-local-vs-not). Example:
> - * parse_header() exists as both a static and a global function. Level 1
> - * sees both (same demangled name), but Level 2 separates them by scope.
> - *
> - * Level 3 (file): Strict file matching via same_file(), which rejects scope
> - * changes. Example: LLVM-promoted foo.llvm.12345 (global, no FILE symbol)
> - * vs genuine local foo (has FILE symbol). Both are TU-local so Level 2
> - * can't distinguish them, but same_file() rejects the pair because one has
> - * a file association and the other doesn't.
> - *
> - * Level 4 (checksum): Distinguishes by function checksum. Example:
> - * usb_devnode.llvm.AAA and usb_devnode.llvm.BBB are two LLVM-promoted
> - * functions from different TUs with the same demangled name. After a TU
> - * change, the .llvm. hashes change but the functions themselves may be
> - * unchanged. Level 4 matches each to the patched candidate with the
> - * same checksum.
> - */
> -static struct symbol *find_twin(struct elfs *e, struct symbol *sym1)
> +static struct symbol *find_twin(struct elf *elf, struct symbol *sym1)
> {
> - struct symbol *name_last = NULL, *scope_last = NULL,
> - *file_last = NULL, *csum_last = NULL;
> - unsigned int name_orig = 0, name_patched = 0;
> - unsigned int scope_orig = 0, scope_patched = 0;
> - unsigned int file_orig = 0, file_patched = 0;
> - unsigned int csum_orig = 0, csum_patched = 0;
> struct symbol *sym2, *match = NULL;
> + unsigned int candidates = 0;
> + u32 key;
>
> - /* Count orig candidates */
> - for_each_sym_by_demangled_name(e->orig, sym1->identity.demangled_name, sym2) {
> - if (sym2->twin || sym1->type != sym2->type || sym2->dont_correlate ||
> - (!maybe_same_file(sym1, sym2)))
> + if (!has_dwarf_identity(sym1))
> + return NULL;
> + key = identity_hash(&sym1->identity);
> +
> + elf_hash_for_each_possible(elf, symbol_identity, sym2, identity_hash, key) {
> + if (sym2->twin || !is_twin_candidate(sym1, sym2))
> continue;
> + match = sym2;
> + candidates++;
> + }
>
> - /* Level 1: name match (widest filter) */
> - name_orig++;
> + return candidates == 1 ? match : NULL;
> +}
>
> - /* Level 2: scope (scope changes allowed) */
> - if (is_tu_local_sym(sym1) != is_tu_local_sym(sym2))
> - continue;
> - scope_orig++;
> +/* Checksums can disambiguate only unchanged symbols. */
> +static struct symbol *find_twin_by_identity_and_checksum(struct elf *elf, struct symbol *sym1)
> +{
> + struct symbol *sym2, *match = NULL;
> + unsigned int candidates = 0;
> + u32 key;
>
> - /* Level 3: file (scope changes disallowed) */
> - if (!same_file(sym1, sym2))
> - continue;
> - file_orig++;
> + if (!has_dwarf_identity(sym1) || !sym1->csum.checksum)
> + return NULL;
> + key = identity_hash(&sym1->identity);
>
> - /* Level 4: checksum (unchanged symbols) */
> - if (sym1->len != sym2->len || !sym1->csum.checksum ||
> + elf_hash_for_each_possible(elf, symbol_identity, sym2, identity_hash, key) {
> + if (!is_twin_candidate(sym1, sym2))
> + continue;
> + if (sym2->twin || sym1->len != sym2->len ||
> sym1->csum.checksum != sym2->csum.checksum)
> continue;
> - csum_orig++;
> + match = sym2;
> + candidates++;
> }
>
> - /* Count patched candidates */
> - for_each_sym_by_demangled_name(e->patched, sym1->identity.demangled_name, sym2) {
> - if (sym2->twin || sym1->type != sym2->type || sym2->dont_correlate ||
> - !maybe_same_file(sym1, sym2))
> - continue;
> -
> - /* Level 1 */
> - name_patched++;
> - name_last = sym2;
> + return candidates == 1 ? match : NULL;
> +}
>
> - /* Level 2 */
> - if (is_tu_local_sym(sym1) != is_tu_local_sym(sym2))
> - continue;
> - scope_patched++;
> - scope_last = sym2;
> +static int correlate_by_identity(struct elfs *e)
> +{
> + struct symbol *sym, *match;
>
> - /* Level 3 */
> - if (!same_file(sym1, sym2))
> + for_each_sym(e->orig, sym) {
> + /* Compound literal names are compiler counters, not stable identities. */
> + if (sym->twin || sym->dont_correlate || is_compound_constant(sym))
> continue;
> - file_patched++;
> - file_last = sym2;
> -
> - /* Level 4 */
> - if (sym1->len != sym2->len || !sym1->csum.checksum ||
> - sym1->csum.checksum != sym2->csum.checksum)
> + match = find_twin(e->patched, sym);
> + /* Two originals can share the same sole patched candidate. */
> + if (!match || find_twin(e->orig, match) != sym)
> continue;
> - csum_patched++;
> - csum_last = sym2;
> + sym->twin = match;
> + match->twin = sym;
> + dbg_correlate("%s(): %s -> %s", __func__, sym->name, match->name);
> }
> + return 0;
> +}
>
> - /* Return the widest level that yields a unique (1:1) match */
> - if (name_orig == 1 && name_patched == 1)
> - match = name_last;
> - else if (scope_orig == 1 && scope_patched == 1)
> - match = scope_last;
> - else if (file_orig == 1 && file_patched == 1)
> - match = file_last;
> - else if (csum_orig == 1 && csum_patched == 1)
> - match = csum_last;
> -
> - if (!match)
> - return NULL;
> +static int extend_same_twins_by_checksum(struct elfs *e)
> +{
> + struct symbol *sym, *match;
>
> - if (name_orig != 1 || name_patched != 1)
> - dbg_correlate("find_twin(): %s%s -> %s%s",
> - sym1->name, is_func_sym(sym1) ? "()" : "",
> - match->name, is_func_sym(match) ? "()" : "");
> + for_each_sym(e->orig, sym) {
> + if (sym->twin || sym->dont_correlate)
> + continue;
> + match = find_twin_by_identity_and_checksum(e->patched, sym);
> + /* Two originals can share the same sole patched candidate. */
> + if (!match || find_twin_by_identity_and_checksum(e->orig, match) != sym)
> + continue;
> + sym->twin = match;
> + match->twin = sym;
> + dbg_correlate("%s(): %s -> %s", __func__, sym->name, match->name);
> + }
> + return 0;
> +}
>
> - return match;
> +/* Data aliases can reuse the representative's storage without another twin. */
> +static struct symbol *reference_twin(struct symbol *sym)
> +{
> + if (!sym->twin && is_object_sym(sym))
> + return sym->alias->twin;
> + return sym->twin;
> }
[Severity: High]
How does alias handling work across find_twin_by_identity_and_checksum(),
reference_twin(), check_linkage_changes(), and mark_changed_functions() when
non-representative alias symbols are present?
In tools/objtool/elf.c (elf_add_symbol()), when multiple symbols share the
same section, offset, type, and non-zero length, each earlier symbol has
sym->alias set to the last symbol at that offset (the representative symbol,
where sym->alias == sym). In tools/objtool/klp-checksum.c
(calculate_checksums()), symbols with is_alias_sym(sym) (sym->alias != sym)
are skipped, so read_sym_checksums() populates sym->csum.checksum only on
the representative symbol and leaves sym->csum.checksum as 0 on all other
aliases at that address.
However, correlate_by_identity() correlates symbols regardless of
is_alias_sym(sym), which appears to cause several issues:
1. In mark_changed_functions(), orig_sym->csum.checksum !=
patched_sym->csum.checksum is compared directly instead of via
orig_sym->alias->csum.checksum and patched_sym->alias->csum.checksum. If
constant merging or symbol ordering changes whether a correlated symbol
is the representative (checksum != 0) versus a non-representative alias
(checksum == 0) between orig and patched, mark_changed_functions()
compares a non-zero checksum against 0 and aborts with
ERROR("changed data: %s") on unchanged data (or falsely marks an
unchanged function as changed). Conversely, if both sides of a correlated
pair are non-representative aliases (checksum == 0 on both sides) while
the representative symbol is uncorrelated, mark_changed_functions()
compares 0 != 0 and misses actual data modifications.
2. In find_twin_by_identity_and_checksum(), sym1->csum.checksum and
sym2->csum.checksum are checked directly instead of
sym1->alias->csum.checksum and sym2->alias->csum.checksum, so
non-representative aliases always have checksum == 0 and cannot be
disambiguated by checksum.
3. In reference_twin(), sym->alias only points one-way from earlier symbols
to the representative symbol. If an earlier non-representative alias at
the same address was correlated while the representative symbol itself
was not correlated, calling reference_twin() on the representative symbol
evaluates sym->alias->twin (which is sym->twin == NULL) and fails to find
the correlated alias's twin.
4. In check_linkage_changes(), orig_sym->twin and patched_sym->twin are
checked directly rather than reference_twin(orig_sym) and
reference_twin(patched_sym).
>
> -struct llvm_suffix_pair {
> +struct symtab_entry {
> struct hlist_node hash;
> - const char *orig;
> - const char *patched;
> + const char *file, *name;
> + struct symbol *sym;
> + bool is_unique;
> +};
> +
> +struct symtab_hash {
> + DECLARE_HASHTABLE(entries, 12);
> };
>
> -static DECLARE_HASHTABLE(suffix_map, 7);
> +static struct symtab_entry *find_symtab_entry(struct symtab_hash *table, struct symbol *sym)
> +{
> + const char *file = sym->file ? sym->file->name : "";
> + const char *name = sym->identity.demangled_name;
> + u32 key = jhash_2words(str_hash(file), str_hash(name), 0);
> + struct symtab_entry *entry;
>
> -/*
> - * Build a mapping of known orig-to-patched LLVM suffixes based on
> - * already-correlated symbol pairs. All promoted symbols from the same TU
> - * share the same .llvm.<hash> suffix, so one correlated pair seeds the map
> - * for the entire TU.
> - */
> -static int update_suffix_map(struct elf *elf)
> + hash_for_each_possible(table->entries, entry, hash, key) {
> + if (!strcmp(entry->file, file) && !strcmp(entry->name, name))
> + return entry;
> + }
> + return NULL;
> +}
> +
> +static int build_symtab_hash(struct elf *elf, struct symtab_hash *table)
> {
> - struct llvm_suffix_pair *entry;
> + struct symtab_entry *entry;
> struct symbol *sym;
> + u32 key;
>
> + hash_init(table->entries);
> for_each_sym(elf, sym) {
> - const char *s1, *s2;
> - bool found;
> -
> - if (!sym->twin)
> + if (!sym->len || is_undef_sym(sym) || sym->dont_correlate ||
> + is_compound_constant(sym))
> continue;
> -
> - s1 = llvm_suffix(sym->name);
> - s2 = llvm_suffix(sym->twin->name);
> -
> - if (!s1 || !s2)
> + /* Already matched symbols still count against uniqueness. */
> + entry = find_symtab_entry(table, sym);
> + if (entry) {
> + entry->is_unique = false;
> continue;
> -
> - found = false;
> - hash_for_each_possible(suffix_map, entry, hash, str_hash(s1)) {
> - if (!strcmp(entry->orig, s1)) {
> - found = true;
> - break;
> - }
> }
> - if (found)
> - continue;
> -
> - entry = calloc(1, sizeof(*entry));
> + entry = malloc(sizeof(*entry));
> if (!entry) {
> - ERROR_GLIBC("calloc");
> + ERROR_GLIBC("malloc symtab entry");
> return -1;
> }
> -
> - entry->orig = s1;
> - entry->patched = s2;
> - hash_add(suffix_map, &entry->hash, str_hash(s1));
> + /* Assembly may inherit FILE from the preceding input. */
> + entry->file = sym->file ? sym->file->name : "";
> + entry->name = sym->identity.demangled_name;
> + entry->sym = sym;
> + entry->is_unique = true;
> + key = jhash_2words(str_hash(entry->file), str_hash(entry->name), 0);
> + hash_add(table->entries, &entry->hash, key);
> }
> -
> return 0;
> }
[Severity: Medium]
Should find_symtab_entry() and build_symtab_hash() use
sym->file->identity.source instead of sym->file->name?
In tools/objtool/klp-identity.c (prepare_identities()), STT_FILE symbols
have sym->identity.source initialized to sym->name and then updated by
complete_file_source() in tools/objtool/klp-dwarf.c with the full normalized
DWARF CU path.
By reading sym->file->name here instead of sym->file->identity.source,
sym->file->identity.source is never read after complete_file_source(), and
in multi-TU objects such as vmlinux.o where different directories share an
STT_FILE basename (for example, drivers/base/core.c and net/core/core.c),
symbols from different translation units collide in symtab_hash under the
same basename.
[ ... ]
> @@ -744,102 +731,67 @@ static struct symbol *find_twin_positional(struct elfs *e, struct symbol *sym1)
> {
> - unsigned int idx_orig = 0, idx_patched = 0;
> - unsigned int sym1_pos = 0;
> - struct symbol *sym2, *match = NULL;
> + struct symtab_hash orig_table = {}, patched_table = {};
> + struct symtab_entry *orig_entry, *patched_entry;
> + struct symbol *orig_sym, *patched_sym;
> + int ret = -1;
>
> - for_each_sym_by_demangled_name(e->orig, sym1->identity.demangled_name, sym2) {
> - if (sym2->twin || sym1->type != sym2->type || sym2->dont_correlate ||
> - !maybe_same_file(sym1, sym2))
> + if (build_symtab_hash(e->orig, &orig_table) ||
> + build_symtab_hash(e->patched, &patched_table))
> + goto out;
> +
> + for_each_sym(e->patched, patched_sym) {
> + if (!patched_sym->len || is_undef_sym(patched_sym) ||
> + patched_sym->dont_correlate || reference_twin(patched_sym))
> continue;
[Severity: Medium]
What happens to defined zero-length symbols (where sym->len == 0, such as
assembly SYM_INNER_LABEL or SYM_DATA_END_LABEL markers like __bhi_args_end,
hv_crash_asm_end, ftrace_caller_end, or empty C arrays) when they are
referenced by a changed function?
Previously, find_twin() correlated defined zero-length symbols by name,
scope, and file because only Level 4 (checksum) required sym->len > 0. With
this commit, is_dwarf_identity_candidate() in tools/objtool/klp-identity.c
requires sym->len, and both build_symtab_hash() and validate_symbol_twins()
skip !sym->len. (In the follow-up commits "objtool/klp: Extend twins through
unchanged function references" and "objtool/klp: Check unmatched data
references", extend_twins_by_relocations() only walks relocations in
unchanged functions and check_unmatched_reference() also skips
!patched_sym->len.)
If a changed function references an existing unexported or local defined
zero-length symbol that is not referenced by an unchanged function in the
object, reference_twin(patched_sym) remains NULL, klp_reloc_needed() returns
false, and clone_reloc() clones a new 0-byte symbol into the livepatch
module instead of emitting a KLP relocation to the original symbol.
> - if (is_tu_local_sym(sym1) != is_tu_local_sym(sym2) ||
> - is_llvm_sym(sym1) != is_llvm_sym(sym2))
> + patched_entry = find_symtab_entry(&patched_table, patched_sym);
> + if (!patched_entry || !patched_entry->is_unique)
> continue;
> - if (sym1 == sym2)
> - sym1_pos = idx_orig;
> - idx_orig++;
> - }
> -
> - for_each_sym_by_demangled_name(e->patched, sym1->identity.demangled_name, sym2) {
> - if (sym2->twin || sym1->type != sym2->type || sym2->dont_correlate ||
> - !maybe_same_file(sym1, sym2))
> + orig_entry = find_symtab_entry(&orig_table, patched_sym);
> + if (!orig_entry || !orig_entry->is_unique)
> continue;
> - if (is_tu_local_sym(sym1) != is_tu_local_sym(sym2) ||
> - is_llvm_sym(sym1) != is_llvm_sym(sym2))
> + orig_sym = orig_entry->sym;
> + if (reference_twin(orig_sym))
> continue;
> - if (idx_patched == sym1_pos)
> - match = sym2;
> - idx_patched++;
> - }
> -
> - if (idx_orig != idx_patched)
> - return NULL;
> -
> - dbg_correlate("find_twin_positional(): %s%s -> %s%s",
> - sym1->name, is_func_sym(sym1) ? "()" : "",
> - match->name, is_func_sym(match) ? "()" : "");
> -
> - return match;
> + if (strcmp(orig_sym->name, patched_sym->name) ||
> + orig_sym->bind != patched_sym->bind ||
> + !is_twin_candidate(orig_sym, patched_sym))
> + continue;
> + if (has_dwarf_identity(orig_sym) != has_dwarf_identity(patched_sym)) {
> + WARN("unmatched symbol %s (FILE %s): DWARF identity is missing on the %s side",
> + patched_sym->name,
> + *patched_entry->file ? patched_entry->file : "<none>",
> + has_dwarf_identity(orig_sym) ? "patched" : "original");
> + continue;
> + }
> + if (!known_symtab_fallback(orig_sym) || !known_symtab_fallback(patched_sym))
> + WARN("ELF FILE/name fallback outside known rules: %s (FILE %s, binding %u, DWARF %s), orig[%u] -> patched[%u]",
> + patched_sym->name,
> + *patched_entry->file ? patched_entry->file : "<none>",
> + patched_sym->bind, has_dwarf_identity(patched_sym) ? "yes" : "no",
> + orig_sym->idx, patched_sym->idx);
> + orig_sym->twin = patched_sym;
> + patched_sym->twin = orig_sym;
> + dbg_correlate("%s(): %s (FILE %s)", __func__,
> + patched_sym->name, patched_entry->file);
> + }
> + ret = 0;
> +out:
> + free_symtab_hash(&orig_table);
> + free_symtab_hash(&patched_table);
> + return ret;
> }
>
> /*
> @@ -850,96 +833,61 @@ static struct symbol *find_twin_positional(struct elfs *e, struct symbol *sym1)
> */
> static int correlate_symbols(struct elfs *e)
> {
> - struct symbol *file1_sym, *file2_sym;
> - struct symbol *sym1, *sym2;
> - bool progress;
> -
> - for_each_sym(e->orig, sym1)
> - sym1->dont_correlate = dont_correlate(sym1);
> - for_each_sym(e->patched, sym2)
> - sym2->dont_correlate = dont_correlate(sym2);
> -
> - /* Correlate FILE symbols */
> - file1_sym = first_file_symbol(e->orig);
> - file2_sym = first_file_symbol(e->patched);
> -
> - for (; ; file1_sym = next_file_symbol(e->orig, file1_sym),
> - file2_sym = next_file_symbol(e->patched, file2_sym)) {
> -
> - if (!file1_sym && file2_sym) {
> - ERROR("FILE symbol mismatch: NULL != %s", file2_sym->name);
> - return -1;
> - }
> + struct symbol *sym;
>
> - if (file1_sym && !file2_sym) {
> - ERROR("FILE symbol mismatch: %s != NULL", file1_sym->name);
> - return -1;
> - }
> + for_each_sym(e->orig, sym)
> + sym->dont_correlate = dont_correlate(sym);
> + for_each_sym(e->patched, sym)
> + sym->dont_correlate = dont_correlate(sym);
>
> - if (!file1_sym)
> - break;
> + if (prepare_identities(e->orig) || prepare_identities(e->patched))
> + return -1;
>
> - if (strcmp(file1_sym->name, file2_sym->name)) {
> - ERROR("FILE symbol mismatch: %s != %s", file1_sym->name, file2_sym->name);
> - return -1;
> - }
> + if (correlate_by_identity(e) ||
> + extend_same_twins_by_checksum(e) ||
> + validate_symbol_twins(e))
> + return -1;
>
> - file1_sym->twin = file2_sym;
> - file2_sym->twin = file1_sym;
> + for_each_sym(e->orig, sym) {
> + if (!sym->len || is_compound_constant(sym))
> + continue;
> + if (!reference_twin(sym) && !sym->dont_correlate)
> + WARN("no correlation: %s", sym->name);
> }
>
> + return 0;
> +}
>
> - /*
> - * Correlate in two phases: loop deterministic levels until no more
> - * progress, then use positional fallback for the rest. This prevents
> - * the nondeterministic positional matching from stealing symbols that
> - * have deterministic matches.
> - */
> - hash_init(suffix_map);
> - do {
> - progress = false;
> - for_each_sym(e->orig, sym1) {
> - if (sym1->twin || sym1->dont_correlate)
> - continue;
> - sym2 = find_twin(e, sym1);
> - if (!sym2)
> - continue;
> - sym1->twin = sym2;
> - sym2->twin = sym1;
> - progress = true;
> - }
> +static int check_linkage_changes(struct elfs *e)
> +{
> + struct symbol *orig_sym, *patched_sym;
>
> - if (update_suffix_map(e->orig))
> - return -1;
> + for_each_sym(e->orig, orig_sym) {
> + if (orig_sym->twin || orig_sym->dont_correlate ||
> + !has_dwarf_identity(orig_sym) || is_undef_sym(orig_sym) || !orig_sym->len)
> + continue;
>
> - for_each_sym(e->orig, sym1) {
> - if (sym1->twin || sym1->dont_correlate)
> + /*
> + * Identities distinguish linkage changes, but later name-based
> + * resolution can bind references to the wrong state. Reject them.
> + */
> + for_each_sym_by_demangled_name(e->patched, orig_sym->identity.demangled_name,
> + patched_sym) {
> + if (patched_sym->twin || patched_sym->dont_correlate ||
> + !has_dwarf_identity(patched_sym) ||
> + is_undef_sym(patched_sym) || !patched_sym->len)
> continue;
> - sym2 = find_twin_suffixed(e->patched, sym1);
> - if (!sym2)
> + if (orig_sym->type != patched_sym->type ||
> + strcmp(orig_sym->identity.scope, patched_sym->identity.scope) ||
> + orig_sym->identity.external == patched_sym->identity.external)
> continue;
> - sym1->twin = sym2;
> - sym2->twin = sym1;
> - progress = true;
> + ERROR("unsupported linkage change (%s, %s): original %s symtab[%u] (external=%d), patched %s symtab[%u] (external=%d)",
> + orig_sym->identity.scope, orig_sym->identity.demangled_name,
> + orig_sym->name, orig_sym->idx, orig_sym->identity.external,
> + patched_sym->name, patched_sym->idx, patched_sym->identity.external);
> + return -1;
> }
> - } while (progress);
[Severity: Medium]
Does check_linkage_changes() need to verify that orig_sym and patched_sym
come from the same translation unit before reporting an unsupported linkage
change?
In tools/objtool/klp-dwarf.c (get_die_identity() and
needs_identity_source()), global and weak external definitions have
identity.source set to "" and identity.scope set to "/". File-scope static
definitions also have identity.scope set to "/" (with
identity.external == false).
In multi-TU objects such as vmlinux.o or multi-file modules, if one
translation unit has an uncorrelated file-scope static symbol (for example,
a static helper that was inlined or removed by the patch) and a different
translation unit has an uncorrelated global symbol with the same demangled
name and type (for example, a newly added global function), both have
identity.scope == "/" with opposite identity.external values, causing
check_linkage_changes() to abort with an unsupported linkage change error
across unrelated translation units.
> @@ -1110,7 +1058,8 @@ static int mark_changed_functions(struct elfs *e)
>
> /* Find added functions and print them */
> for_each_sym(e->patched, patched_sym) {
> - if (!is_func_sym(patched_sym) || patched_sym->dont_correlate)
> + if (!is_func_sym(patched_sym) || patched_sym->dont_correlate ||
> + is_undef_sym(patched_sym))
> continue;
>
> if (!patched_sym->twin) {
[Severity: High]
When an existing function remains uncorrelated (patched_sym->twin == NULL)
because of ambiguity or a missing DWARF identity on one side, doesn't
mark_changed_functions() misclassify it as a newly added function?
For STT_FUNC symbols, multiple compiler clones of the same function in a
translation unit (such as foo.isra.0 and foo.isra.1, or foo.part.0 and
foo.part.1) have distinct ELF symbol names (so validate_dwarf_identity()
permits them) but share the same DWARF identity and match
compatible_symbol_name(). Likewise, functions missing a DWARF identity on
one side are skipped with only a warning in validate_symbol_twins().
When such a function is modified (or when two clones have identical
checksums and are not disambiguated by an unchanged caller),
patched_sym->twin remains NULL. In mark_changed_functions(), the loop over
e->patched treats any !patched_sym->twin function as a newly added function
(setting included = 1 while leaving changed = 0), so create_klp_sections()
does not emit a klp_func_ext livepatch entry for the modified function and
the original function in the running kernel remains unpatched. (The
follow-up commit "objtool/klp: Check unmatched data references" only checks
is_object_sym(patched_sym) in check_unmatched_reference() and does not check
uncorrelated STT_FUNC symbols.)
> diff --git a/tools/objtool/tests/generic/test-thinlto-ambiguity.sh b/tools/objtool/tests/generic/test-thinlto-ambiguity.sh
> index 34f4f3a58e201..4e0e49a9fc567 100755
> --- a/tools/objtool/tests/generic/test-thinlto-ambiguity.sh
> +++ b/tools/objtool/tests/generic/test-thinlto-ambiguity.sh
> @@ -38,7 +38,7 @@ build_thinlto() # $1 output object, $2 extra flags
> {
> local t
> for t in "" -DTU_B -DTU_C; do
> - $THIN_CC -flto=thin -O2 -ffunction-sections -fdata-sections \
> + $THIN_CC -flto=thin -O2 -g -ffunction-sections -fdata-sections \
> $2 $t -c "$FIXTURES_DIR/thinlto_ambiguity.c" \
> -o "$workdir/tu$t.o" 2>/dev/null || return 1
> done
> diff --git a/tools/objtool/tests/lib.sh b/tools/objtool/tests/lib.sh
> index 8e1aa7614dc68..44ce670c4a6c3 100644
> --- a/tools/objtool/tests/lib.sh
> +++ b/tools/objtool/tests/lib.sh
[ ... ]
> @@ -150,7 +151,7 @@ EOF
> # checksum
> #
> # A test overrides it; see tools/objtool/Documentation/klp-write-tests.txt.
> -FIXTURE_CFLAGS="-O2 -ffunction-sections -fdata-sections -fno-common \
> +FIXTURE_CFLAGS="-O2 -g -ffunction-sections -fdata-sections -fno-common \
> -fno-asynchronous-unwind-tables"
>
> test_name="$(basename "$0" .sh)"
[Severity: Medium]
As noted in the commit message, test-local-to-global-flip.sh,
test-sympos.sh, and test-sympos-vmlinux.sh now fail when running the objtool
test suite.
For test-sympos.sh and test-sympos-vmlinux.sh, those tests exercise
klp_find_sympos() and .klp.symid disambiguation for duplicate local symbols
across translation units (which is still used in clone_reloc_klp() and
create_klp_sections()), and only fail because the test scripts simulate two
translation units by compiling the same fixture .c file twice with -g,
giving both objects the same DWARF CU path. Similarly,
test-thinlto-ambiguity.sh compiles thinlto_ambiguity.c three times with -g,
giving both promoted helper.llvm.<hash> functions the same DWARF CU path
instead of distinct per-TU paths, so at this commit the changed helper fails
correlation and is emitted as a new function with no klp_func_ext entry.
Should the multi-TU test scripts compile distinct source paths (or override
-fdebug-prefix-map) so sympos and ThinLTO multi-TU disambiguation remain
tested, and should test-local-to-global-flip.sh be updated to test the new
linkage-change rejection so the test suite passes cleanly?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261104133158.3187525-1-luolongjuna@gmail.com?part=7
next prev parent reply other threads:[~2026-11-04 14:09 UTC|newest]
Thread overview: 19+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-04 13:31 [RFC PATCH v2 00/11] objtool/klp: DWARF-based cross-build symbol correlation Longjun Luo
2026-11-04 13:31 ` [RFC PATCH v2 01/11] objtool/klp: Fix checksums and copying of wide strings Longjun Luo
2026-11-04 14:03 ` sashiko-bot
2026-11-04 13:31 ` [RFC PATCH v2 02/11] objtool/klp: Hash compound literals by content Longjun Luo
2026-11-04 13:55 ` sashiko-bot
2026-11-04 13:31 ` [RFC PATCH v2 03/11] objtool/klp: Preserve identifier digits when stripping suffixes Longjun Luo
2026-11-04 13:31 ` [RFC PATCH v2 04/11] objtool/klp: Read symbol identities from DWARF Longjun Luo
2026-11-04 13:51 ` sashiko-bot
2026-11-04 13:31 ` [RFC PATCH v2 05/11] objtool/klp: Support reduced and split DWARF Longjun Luo
2026-11-04 13:31 ` [RFC PATCH v2 06/11] klp-build: Require CONFIG_DEBUG_INFO Longjun Luo
2026-11-04 13:31 ` [RFC PATCH v2 07/11] objtool/klp: Correlate symbols using DWARF identities Longjun Luo
2026-11-04 14:09 ` sashiko-bot [this message]
2026-11-04 13:31 ` [RFC PATCH v2 08/11] objtool/klp: Include symbol identities in reference checksums Longjun Luo
2026-11-04 14:06 ` sashiko-bot
2026-11-04 13:31 ` [RFC PATCH v2 09/11] objtool/klp: Extend twins through unchanged function references Longjun Luo
2026-11-04 14:05 ` sashiko-bot
2026-11-04 13:31 ` [RFC PATCH v2 10/11] objtool/klp: Check unmatched data references Longjun Luo
2026-11-04 13:31 ` [RFC PATCH v2 11/11] objtool/klp: Test symbol identity and state preservation Longjun Luo
2026-11-04 13:50 ` sashiko-bot
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=sashiko-outbox-197397@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=joe.lawrence@redhat.com \
--cc=jpoimboe@kernel.org \
--cc=justinstitt@google.com \
--cc=live-patching@vger.kernel.org \
--cc=luolongjuna@gmail.com \
--cc=mbenes@suse.cz \
--cc=morbo@google.com \
--cc=nathan@kernel.org \
--cc=ndesaulniers@google.com \
--cc=peterz@infradead.org \
--cc=pmladek@suse.com \
--cc=puranjay@kernel.org \
--cc=sashiko-reviews@lists.linux.dev \
--cc=song@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.