Live Patching
 help / color / mirror / Atom feed
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

  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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox