mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Longjun Luo <luolongjuna@gmail.com>
To: 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,
	llvm@lists.linux.dev, linux-kernel@vger.kernel.org,
	Longjun Luo <luolongjuna@gmail.com>
Subject: [RFC PATCH v2 07/11] objtool/klp: Correlate symbols using DWARF identities
Date: Wed,  4 Nov 2026 21:31:53 +0800	[thread overview]
Message-ID: <20261104133158.3187525-8-luolongjuna@gmail.com> (raw)
In-Reply-To: <20261104133158.3187525-1-luolongjuna@gmail.com>

Matching local symbols by compiler suffixes or symbol-table position
can pair unrelated state or copy existing state into a patch.

Match unique DWARF identities in both directions. Use checksums to
resolve remaining candidates within the same identity. Leave ambiguous
symbols uncorrelated instead of pairing them by position.

Compare __UNIQUE_ID data names without their counters. Use identity
and checksum uniqueness to preserve the original debug descriptors
when an earlier macro shifts their counters. Add a regression test.

Reuse data aliases through their representative twins. Retain a separate
unique FILE/name fallback with warnings for unknown cases, and exclude
link-time metadata from correlation.

Reject linkage changes because later name-based resolution can disagree
with the changed identity. Reject duplicate identities when the same
source is compiled into multiple objects; distinguishing those instances
would require another qualifier such as link order.

Validated the available objtool tests with GCC 16.2.1 and Clang
22.1.8. Only local-to-global-flip, sympos and sympos-vmlinux fail,
reflecting the linkage and duplicate-identity policies.

Assisted-by: LLM
Signed-off-by: Longjun Luo <luolongjuna@gmail.com>
---
 tools/objtool/include/objtool/klp.h           |   1 +
 tools/objtool/klp-diff.c                      | 728 ++++++++----------
 .../generic/fixtures/identity/unique_id.c     |  50 ++
 .../tests/generic/test-identity-unique-id.sh  |  31 +
 .../tests/generic/test-thinlto-ambiguity.sh   |   2 +-
 .../tests/generic/test-thinlto-local.sh       |   4 +-
 tools/objtool/tests/lib.sh                    |   5 +-
 7 files changed, 429 insertions(+), 392 deletions(-)
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/unique_id.c
 create mode 100755 tools/objtool/tests/generic/test-identity-unique-id.sh

diff --git a/tools/objtool/include/objtool/klp.h b/tools/objtool/include/objtool/klp.h
index 90468e141124..fa794bab116f 100644
--- a/tools/objtool/include/objtool/klp.h
+++ b/tools/objtool/include/objtool/klp.h
@@ -51,6 +51,7 @@ struct objtool_file;
 struct elf;
 struct symbol;
 
+bool dont_correlate(struct symbol *sym);
 bool is_compound_constant(struct symbol *sym);
 
 int klp_create_symid_sections(struct objtool_file *file);
diff --git a/tools/objtool/klp-diff.c b/tools/objtool/klp-diff.c
index cd5e683d6468..468b8654b687 100644
--- a/tools/objtool/klp-diff.c
+++ b/tools/objtool/klp-diff.c
@@ -3,14 +3,15 @@
 #include <subcmd/parse-options.h>
 #include <stdlib.h>
 #include <string.h>
-#include <libgen.h>
 #include <stdio.h>
 #include <ctype.h>
+#include <fnmatch.h>
 
 #include <objtool/objtool.h>
 #include <objtool/warn.h>
 #include <objtool/arch.h>
 #include <objtool/klp.h>
+#include <objtool/klp-identity.h>
 #include <objtool/util.h>
 #include <objtool/special.h>
 
@@ -19,7 +20,7 @@
 #include <linux/livepatch_external.h>
 #include <linux/stringify.h>
 #include <linux/string.h>
-#include <linux/jhash.h>
+#include <linux/hashtable.h>
 
 #define sizeof_field(TYPE, MEMBER) sizeof((((TYPE *)0)->MEMBER))
 
@@ -260,28 +261,6 @@ static int read_sym_checksums(struct elf *elf)
 	return 0;
 }
 
-static struct symbol *first_file_symbol(struct elf *elf)
-{
-	struct symbol *sym;
-
-	for_each_sym(elf, sym) {
-		if (is_file_sym(sym))
-			return sym;
-	}
-
-	return NULL;
-}
-
-static struct symbol *next_file_symbol(struct elf *elf, struct symbol *sym)
-{
-	for_each_sym_continue(elf, sym) {
-		if (is_file_sym(sym))
-			return sym;
-	}
-
-	return NULL;
-}
-
 /*
  * Certain static local variables should never be correlated.  They will be
  * used in place rather than referencing the originals.
@@ -469,11 +448,19 @@ bool is_compound_constant(struct symbol *sym)
 	return !*name;
 }
 
+/* __tracepoint_check records build-time checks and is discarded by the linker. */
+static bool is_metadata_section(struct section *sec)
+{
+	return !strcmp(sec->name, ".modinfo") ||
+	       !strcmp(sec->name, ".no_trim_symbol") ||
+	       !strcmp(sec->name, "__tracepoint_check");
+}
+
 /*
  * These symbols should never be correlated, so their local patched versions
  * are used instead of linking to the originals.
  */
-static bool dont_correlate(struct symbol *sym)
+bool dont_correlate(struct symbol *sym)
 {
 	return is_file_sym(sym) ||
 	       is_null_sym(sym) ||
@@ -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));
+}
+
+/* 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);
 }
 
-/*
- * 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);
 }
 
-/*
- * 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;
 }
 
-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;
 }
 
-/*
- * Match by translating the symbol's .llvm.<hash> suffix through the suffix
- * map to find the corresponding hash suffix for the patched object.
- *
- * Example: In the original kernel, TU drivers/base/core.c contains
- * foo.llvm.12345 and bar.llvm.12345 (same TU, same hash).  After patching,
- * they become foo.llvm.67890 and bar.llvm.67890.  If foo was already
- * correlated by find_twin() (e.g., unique by name), the suffix map records
- * .llvm.12345 -> .llvm.67890.  When processing bar.llvm.12345, this
- * function looks up .llvm.12345, gets .llvm.67890, constructs the name
- * bar.llvm.67890, and finds the match.
- */
-static struct symbol *find_twin_suffixed(struct elf *elf, struct symbol *sym1)
+static void free_symtab_hash(struct symtab_hash *table)
 {
-	const char *suffix, *patched_suffix = NULL;
-	struct symbol *sym2, *match = NULL;
-	char name[SYM_NAME_LEN];
-	struct llvm_suffix_pair *entry;
-	int count = 0;
-
-	suffix = llvm_suffix(sym1->name);
-	if (!suffix)
-		return NULL;
+	struct symtab_entry *entry;
+	struct hlist_node *tmp;
+	unsigned int bucket;
 
-	hash_for_each_possible(suffix_map, entry, hash, str_hash(suffix)) {
-		if (!strcmp(entry->orig, suffix)) {
-			patched_suffix = entry->patched;
-			break;
-		}
+	hash_for_each_safe(table->entries, bucket, tmp, entry, hash) {
+		hash_del(&entry->hash);
+		free(entry);
 	}
-	if (!patched_suffix)
-		return NULL;
+}
 
-	if (snprintf_check(name, SYM_NAME_LEN, "%s%s",
-			   sym1->identity.demangled_name, patched_suffix))
-		return NULL;
+static bool known_symtab_fallback(struct symbol *sym)
+{
+	static const struct {
+		unsigned char bind, type;
+		const char *section, *name;
+	} whitelist[] = {
+		/* Local data defined in relocate_kernel_64.S. */
+		{ STB_LOCAL,  STT_OBJECT, ".data..relocate_kernel", "*" },
+		/* Static-call and paravirt macros emit top-level assembly. */
+		{ STB_GLOBAL, STT_FUNC,   ".static_call.text", "__SCT__*" },
+		{ STB_GLOBAL, STT_FUNC,   ".text", "__raw_callee_save_*" },
+		{ STB_GLOBAL, STT_FUNC,   ".entry.text", "paravirt_ret0" },
+		/* The VERW selector is assembly data placed in entry text. */
+		{ STB_GLOBAL, STT_NOTYPE, ".entry.text", "x86_verw_sel" },
+		/* Boot page tables defined in arch/x86/kernel/head_64.S. */
+		{ STB_GLOBAL, STT_OBJECT, ".data", "level4_kernel_pgt" },
+		{ STB_GLOBAL, STT_OBJECT, ".data", "level3_kernel_pgt" },
+		{ STB_GLOBAL, STT_OBJECT, ".data", "level2_kernel_pgt" },
+		{ STB_GLOBAL, STT_OBJECT, ".data", "level2_fixmap_pgt" },
+		{ STB_GLOBAL, STT_OBJECT, ".data", "level1_fixmap_pgt" },
+		/* CPU startup state defined in head_64.S. */
+		{ STB_GLOBAL, STT_OBJECT, ".ref.data", "initial_code" },
+		{ STB_GLOBAL, STT_OBJECT, ".data", "smpboot_control" },
+	};
+	unsigned int i;
 
-	for_each_sym_by_name(elf, name, sym2) {
-		if (sym2->twin || sym1->type != sym2->type || sym2->dont_correlate)
-			continue;
-		count++;
-		match = sym2;
+	for (i = 0; i < ARRAY_SIZE(whitelist); i++) {
+		if (sym->bind == whitelist[i].bind && sym->type == whitelist[i].type &&
+		    !strcmp(sym->sec->name, whitelist[i].section) &&
+		    !fnmatch(whitelist[i].name, sym->name, 0))
+			return true;
 	}
-
-	if (count != 1)
-		return NULL;
-
-	dbg_correlate("find_suffixed_twin(): %s%s -> %s%s",
-		      sym1->name, is_func_sym(sym1) ? "()" : "",
-		      match->name, is_func_sym(match) ? "()" : "");
-
-	return match;
+	return false;
 }
 
-/*
- * Last-resort positional matching.
- *
- * Finds a symbol with the same position in the symbol table among
- * same-demangled-name candidates, similar to livepatch sympos.  Note that
- * LLVM-promoted symbols are globals, which come after locals in the symbol
- * table, so we have to be careful not to compare different scopes.
- *
- * Example: arch/x86/events/intel/core.c defines many __quirk variables via
- * X86_MATCH_*() macros.  In the symbol table they appear as __quirk.90,
- * __quirk.97, __quirk.101, etc., all with demangled name __quirk, same
- * scope, and same FILE symbol.  No deterministic filter can distinguish
- * them, so they're matched by position: the 1st __quirk in orig matches the
- * 1st in patched, the 2nd matches the 2nd, etc.
- *
- * This is less deterministic than the other strategies, so it's done last.
- */
-static struct symbol *find_twin_positional(struct elfs *e, struct symbol *sym1)
+/* Audit unmatched symbols and pair unique FILE/name candidates. */
+static int validate_symbol_twins(struct elfs *e)
 {
-	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;
-		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);
-
-	for_each_sym(e->orig, sym1) {
-		if (sym1->twin || sym1->dont_correlate)
-			continue;
-		sym2 = find_twin_positional(e, sym1);
-		if (!sym2)
-			continue;
-		sym1->twin = sym2;
-		sym2->twin = sym1;
 	}
-
-	for_each_sym(e->orig, sym1) {
-		if (sym1->twin || sym1->dont_correlate)
-			continue;
-		WARN("no correlation: %s", sym1->name);
-	}
-
 	return 0;
 }
 
@@ -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) {
@@ -1231,7 +1180,7 @@ static bool klp_reloc_needed(struct reloc *patched_reloc)
 		return export->mod_ns;
 	}
 
-	if (!patched_sym->twin) {
+	if (!reference_twin(patched_sym)) {
 		/*
 		 * Presumably the symbol and its reference were added by the
 		 * patch.  The symbol could be defined in this .o or in another
@@ -1376,6 +1325,7 @@ static int clone_reloc_klp(struct elfs *e, struct reloc *patched_reloc,
 {
 	const char *sym_modname, *sym_orig_name, *sec_objname;
 	struct symbol *patched_sym = patched_reloc->sym;
+	struct symbol *orig_sym = reference_twin(patched_sym);
 	s64 addend = reloc_addend(patched_reloc);
 	char tombstone_name[SYM_NAME_LEN];
 	struct symbol *sym, *klp_sym;
@@ -1386,7 +1336,7 @@ static int clone_reloc_klp(struct elfs *e, struct reloc *patched_reloc,
 	struct klp_reloc klp_reloc;
 	unsigned long sympos;
 
-	if (!patched_sym->twin) {
+	if (!orig_sym) {
 		if (!export) {
 			ERROR("unexpected klp reloc for new symbol %s", patched_sym->name);
 			return -1;
@@ -1443,8 +1393,8 @@ static int clone_reloc_klp(struct elfs *e, struct reloc *patched_reloc,
 		if (!sym_modname)
 			return -1;
 
-		sym_orig_name = patched_sym->twin->name;
-		sympos = klp_find_sympos(e->orig, patched_sym->twin);
+		sym_orig_name = orig_sym->name;
+		sympos = klp_find_sympos(e->orig, orig_sym);
 		if (sympos == ULONG_MAX)
 			return -1;
 	}
@@ -1546,6 +1496,7 @@ static int clone_reloc(struct elfs *e, struct reloc *patched_reloc,
 	struct symbol *out_sym;
 	bool klp;
 
+	/* Reject ambiguous data references before treating them as new. */
 	klp = klp_reloc_needed(patched_reloc);
 
 	dbg_clone_reloc(sec, offset, patched_sym, addend, export, klp);
@@ -2291,6 +2242,9 @@ int cmd_klp_diff(int argc, const char **argv)
 	if (correlate_symbols(&e))
 		return -1;
 
+	if (check_linkage_changes(&e))
+		return -1;
+
 	ret = mark_changed_functions(&e);
 	if (ret < 0)
 		return -1;
diff --git a/tools/objtool/tests/generic/fixtures/identity/unique_id.c b/tools/objtool/tests/generic/fixtures/identity/unique_id.c
new file mode 100644
index 000000000000..df79605bc192
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/unique_id.c
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+
+#define PASTE0(a, b) a##b
+#define PASTE(a, b) PASTE0(a, b)
+#define UNIQUE_ID(name) PASTE(__UNIQUE_ID_, PASTE(name, PASTE(_, __COUNTER__)))
+#define MAX_VALUE(a, b, id) ({ __typeof__(a) id = (a); id > (b) ? id : (b); })
+#define MAX(a, b) MAX_VALUE(a, b, UNIQUE_ID(max))
+
+struct descriptor {
+	const char *function;
+	const char *file;
+	const char *format;
+	unsigned int line;
+	unsigned int flags;
+};
+
+extern void emit(struct descriptor *desc, int value);
+
+/* pr_debug() descriptors carry a per-TU counter in their names. */
+#define DEBUG_CALL(id, fmt, value) do { \
+	static struct descriptor id __attribute__((used, aligned(8), section("__dyndbg"))) = { \
+		__func__, __FILE__, fmt, __LINE__, 0 \
+	}; \
+	if (id.flags) \
+		emit(&id, value); \
+} while (0)
+#define DEBUG(fmt, value) DEBUG_CALL(UNIQUE_ID(ddebug), fmt, value)
+
+NOIPA int clamp_value(int value)
+{
+#ifdef PATCHED
+	return MAX(value, 1);
+#else
+	return value > 0 ? value : 0;
+#endif
+}
+
+NOIPA void report(int value)
+{
+#if defined(PATCHED) && defined(CHANGE_REPORT)
+	value++;
+#endif
+	DEBUG("first", value);
+#ifdef TWO
+	DEBUG("second", value + 1);
+#endif
+}
diff --git a/tools/objtool/tests/generic/test-identity-unique-id.sh b/tools/objtool/tests/generic/test-identity-unique-id.sh
new file mode 100755
index 000000000000..d4dbaafbe575
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-unique-id.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# Adding a counter-using macro must preserve existing dynamic debug descriptors.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+for flags in "" "-DTWO" "-DTWO -DCHANGE_REPORT"; do
+	build_pair identity/unique_id.c $flags
+	if [[ "$flags" != *CHANGE_REPORT* ]]; then
+		assert_checksum_matches report
+	fi
+	run_diff
+	assert_patched clamp_value
+	assert_no_section __dyndbg
+	diff_log | grep -q 'no correlation:' && fail "dynamic debug descriptor lost correlation"
+	if [[ "$flags" == *CHANGE_REPORT* ]]; then
+		assert_patched report
+		old="$(in_symbols orig.o |
+			awk '$4 == "OBJECT" && $8 ~ /__UNIQUE_ID_ddebug_/ { print $8 }')"
+		[ "$(echo "$old" | wc -w)" = 2 ] || fail "expected two original descriptors"
+		run_post_link
+		for sym in $old; do
+			assert_original_reference report "$sym"
+		done
+	else
+		assert_not_patched report
+	fi
+done
+
+pass "counter shifts preserve descriptors in unchanged and changed functions"
diff --git a/tools/objtool/tests/generic/test-thinlto-ambiguity.sh b/tools/objtool/tests/generic/test-thinlto-ambiguity.sh
index 34f4f3a58e20..4e0e49a9fc56 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/generic/test-thinlto-local.sh b/tools/objtool/tests/generic/test-thinlto-local.sh
index a266263676ed..71156e0b59ce 100755
--- a/tools/objtool/tests/generic/test-thinlto-local.sh
+++ b/tools/objtool/tests/generic/test-thinlto-local.sh
@@ -15,9 +15,9 @@ find_thinlto_toolchain ||
 
 build_thinlto()		# $1 output object, $2 extra flags
 {
-	$THIN_CC -flto=thin -O2 -ffunction-sections -fdata-sections $2 \
+	$THIN_CC -flto=thin -O2 -g -ffunction-sections -fdata-sections $2 \
 		-c "$FIXTURES_DIR/thinlto_local.c" -o "$workdir/tu_a.o" 2>/dev/null || return 1
-	$THIN_CC -flto=thin -O2 -ffunction-sections -fdata-sections $2 -DTU_B \
+	$THIN_CC -flto=thin -O2 -g -ffunction-sections -fdata-sections $2 -DTU_B \
 		-c "$FIXTURES_DIR/thinlto_local.c" -o "$workdir/tu_b.o" 2>/dev/null || return 1
 	"$THIN_LD" -r "$workdir/tu_a.o" "$workdir/tu_b.o" -o "$1" 2>/dev/null || return 1
 }
diff --git a/tools/objtool/tests/lib.sh b/tools/objtool/tests/lib.sh
index 8e1aa7614dc6..44ce670c4a6c 100644
--- a/tools/objtool/tests/lib.sh
+++ b/tools/objtool/tests/lib.sh
@@ -67,7 +67,7 @@ klp_preflight()
 
 	tmp="$(mktemp -d)" || bail "mktemp failed"
 	echo 'int probe(void) { return 0; }' > "$tmp/probe.c"
-	$CC -c -o "$tmp/probe.o" "$tmp/probe.c" 2>/dev/null ||
+	$CC -g -c -o "$tmp/probe.o" "$tmp/probe.c" 2>/dev/null ||
 		{ rm -rf "$tmp"; bail "$CC cannot compile a trivial object"; }
 
 	# $CC, $ARCH and objtool have to agree about the target, and cross runs
@@ -139,6 +139,7 @@ EOF
 # it lives at the compile where an override cannot drop it.
 #
 #   -O2					the kernel's default
+#   -g					required for DWARF symbol identity
 #   -ffunction-sections -fdata-sections	klp-build passes these itself, through
 #					KCFLAGS, whatever the configuration
 #   -fno-asynchronous-unwind-tables	arch/x86/Makefile sets this always, so
@@ -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)"
-- 
2.55.0


  parent reply	other threads:[~2026-11-04 13:32 UTC|newest]

Thread overview: 12+ 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 13:31 ` [RFC PATCH v2 02/11] objtool/klp: Hash compound literals by content Longjun Luo
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: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 ` Longjun Luo [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 13:31 ` [RFC PATCH v2 09/11] objtool/klp: Extend twins through unchanged function references Longjun Luo
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

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=20261104133158.3187525-8-luolongjuna@gmail.com \
    --to=luolongjuna@gmail.com \
    --cc=joe.lawrence@redhat.com \
    --cc=jpoimboe@kernel.org \
    --cc=justinstitt@google.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=live-patching@vger.kernel.org \
    --cc=llvm@lists.linux.dev \
    --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=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

all inboxes | Powered by JetHome®