mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [RFC PATCH v2 00/11] objtool/klp: DWARF-based cross-build symbol correlation
@ 2026-11-04 13:31 Longjun Luo
  2026-11-04 13:31 ` [RFC PATCH v2 01/11] objtool/klp: Fix checksums and copying of wide strings Longjun Luo
                   ` (10 more replies)
  0 siblings, 11 replies; 12+ messages in thread
From: Longjun Luo @ 2026-11-04 13:31 UTC (permalink / raw)
  To: jpoimboe, peterz
  Cc: mbenes, pmladek, joe.lawrence, song, puranjay, nathan,
	ndesaulniers, morbo, justinstitt, live-patching, llvm,
	linux-kernel, Longjun Luo

v1 [1] introduced a DWARF-based symbol identity as the representation
layer for cross-build symbol correlation. Building on that representation,
v2 shows how `klp-build`'s existing single-object checksums and reference
context are used to extend symbol-twin matching in stages, and how the
resulting twins and remaining unmatched symbols are audited and
classified.

Existing ELF-only correlation can produce incorrect patches in three
cases: connecting patched code to a different same-named symbol (incorrect
twins), duplicating existing static state as a new variable (copied static
state), and missing changes in referenced targets (missed reference
changes). Across the regression suite, the baseline fails 13 tests on
both GCC and Clang. In 6 cases per compiler, it miscorrelates symbols or
misses changes without diagnosing the incorrect result. v2 passes all
13 of those tests on each compiler.

This is a design-review RFC, not a merge-ready series. The identity
representation and twin algorithm are presented together for review;
the proposed integration path is discussed in "Open Questions and Next
Steps". v2 reports an error on ambiguous data references in patched code;
Case D describes the state-inheritance boundary and proposed annotations.

The series contains 11 patches:

- Patches 1-3: Standalone fixes independent of DWARF (wide-string
  truncation, compound-literal content checksums, and preserving trailing
  digits in identifiers), which can be prepared as a separate fix series.
- Patches 4-6: DWARF identity reader, reduced/split DWARF support, and
  the `CONFIG_DEBUG_INFO` build check, without switching the correlation
  algorithm yet.
- Patches 7-10: Correlating by identity triple and checksum (Patch 7),
  including identities in reference checksums (Patch 8), extending twins
  through references from unchanged functions (Patch 9), and checking
  unmatched data references (Patch 10).
- Patch 11: Regression test suite covering symbol identities, LTO, DWARF
  variants, and state preservation.

Responses to v1 Review (Guide)
------------------------------

Thanks to Josh for reviewing v1. v2 responds as follows:

1. `CONFIG_DEBUG_INFO` and split/reduced DWARF: as suggested, `klp-build`
   now requires `CONFIG_DEBUG_INFO` and uses Kbuild's debug flags
   (Patch 6). Reduced and split DWARF are supported (Patch 5). See
   "Compatibility Limits and Build Requirements".

2. Repeated macro statics and the one-line test: building on the
   referencing-function idea, unchanged-function references resolve
   repeated statics (Patch 9). The tree-wide test reduces v1's
   `no correlation` warnings from 682/764 to zero (GCC/Clang). See
   "Evaluation and Performance Overhead".

3. Upgrading `no correlation` to an error and explicit annotations:
   following this direction, v2 rejects ambiguous data references and
   allows verified new data (Patch 10). See Case D for the
   state-inheritance boundary, its kernel-wide impact, and proposed
   annotations.

4. Referencing functions, ThinLTO naming flags, and Full LTO: in ELF, the
   referencing function only approximates the defining scope, and
   `STT_FILE` and `.llvm.<hash>` suffixes only approximate the defining
   TU. Definition identity distinguishes inlined helpers' state and
   survives LTO renaming and `STT_FILE` merging; see Cases A-B.

5. Toolchain reliability and performance: v2 cross-checks DWARF against
   ELF and measures checksum/diff costs, including a separate memory
   prototype. See "Design Overview", "Evaluation and Performance
   Overhead", and "Open Questions and Next Steps".

Design Overview: Staged Matching and Verification of Symbol Twins
-----------------------------------------------------------------

Background and Motivation
~~~~~~~~~~~~~~~~~~~~~~~~~

This design grew out of shared challenges in kernel and userspace
livepatching. In production kernel livepatching, patches are commonly
managed cumulatively to avoid hook conflicts across patches; over a
maintenance cycle, a single cumulative patch can accumulate changes from
dozens of fixes. Under LTO, these stacked edits can shift global inlining,
cross-TU promotion, and function-cloning decisions, perturbing the symbol
table far beyond the edited functions and breaking heuristic correlation.
Meanwhile, complex C++ scopes and optimizations in userspace livepatching
similarly require a strict separation between definition ownership and the
actual referencer (v1 summarized my evaluation across 17 security-fix
cases in 15 C/C++ projects and 6 distributions [1]).

This motivated a correlation mechanism grounded in definition ownership
and reference context. In adapting it to the kernel, v2 directly reuses
`klp-build`'s existing single-object checksum pipeline to summarize
context, incorporating referenced target identities and compound-literal
contents into the digest.

Supplementing and Cross-Checking ELF with DWARF
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This design takes ELF as the ground truth for emitted code and storage,
using DWARF to supplement and cross-check ELF information: ELF provides
the emitted symbols, sizes, bindings, and relocations, while DWARF
supplies the defining TU (`source`) and lexical scope (`scope`). Starting
from each ELF symbol, the reader looks up the DWARF definition and
verifies agreement on symbol names, storage coverage, and `STT_FILE`
versus CU paths; conflicts are reported, while missing information is
routed to controlled fallback and reference checks. These cross-checks
helped identify both upstream toolchain DWARF issues [2-6] and existing
ELF/checksum corner cases (Patches 1-3 and the `FIXME`s in
Patches 1 and 4).

The reader explicitly handles known DWARF issues affecting kernel
builds, so those cases require no compiler changes or backports.

Because the `(source, scope, name)` triple follows directly from standard
DWARF semantics, it also provides a clean target for upstream compiler
DWARF tests. Correlation can then rely on the triple and stable
optimization representations (such as address pieces and shrunk storage)
rather than compiler-specific suffix conventions. This can make livepatch
generation substantially less sensitive to changes in compiler naming and
optimization decisions.

Symbol Classification by DWARF Attribution
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Ideally, the `(source, scope, name)` triple alone would uniquely pair
symbols across builds in both directions; in practice, the distinguishing
power provided by DWARF varies. Before describing the twin algorithm,
admitted symbols fall into three categories:

- Complete identity: Named functions and static variables whose triple is
  unique within their minimal `(source, scope)`. The vast majority of
  symbols fall into this category and match directly, serving as the
  trustworthy foundation for subsequent context-based propagation.
- Non-unique identity: Repeated `static`s within the same named scope, or
  multiple storage pieces split from a single definition by optimization,
  can share a triple and require checksum and reference context to
  disambiguate. Anonymous-block representation choices and
  state-inheritance boundaries are covered in Case D.
- Absent identity: Symbols with no definition attribution in DWARF,
  primarily C compound literals with static storage duration
  (`.compoundliteral.N` / `__compound_literal.N`) and a small number of
  top-level assembly or special-mechanism symbols.

Four-Stage Twin Construction and Unmatched Reference Checks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Based on this classification, symbol twins are built and verified in four
successive stages (Stages 1-4):

- Stage 1 (Unique identity match): Within the same identity bucket, pair
  the unique compatible candidate in both directions.

- Stage 2 (Intra-identity checksum disambiguation): Among remaining
  candidates in the same identity bucket, use symbol length and checksum
  to pair the unique unchanged candidate in both directions.

- Stage 3 (Reference extension and verification via unchanged functions):
  For paired functions with equal checksums, compare relocation layouts in
  order and extend twins to compatible referenced targets. Existing twins
  must agree with these references; any conflict aborts patch generation.
  This cross-checks Stages 1 and 2, catching switches between instances of
  the same identity that leave the function checksum unchanged.

- Stage 4 (Controlled ELF `FILE`/name fallback): For remaining unpaired
  symbols, pair only when `FILE`/name is unique in both directions and raw
  symbol names, bindings, and DWARF availability agree on both sides;
  fallbacks outside the known whitelist emit a warning.

Data aliases reuse their representative symbol's twin. After the four
stages complete and changed functions are identified, the tool checks
unmatched data references when constructing the patch (Patch 10) to
separate verified new data from ambiguous references that must be
rejected. The acceptance rules for ordinary static data and compound
objects are detailed in Case D.

Problem Classes and Handling Boundaries
---------------------------------------

Cases A-C illustrate incorrect twins, existing state treated as new, and
missed reference changes. Case D discusses the handling boundary for state
inheritance and why references beyond current disambiguation evidence must
be rejected.

Case A. Incorrect twins: same name, different owner
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Case 2 in v1 [1] showed a real kernel example (Clang/LLD 23.1.0, ThinLTO):
a patch adds `modules_seq_ops()` and uses it in `read_file_mod_stats()`.
Cross-TU import decisions shift, moving the unsuffixed names of
`m_start`/`m_next`/`m_stop`/`m_show` between `fs/namespace.c` and
`kernel/module/procfs.c`. The baseline silently pairs the two iterator
sets against each other and reports 8 extra changed functions (4 in
untouched `fs/namespace.c`), producing a module that fails to load on a
duplicate `m_start,1` registration; v2 reports only
`read_file_mod_stats()` as changed and `modules_seq_ops()` as new.

v2 adds a test covering simultaneous edits in two TUs (Clang/LLD
23.0.0git, ThinLTO). `a.c` and `d.c` each define
`static volatile int state[4]`, and `target()` in `b.c` calls both
accessors; the patch adds a bounds check and an index mask:

    /* a.c */
    int a_get(unsigned int index)
    {
+       if (index >= 4)
+           return 0;
        return state[index];
    }

    /* d.c */
    int d_get(unsigned int index)
    {
-       return state[index];
+       return state[index & 3];
    }

Both accessors should keep using their respective old arrays. However, the
bounds check stops `a_get()` from being imported into `b.c`, turning its
array from a promoted global back into a local; under the kernel's
`-always-rename-promoted-locals=false`, the plain global name `state`
previously held by `a.c` moves to `d.c`. The baseline wires patched
`a_get()` to `d.c`'s old array and the inlined `d_get()` in `target()` to
`a.c`'s old array.

To test the effect of reverting that flag directly, I compared keeping and
restoring suffixes on the same LLVM 23 toolchain, source, and
`import-instr-limit=5`. The table varies
`-always-rename-promoted-locals`; modifying only `a.c` gives the same
result as modifying both TUs:

  Flag value                Baseline result             v2 result
  ------------------------  --------------------------  --------------------
  false (kernel default)    No warning; swaps arrays    Uses each TU's array
  true                      2 warnings; new arrays      Uses each TU's array

Although reverting the flag prevents the plain-name shift, the baseline's
`suffix_map` relies on another already-paired promoted symbol in the same
TU to map old and new `.llvm.<hash>` suffixes. Here, `a.c`'s array is
local and unsuffixed again while `d.c`'s hash changes with the edit, so
the baseline still loses correlation and reallocates both states
(Clang/LLD 22.1.8 behaves the same way under its default suffix naming;
see `test-identity-thinlto-name-drift.sh`). Toggling the compiler naming
flag cannot replace defining-TU attribution, and it also shows why DWARF
cannot be consulted only on name collisions: under plain-name drift, the
miscorrelated name is already unique.

Miscorrelation is not limited to LTO. In `test-identity-static-replace.sh`
(GCC, non-LTO), a patch removes `retired()` and adds `added()`. Each
function has a local `static volatile long count`, as does `unchanged()`:

-   long retired(long value)
-   {
-       static volatile long count;
-       return count += value;
-   }
-
    long unchanged(long value)
    {
        static volatile long count;
        return count += value;
    }
+
+   long added(long value)
+   {
+       static volatile long count;
+       return count += value;
+   }

GCC emits two `count.N` symbols before and after the patch. The baseline
warns about the removed function but pairs `added()`'s new counter with
`unchanged()`'s live counter by symbol-table position. In v2, the new
counter's scope `/added/` has no original candidate, so it gets new
storage.

Case B. Existing state treated as new: referencing function != defining scope
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Case 1 in v1 [1] showed JFS's same-named `noisy` counters (GCC 16.2.1,
non-LTO): adding a counter renumbers existing instances, causing the
baseline to copy old state into the patch and reviving an exhausted
5-message warning budget; v2 reuses the original storage via function
scope (`test-identity-static-scope.sh`). An inlined-initializer test in v2
further separates "who references it" from "who defines it".

In `test-identity-inline-helpers.sh`, two `always_inline` helpers each
define a block-local `static volatile int done` via `ONCE_INIT` to guard
one-time seed initialization:

    static inline __attribute__((always_inline)) long initialize_rx(long value)
    {
        ONCE_INIT(rx_seed, 1);
        return value ^ rx_seed;
    }

    static inline __attribute__((always_inline)) long initialize_tx(long value)
    {
        ONCE_INIT(tx_seed, 2);
        return value ^ tx_seed;
    }

Originally `target()` only calls RX; the patch adds TX before it, with no
other unchanged caller available to recover old state:

    long target(long value)
    {
+       value = initialize_tx(value);
        return initialize_rx(value);
    }

Only TX should get a new `done` flag while RX keeps its original flag.
Under GCC 16.2.1 non-LTO, the baseline warns and copies both flags,
allowing RX initialization to run again; v2 allocates only TX's flag and
binds RX's reference to the original storage (under Clang 22.1.8 non-LTO,
the baseline also correlates it because the symbol name encodes the
function).

After inlining, both `done` flags are referenced by outer `target()` in
ELF, so grouping by referencing function cannot distinguish them; their
DWARF defining scopes remain `/initialize_rx/` and `/initialize_tx/`,
allowing v2 to distinguish new and existing state in a modified function
without author annotations. Full LTO's merged `STT_FILE` entries are
handled by the same definition identity; see v1 [1] Case 3 and
`test-identity-full-lto.sh`.

Case C. Missed reference changes: identities in checksums and references
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Hashing only the normalized target name misses switches between same-named
targets in different TUs or scopes. v2 hashes the target's identity triple
into both function and data reference checksums; see
`test-checksum-identity.sh` and `test-checksum-identity-source.sh`.

Multiple split instances can still share one identity. In
`test-identity-pieces.sh` (Clang 22.1.8, non-LTO), the compiler splits
`static struct { long first, second; } state` into two 8-byte objects,
`state.0` and `state.1`. Unchanged `set()` and `sum()` access both fields,
while the patch switches the field read by `target()`:

    long target(void)
    {
-       return state.first;
+       return state.second;
    }

The instruction bytes are identical, and both `state.0` and `state.1` have
the same normalized name and DWARF triple, so the baseline checksum is
unchanged and `target()` is omitted from the patch. In v2, Stage 3 uses
the references from unchanged `set()` and `sum()` to establish each
piece's twin, and then detects a twin conflict when checking `target()`'s
reference. Equal function checksums alone are not sufficient; relocation
references must still agree with established twins.

Case D. Handling boundary: inheritance of static state
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

An anonymous block can narrow visibility without changing storage or use:

        int *a;
-       static int b = 0; a = &b;
+       { static int b = 0; a = &b; }
        return (*a)++;

In this example, GCC 16.2.1 and Clang 22.1.8 (non-LTO, `-O0` and `-O2`)
produce byte-identical stripped objects for both forms. A livepatch should
keep using the same state. The triple therefore records named scopes,
without anonymous-block depth or ordinals.

When multiple same-named `static`s exist within the same named scope,
their state-inheritance mapping must be determined. Denote the old
instances by A, B and the new instances by X, Y: positional pairing
chooses A->X and B->Y, whereas the author may have deleted A and added X
while keeping only B->Y. When their types and initial values are
identical, neither source definitions nor compiled positions distinguish
the two intents. Whether X should inherit A's live state or start from its
initial value requires author intent; even a human reviewer cannot make a
unique choice from the binary alone.

v2 first resolves what existing evidence can prove: when a function is
unchanged, twins are extended along its references; another unchanged
caller can also establish those twins (`test-identity-inline-state.sh`).
For remaining unpaired ordinary data, symbols with a DWARF identity and no
old candidate are treated as new, whereas references with existing
same-identity candidates report an error.

`test-identity-once-insert.sh` (GCC 16.2.1, Clang 22.1.8, non-LTO) shows
the consequence when a patch inserts a new initializer between two
existing ones:

    long target(long value)
    {
        ONCE_INIT(first, 1);
+       ONCE_INIT(extra, 3);
        ONCE_INIT(second, 2);
-       return value ^ first ^ second;
+       return value ^ first ^ second ^ extra;
    }

Each `ONCE_INIT` expansion defines a block-local `static done`. The test
requires `first` and `second` to keep their old flags and only `extra` to
get a new flag. The baseline warns and copies all three `done` flags,
allowing the old initializations to run again; v2 rejects the unresolved
reference and reports the function, offset, and old candidate:

    orig.o: error: objtool: target+0x2: unresolved data reference:
        done.2 (.../once_insert.c, /target/, done), old candidate done.0[4]

Symbol-table order itself can also change without adding or removing
`static`s. In `test-identity-branch-hint.sh` (GCC, Clang, non-LTO), merely
flipping branch-probability hints reorders the two counters in the symbol
table and causes the baseline to swap them silently. The ACPICA kernel
test patch hits the same boundary (Clang/LLD 22.1.8, ThinLTO): the two
1-byte BSS flags `_done` and `_done.2` in `acpi_ex_system_do_stall` share
the same triple and initial value; the baseline pairs them by position,
whereas v2 reports an error when the modified function references the
ambiguous instances.

Adding another `NL_SET_ERR_MSG()` call to a function that already has one
(GCC 16.2.1, non-LTO) produces two `__msg` objects with the same identity;
the baseline matches the old `__msg` by unchanged content and copies only
the new one, whereas v2 rejects the reference because an old candidate
with the same identity exists. Dedicated rules can be added for objects
known to be used only for diagnostic messages, provided object usage is
well-defined; by contrast, in `test-identity-readonly-address.sh`, the
address of a read-only descriptor is itself part of the interface.

Static compound literals lack a source definition identity and face a
similar ambiguity between new allocation and modified storage. v2
currently reuses a unique unchanged candidate within the reference context
and rejects the rest, as changed content alone cannot distinguish a new
literal from an edit to an existing one.

To quantify how often this boundary is reached in a full kernel, I kept
data and references fixed in the audited ThinLTO `vmlinux.o` build and
perturbed each main function's checksum one by one to simulate modifying
that function: out of 67,673 main functions, only 62 (0.09%) trigger an
unresolved data reference error, and only 33 (0.05%) involve writable data
sections.

For these rare cases, I plan a separate RFC for two annotations
prototyped locally, following the source-annotation model of kpatch's
`KPATCH_IGNORE_FUNCTION()` [8]. `KLP_NEW_STATIC()` would mark enclosed
state as new; `KLP_DEL_STATIC()` would omit the operation while retaining
a matching placeholder. Remaining instances would be paired by
DWARF declaration position, with their relative source order preserved.

Standalone Fixes Independent of DWARF (Patches 1-3)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Patches 1-3 fix wide-string truncation (`test-checksum-wide-string.sh`),
compound-literal contents omitted from reference checksums
(`test-identity-compound.sh`), and identifier digits lost when stripping
suffixes (`test-identity-thinlto-crc32.sh`). On a real DisplayPort PSR
table edit, the baseline misses the change, while v2 detects
`drm_dp_psr_setup_time()` as changed.

Evaluation and Performance Overhead
-----------------------------------

One-Line Patch Tests
~~~~~~~~~~~~~~~~~~~~

To test the macro-`static` issue raised in v1 review, I added a blank line
at the top of each `.c` file, first under `drivers/gpu/drm/radeon` (103
files), then tree-wide. Both tests used `fix-patch-lines`,
`CONFIG_LIVEPATCH=y`, `CONFIG_DRM_RADEON=m`, and DWARF 5. Radeon used
non-LTO; the tree-wide builds used GCC non-LTO / Clang ThinLTO and covered
`vmlinux.o` plus 16 modules. v1 and v2 ran checksum and diff on the same
build artifacts:

  Build                     v1 no-corr  v2 no-corr  v2 errors  v2 changed/new
  ------------------------  ----------  ----------  ---------  --------------
  GCC 16.2.1, tree-wide            682           0          0           0 / 0
  Clang 22.1.8, tree-wide          764           0         1*         -- / --
  GCC 16.2.1, Radeon               400           0          0           0 / 0
  Clang 22.1.8, Radeon             399           0          0           0 / 0

Radeon audit logs confirm that Stage 3 pairs all 400/399 `__print_once`
symbols, including 45 in `r100.o` and both flags in
`r100_reloc_pitch_offset()`.

* The tree-wide edit includes vDSO sources, which `klp-build` normally
  rejects. In this direct objtool test, all 16 Clang modules pass;
  `vmlinux.o` stops at `changed data: raw_data` because only the embedded
  vDSO build IDs differ. The baseline and v1 reject the same input. `--`
  marks the unfinished changed/new check; correlation completed with zero
  warnings.

Whole-Object vmlinux.o Audit and Multi-Config Self-Comparison
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Across three full `vmlinux.o` build pairs, v2 establishes twins for all
admitted functions, leaves 0 unmatched ordinary symbols, and leaves only
compound objects unused by the patched code, with 0 `no correlation` or
`FILE`/source warnings.

Both tools recompute checksums and run diff on the same `vmlinux.o` build
artifacts against `klp-build` at `945537271df1` (audit counters are
collected via a separate local patch; exit status, diagnostics, and output
bytes are identical to the uninstrumented tool). The table counts nonzero
admitted symbols on the patched side (excluding special sections and
`dont_correlate` symbols), recording newly established twins at each
stage:

  New twins / final disposition  ThinLTO  Full LTO  GCC non-LTO
  -----------------------------  -------  --------  -----------
  Identity: address match         101414    101172       104204
  Identity: name fallback              4         4         1593
  Identity: assembly CU              182       189           82
  Identity + checksum               1012      1012         1014
  Unchanged-func references         2500      2511         2402
  ELF FILE/name fallback              65        65           64
  Alias resolution                     0         0            7
  Unmatched compound objects         334       334          347
  Other unmatched symbols              0         0            0
  Total admitted symbols          105511    105287       109713

The first three rows break down Stage 1 by DWARF lookup method. In the
ThinLTO pair, reference extension pairs 2,215 functions (including 2,210
static-call trampolines) and 285 data symbols, while the ELF fallback adds
65 known-mechanism pairs. All inputs are x86-64 DWARF 5 builds: ThinLTO
`getpid` uses Clang/LLD 22.1.8 on `24a19b953e2a`; Full LTO and GCC non-LTO
`version_show` use Clang/LLD 22.1.8 and GCC 16.2.1 on `4403278e4ea2`
(object-level audit rerun).

In addition, the fixture suite covers full/reduced DWARF 4/5 and DWARF 4
`.dwp` lookup with original `.dwo` files removed; 6 kernel build inputs
(GCC non-LTO and Clang ThinLTO, each with normal, reduced, and split
DWARF 5) also pass self-comparison with 0 unresolved ordinary symbols and
0 warnings (split kernel self-comparisons read the original `.dwo` files).

Regression Test Suite Comparison
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Results on the 81-test suite for the baseline and v2 are shown below (all
skips are declared compiler restrictions; 0 runtime probe skips):

  Compiler      Tool      Pass  Fail  Skip
  ------------  --------  ----  ----  ----
  GCC 16.2.1    Baseline    58    13    10
  GCC 16.2.1    v2          68     3    10
  Clang 22.1.8  Baseline    65    13     3
  Clang 22.1.8  v2          75     3     3

Expected ambiguity rejections count as passes. All 13 baseline failures
pass under v2. In baseline diff, 6 tests per compiler miscorrelate or miss
changes without diagnosing the incorrect result.

The table keeps raw test failures: `compound` fails its function-checksum
assertion but reports `changed data` if baseline diff continues; GCC's
`weak-cold` fails because its assertion does not recognize the baseline's
`.klp.sym` reference format. The 3 v2 failures are intentional compatibility
rejections (see "Compatibility Limits and Build Requirements").

The suite can be run on the patched tree with:

    make -C tools/objtool
    CC=gcc OBJTOOL="$PWD/tools/objtool/objtool" \
        bash tools/objtool/tests/run-tests.sh
    CC=clang OBJTOOL="$PWD/tools/objtool/objtool" \
        bash tools/objtool/tests/run-tests.sh

Performance and Memory Overhead
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Performance was measured on the ThinLTO `getpid` build pair (~1.081 GiB
per `vmlinux.o`) on an Intel Core i7-11850H with 30.6 GiB RAM and libdw
0.196, using 1 warmup and the median of 3 serial alternating runs (audit
logging disabled; `klp diff` reads precomputed checksums from the
respective tool, and `klp checksum` uses `--dry-run`):

  Operation     Tool                Time (s)  Peak RSS (MiB)
  ------------  ------------------  --------  --------------
  klp diff      Baseline                3.62            3566
  klp diff      v2                     28.71            5604
  klp diff      Memory prototype       26.73            3968
  klp checksum  Baseline                4.48            3971
  klp checksum  v2                     15.15            6006
  klp checksum  Memory prototype       13.88            5149

v2 adds ~25 s to `diff` and ~11 s to single-side `checksum`, alongside the
two full kernel builds performed by `klp-build`. A separate memory
prototype reduces peak RSS by 29% for `diff` and 14% for `checksum`
without changing matching rules or DWARF checks. It preserves all 81
per-test outcomes. On 13 retained kernel cases, checksums, recorded twins,
diagnostics and output bytes are identical to v2. The prototype remains
in a separate branch.

Compatibility Limits and Build Requirements
-------------------------------------------

Two Compatibility Limits (3 Policy Test Failures)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The 3 test failures under v2 reflect deliberate policy boundaries:

- Source-level linkage flips (`local-to-global-flip`): Converting a
  TU-local definition into an external one (or vice versa) changes the
  `source` qualifier in its identity and affects subsequent name
  resolution, so this series rejects it. When a patch needs to expose a
  local helper, it can keep the `static` definition and add a thin global
  wrapper.
- Duplicate compilation instances (`sympos`, `sympos-vmlinux`): These two
  tests compile the same source file into multiple objects in one link,
  producing instances with identical definition identities and raw ELF
  names. The triple does not distinguish duplicate build instances, and
  this series avoids depending on link order so correlation remains
  unaffected by newly added source files and LTO repartitioning.

Build Requirements and Remaining Limitations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

`klp-build` requires `CONFIG_DEBUG_INFO` and reuses Kbuild's debug flags.
`objtool klp` now requires `libdw` in addition to `libxxhash`. If either
library is missing, objtool still builds without klp support.

With `CONFIG_DEBUG_INFO_SPLIT`, `klp-build` uses `llvm-dwp` by default to
save matching `.dwo` files in a `.dwp` package. The `libdw >= 0.191`
requirement applies only to DWP lookup.

The remaining toolchain and inherited implementation limitations are:

- `llvm-dwp` 22.1.8 hangs on some GCC DWARF 5 inputs containing
  `DW_FORM_implicit_const` (such as `lib/bsearch.dwo`), blocking `.dwp`
  packaging on those inputs.
- LLD Full LTO with `--plugin-opt=dwo_dir` requires
  `-mllvm -split-dwarf-cross-cu-references=true` to preserve per-TU paths
  (Kbuild does not enable this mode).
- The existing ELF reader (`elf.c`) can attach an overridden weak
  definition's local `foo.cold` to the surviving strong `foo`, causing
  both the baseline and this series to miss cold-only edits. This series
  preserves separate CU identities for both cold fragments and records the
  parent-attachment issue as a `FIXME` for a separate fix.

Open Questions and Next Steps
-----------------------------

This is a substantial change: 1,755 of the 4,010 added lines are tests,
and about 1,300 implement the DWARF reader and identity layer. Before
preparing a merge-ready series, I would like explicit maintainer guidance
on how to stage its integration. My proposed sequence is:

- Submit Patches 1-3 separately with their regression tests.
- Prepare the identity representation, DWARF reader, and ELF cross-checks
  as a foundation that can be reviewed separately from twin matching.
- Integrate the matching changes in Patches 7-10 together: reference
  extension pairs repeated statics, and the reference checks reject
  remaining ambiguous data references.
- Submit the memory optimizations separately and pursue explicit state
  annotations in a separate RFC.

This RFC retains upstream expectations for the three policy tests to
make the behavior changes visible. For the merge-ready series, I will
update their assertions to reflect the agreed policy and move Patch 11's
tests alongside the changes they cover.

I plan to add triple-based DWARF correctness tests in upstream compilers,
using known source definitions and optimization variants to check
coverage, non-conflict, and attribution accuracy (with clones, pieces,
anonymous scopes, and folding checked according to their respective
semantics) without locking in a specific DIE layout or suffix scheme. My
earlier bug reports and fixes on IPA-ICF, weak-override attribution, and
assembly CU selection [2-6] provide concrete test inputs, and the same
attribution model applies to consumers such as AutoFDO [7].

I would appreciate feedback on four questions:

1. Would maintainers prefer to integrate the identity representation and
   reader first, then evolve twin matching in a follow-up series, or keep
   them together for integration?
2. Is constraining candidates by the identity triple and extending twins
   via checksums and unchanged-function references the right foundation
   for correlation, and how should the final ELF fallback be bounded?
3. Is the handling boundary in Case D reasonable, and should explicit
   state annotations (such as `KLP_NEW_STATIC()` / `KLP_DEL_STATIC()`) be
   pursued in a separate RFC?
4. How should we structure upstream compiler tests around the identity
   triple to protect both DWARF producers and livepatch consumers?

Changes Since v1
----------------

Compared with v1, v2 adds reference-based twin extension through unchanged
functions, unmatched data reference checks, compound-literal content
checksums, reduced/split DWARF support, assembly and constrained name
lookup, data-alias reuse, and explicit ELF fallback diagnostics; it also
fixes wide-string truncation, trailing digits in identifiers, and
`__UNIQUE_ID` counter shifts.

Related Work & References
-------------------------

[1] RFC v1 (including userspace evaluation and kernel runtime cases):
    https://lore.kernel.org/all/20261026101916.3823942-1-luolongjuna@gmail.com/
[2] GCC PR126355 (address-bearing DIEs for IPA-ICF wrappers,
    fixed for GCC 17):
    https://gcc.gnu.org/bugzilla/show_bug.cgi?id=126355
[3] GCC PR126348 (abstract-origin TU attribution across LTO partitions,
    patch under review):
    https://gcc.gnu.org/bugzilla/show_bug.cgi?id=126348
[4] GDB PR34741 (overridden weak definition address resolution):
    https://sourceware.org/bugzilla/show_bug.cgi?id=34741
[5] LLDB #233545 (fix overridden weak function lookup):
    https://github.com/llvm/llvm-project/pull/233545
[6] GNU AS (CU source selection for preprocessed assembly):
    https://sourceware.org/pipermail/binutils/2026-October/152209.html
[7] AutoFDO #267 (fix absolute DWARF 5 range handling, merged):
    https://github.com/google/autofdo/pull/267
[8] kpatch source annotations (ignoring specified functions or sections):
    https://github.com/dynup/kpatch/blob/master/kmod/patch/kpatch-macros.h

Longjun Luo (11):
  objtool/klp: Fix checksums and copying of wide strings
  objtool/klp: Hash compound literals by content
  objtool/klp: Preserve identifier digits when stripping suffixes
  objtool/klp: Read symbol identities from DWARF
  objtool/klp: Support reduced and split DWARF
  klp-build: Require CONFIG_DEBUG_INFO
  objtool/klp: Correlate symbols using DWARF identities
  objtool/klp: Include symbol identities in reference checksums
  objtool/klp: Extend twins through unchanged function references
  objtool/klp: Check unmatched data references
  objtool/klp: Test symbol identity and state preservation

 scripts/livepatch/klp-build                   |   23 +-
 tools/objtool/Build                           |    4 +-
 .../objtool/Documentation/klp-test-design.txt |    6 +-
 tools/objtool/Makefile                        |   12 +-
 tools/objtool/elf.c                           |  100 +-
 tools/objtool/include/objtool/elf.h           |   43 +-
 tools/objtool/include/objtool/klp-dwarf.h     |   22 +
 tools/objtool/include/objtool/klp-identity.h  |   26 +
 tools/objtool/include/objtool/klp.h           |    3 +
 tools/objtool/klp-checksum.c                  |  155 ++-
 tools/objtool/klp-diff.c                      | 1041 ++++++++------
 tools/objtool/klp-dwarf.c                     | 1206 +++++++++++++++++
 tools/objtool/klp-identity.c                  |   94 ++
 .../tests/generic/fixtures/identity/alias.c   |   25 +
 .../generic/fixtures/identity/block_data.c    |   22 +
 .../generic/fixtures/identity/branch_hint.c   |   28 +
 .../generic/fixtures/identity/cold_strong.c   |   13 +
 .../generic/fixtures/identity/cold_user.c     |   15 +
 .../generic/fixtures/identity/cold_weak.c     |   10 +
 .../tests/generic/fixtures/identity/common.h  |   15 +
 .../generic/fixtures/identity/compound.c      |   65 +
 .../generic/fixtures/identity/consumer.c      |   30 +
 .../tests/generic/fixtures/identity/entries.c |   10 +
 .../generic/fixtures/identity/full_left.c     |   16 +
 .../generic/fixtures/identity/full_right.c    |    6 +
 .../generic/fixtures/identity/icf_left/same.c |   16 +
 .../fixtures/identity/icf_right/same.c        |   16 +
 .../fixtures/identity/inline_helpers.c        |   27 +
 .../tests/generic/fixtures/identity/lookup.c  |   13 +
 .../generic/fixtures/identity/metadata.c      |   24 +
 .../generic/fixtures/identity/move_left.c     |    9 +
 .../generic/fixtures/identity/move_right.c    |    8 +
 .../generic/fixtures/identity/new/same.c      |    6 +
 .../generic/fixtures/identity/new_driver.c    |   15 +
 .../generic/fixtures/identity/new_provider.c  |    4 +
 .../generic/fixtures/identity/old/same.c      |    6 +
 .../tests/generic/fixtures/identity/once.h    |   16 +
 .../generic/fixtures/identity/once_inline.c   |   28 +
 .../generic/fixtures/identity/once_insert.c   |   21 +
 .../tests/generic/fixtures/identity/pieces.c  |   28 +
 .../fixtures/identity/promotion_bounds_a.c    |   12 +
 .../fixtures/identity/promotion_bounds_b.c    |   12 +
 .../fixtures/identity/promotion_bounds_d.c    |   12 +
 .../fixtures/identity/promotion_crc32.c       |    8 +
 .../fixtures/identity/promotion_crc32_user.c  |   15 +
 .../generic/fixtures/identity/provider.c      |    9 +
 .../generic/fixtures/identity/readonly.c      |   30 +
 .../tests/generic/fixtures/identity/record.h  |    8 +
 .../tests/generic/fixtures/identity/rx.c      |    4 +
 .../tests/generic/fixtures/identity/shrink.c  |   19 +
 .../generic/fixtures/identity/source_driver.c |   11 +
 .../fixtures/identity/static_replace.c        |   29 +
 .../generic/fixtures/identity/static_scope.c  |   31 +
 .../fixtures/identity/static_scope_reorder.c  |   31 +
 .../tests/generic/fixtures/identity/tx.c      |    4 +
 .../generic/fixtures/identity/unique_id.c     |   50 +
 .../generic/fixtures/identity/wide_string.c   |   36 +
 .../generic/test-checksum-identity-source.sh  |   24 +
 .../tests/generic/test-checksum-identity.sh   |   32 +
 .../generic/test-checksum-wide-string.sh      |   39 +
 .../tests/generic/test-identity-alias.sh      |   26 +
 .../tests/generic/test-identity-block-data.sh |   12 +
 .../generic/test-identity-branch-hint.sh      |   18 +
 .../tests/generic/test-identity-compound.sh   |   33 +
 .../tests/generic/test-identity-dwarf.sh      |   34 +
 .../tests/generic/test-identity-full-lto.sh   |   30 +
 .../tests/generic/test-identity-icf.sh        |   23 +
 .../generic/test-identity-inline-helpers.sh   |   25 +
 .../generic/test-identity-inline-state.sh     |   21 +
 .../tests/generic/test-identity-metadata.sh   |   20 +
 .../tests/generic/test-identity-new-tu.sh     |   24 +
 .../generic/test-identity-once-insert.sh      |   19 +
 .../tests/generic/test-identity-pieces.sh     |   27 +
 .../generic/test-identity-readonly-address.sh |   23 +
 .../tests/generic/test-identity-shrink.sh     |   19 +
 .../tests/generic/test-identity-source.sh     |   29 +
 .../tests/generic/test-identity-split.sh      |   34 +
 .../generic/test-identity-static-replace.sh   |   20 +
 .../generic/test-identity-static-scope.sh     |   35 +
 .../generic/test-identity-thinlto-crc32.sh    |   23 +
 .../test-identity-thinlto-name-drift.sh       |   79 ++
 .../test-identity-thinlto-promotion.sh        |   26 +
 .../tests/generic/test-identity-unique-id.sh  |   31 +
 .../tests/generic/test-identity-weak-cold.sh  |   35 +
 .../tests/generic/test-thinlto-ambiguity.sh   |    2 +-
 .../tests/generic/test-thinlto-local.sh       |    4 +-
 tools/objtool/tests/identity-lib.sh           |   65 +
 tools/objtool/tests/lib.sh                    |    7 +-
 .../tests/x86/fixtures/identity_assembly.S    |   33 +
 .../tests/x86/fixtures/warn_reference.c       |   36 +
 .../tests/x86/test-identity-assembly.sh       |   19 +
 .../objtool/tests/x86/test-warn-reference.sh  |   21 +
 92 files changed, 4010 insertions(+), 486 deletions(-)
 create mode 100644 tools/objtool/include/objtool/klp-dwarf.h
 create mode 100644 tools/objtool/include/objtool/klp-identity.h
 create mode 100644 tools/objtool/klp-dwarf.c
 create mode 100644 tools/objtool/klp-identity.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/alias.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/block_data.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/branch_hint.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/cold_strong.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/cold_user.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/cold_weak.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/common.h
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/compound.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/consumer.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/entries.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/full_left.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/full_right.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/icf_left/same.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/icf_right/same.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/inline_helpers.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/lookup.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/metadata.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/move_left.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/move_right.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/new/same.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/new_driver.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/new_provider.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/old/same.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/once.h
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/once_inline.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/once_insert.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/pieces.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/promotion_bounds_a.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/promotion_bounds_b.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/promotion_bounds_d.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/promotion_crc32.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/promotion_crc32_user.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/provider.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/readonly.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/record.h
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/rx.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/shrink.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/source_driver.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/static_replace.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/static_scope.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/static_scope_reorder.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/tx.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/unique_id.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/wide_string.c
 create mode 100755 tools/objtool/tests/generic/test-checksum-identity-source.sh
 create mode 100755 tools/objtool/tests/generic/test-checksum-identity.sh
 create mode 100755 tools/objtool/tests/generic/test-checksum-wide-string.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-alias.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-block-data.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-branch-hint.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-compound.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-dwarf.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-full-lto.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-icf.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-inline-helpers.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-inline-state.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-metadata.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-new-tu.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-once-insert.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-pieces.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-readonly-address.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-shrink.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-source.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-split.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-static-replace.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-static-scope.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-thinlto-crc32.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-thinlto-name-drift.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-thinlto-promotion.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-unique-id.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-weak-cold.sh
 create mode 100644 tools/objtool/tests/identity-lib.sh
 create mode 100644 tools/objtool/tests/x86/fixtures/identity_assembly.S
 create mode 100644 tools/objtool/tests/x86/fixtures/warn_reference.c
 create mode 100755 tools/objtool/tests/x86/test-identity-assembly.sh
 create mode 100755 tools/objtool/tests/x86/test-warn-reference.sh


base-commit: 945537271df1e3680be9b32af7041ac20f1184fb
-- 
2.55.0


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [RFC PATCH v2 01/11] objtool/klp: Fix checksums and copying of wide strings
  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 ` Longjun Luo
  2026-11-04 13:31 ` [RFC PATCH v2 02/11] objtool/klp: Hash compound literals by content Longjun Luo
                   ` (9 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Longjun Luo @ 2026-11-04 13:31 UTC (permalink / raw)
  To: jpoimboe, peterz
  Cc: mbenes, pmladek, joe.lawrence, song, puranjay, nathan,
	ndesaulniers, morbo, justinstitt, live-patching, llvm,
	linux-kernel, Longjun Luo

SHF_STRINGS sections can contain elements wider than one byte. Using
strlen() truncates their contents at an embedded zero byte, hiding
changes and copying incomplete strings into the patch.

Find the terminating zero element using sh_entsize. Use the resulting
length for both reference checksums and copying, and propagate errors
through checksum calculation. Keep single-byte string handling unchanged.

Add a test for two- and four-byte strings, including references from data.

Validated the available objtool tests with GCC 16.2.1 and Clang
22.1.8. No failures.

Fixes: 0d83da43b1e1 ("objtool/klp: Add --checksum option to generate per-function checksums")
Fixes: dd590d4d57eb ("objtool/klp: Introduce klp diff subcommand for diffing object files")
Fixes: 873a2208ea31 ("objtool/klp: Calculate object checksums")
Assisted-by: LLM
Signed-off-by: Longjun Luo <luolongjuna@gmail.com>
---
 tools/objtool/elf.c                           | 29 +++++++++
 tools/objtool/include/objtool/elf.h           |  1 +
 tools/objtool/klp-checksum.c                  | 44 ++++++++-----
 tools/objtool/klp-diff.c                      | 14 ++--
 .../tests/generic/fixtures/identity/common.h  | 15 +++++
 .../generic/fixtures/identity/wide_string.c   | 36 ++++++++++
 .../generic/test-checksum-wide-string.sh      | 39 +++++++++++
 tools/objtool/tests/identity-lib.sh           | 65 +++++++++++++++++++
 8 files changed, 223 insertions(+), 20 deletions(-)
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/common.h
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/wide_string.c
 create mode 100755 tools/objtool/tests/generic/test-checksum-wide-string.sh
 create mode 100644 tools/objtool/tests/identity-lib.sh

diff --git a/tools/objtool/elf.c b/tools/objtool/elf.c
index a791f4ea6ec1..71f1a272e468 100644
--- a/tools/objtool/elf.c
+++ b/tools/objtool/elf.c
@@ -1382,6 +1382,35 @@ struct elf *elf_create_file(GElf_Ehdr *ehdr, const char *name)
 	return elf;
 }
 
+/* Wide strings, such as the u16 PSR setup table, end at an all-zero element. */
+ssize_t elf_string_len(struct section *sec, unsigned long offset)
+{
+	size_t entsize = sec->sh.sh_entsize, size, len;
+	const char *str;
+
+	/*
+	 * FIXME: GCC's range_string() uses a one-past-end string pointer.
+	 * strlen() reads past the section; preserve the string base and addend.
+	 */
+	if (entsize == 1)
+		return strlen(sec->data->d_buf + offset);
+
+	if (!entsize || offset >= sec_size(sec)) {
+		ERROR("%s+%#lx: invalid string offset or character size", sec->name, offset);
+		return -1;
+	}
+
+	str = sec->data->d_buf + offset;
+	size = sec_size(sec) - offset;
+	for (len = 0; size - len >= entsize; len += entsize) {
+		if (!memchr_inv(str + len, 0, entsize))
+			return len;
+	}
+
+	ERROR("%s+%#lx: unterminated string", sec->name, offset);
+	return -1;
+}
+
 unsigned int elf_add_string(struct elf *elf, struct section *strtab, const char *str)
 {
 	unsigned int offset;
diff --git a/tools/objtool/include/objtool/elf.h b/tools/objtool/include/objtool/elf.h
index a82517a76a0f..5d7ddadf9471 100644
--- a/tools/objtool/include/objtool/elf.h
+++ b/tools/objtool/include/objtool/elf.h
@@ -179,6 +179,7 @@ struct symbol *elf_create_section_symbol(struct elf *elf, struct section *sec);
 void *elf_add_data(struct elf *elf, struct section *sec, const void *data,
 		   size_t size);
 
+ssize_t elf_string_len(struct section *sec, unsigned long offset);
 unsigned int elf_add_string(struct elf *elf, struct section *strtab, const char *str);
 
 struct reloc *elf_create_reloc(struct elf *elf, struct section *sec,
diff --git a/tools/objtool/klp-checksum.c b/tools/objtool/klp-checksum.c
index ebe25f9c5260..efaf083c03a1 100644
--- a/tools/objtool/klp-checksum.c
+++ b/tools/objtool/klp-checksum.c
@@ -67,8 +67,8 @@ static bool is_anonymous_const_data(struct symbol *sym)
 	       (is_sec_sym(sym) || strstarts(sym->name, ".LC"));
 }
 
-static void checksum_update_insn(struct objtool_file *file, struct symbol *func,
-				 struct instruction *insn)
+static int checksum_update_insn(struct objtool_file *file, struct symbol *func,
+				struct instruction *insn)
 {
 	struct reloc *reloc = insn_reloc(file, insn);
 	struct alternative *alt;
@@ -77,7 +77,7 @@ static void checksum_update_insn(struct objtool_file *file, struct symbol *func,
 	static bool in_alt;
 
 	if (insn->fake)
-		return;
+		return 0;
 
 	if (!reloc) {
 		struct symbol *call_dest = insn_call_dest(insn);
@@ -135,10 +135,13 @@ static void checksum_update_insn(struct objtool_file *file, struct symbol *func,
 	offset = arch_insn_adjusted_addend(insn, reloc);
 
 	if (is_string_sec(sym->sec)) {
-		char *str;
+		ssize_t size = elf_string_len(sym->sec, sym->offset + offset);
+		const void *data;
 
-		str = sym->sec->data->d_buf + sym->offset + offset;
-		__checksum_update_insn(func, insn, str, strlen(str));
+		if (size < 0)
+			return -1;
+		data = sym->sec->data->d_buf + sym->offset + offset;
+		__checksum_update_insn(func, insn, data, size);
 		goto alts;
 	}
 
@@ -180,19 +183,22 @@ static void checksum_update_insn(struct objtool_file *file, struct symbol *func,
 			__checksum_update_insn(func, insn, &alt_group->feature,sizeof(alt_group->feature));
 
 			for (alt_insn = alt->insn; alt_insn; alt_insn = next_insn_same_sec(file, alt_insn)) {
-				checksum_update_insn(file, func, alt_insn);
+				if (checksum_update_insn(file, func, alt_insn))
+					return -1;
 				if (!alt_group->last_insn || alt_insn == alt_group->last_insn)
 					break;
 			}
 		} else {
-			checksum_update_insn(file, func, alt->insn);
+			if (checksum_update_insn(file, func, alt->insn))
+				return -1;
 		}
 
 		in_alt = false;
 	}
+	return 0;
 }
 
-static void checksum_update_object(struct objtool_file *file, struct symbol *sym)
+static int checksum_update_object(struct objtool_file *file, struct symbol *sym)
 {
 	struct reloc *reloc;
 
@@ -211,11 +217,13 @@ static void checksum_update_object(struct objtool_file *file, struct symbol *sym
 		offset = reloc_addend(reloc);
 
 		if (is_string_sec(target->sec)) {
-			char *str;
+			ssize_t size = elf_string_len(target->sec, target->offset + offset);
+			const void *data;
 
-			str = target->sec->data->d_buf + target->offset + offset;
-			__checksum_update_object(sym, sym_offset,
-						 "reloc string", str, strlen(str));
+			if (size < 0)
+				return -1;
+			data = target->sec->data->d_buf + target->offset + offset;
+			__checksum_update_object(sym, sym_offset, "reloc string", data, size);
 			continue;
 		}
 
@@ -233,6 +241,7 @@ static void checksum_update_object(struct objtool_file *file, struct symbol *sym
 		__checksum_update_object(sym, sym_offset, "reloc addend",
 					 &offset, sizeof(offset));
 	}
+	return 0;
 }
 
 int calculate_checksums(struct objtool_file *file)
@@ -256,13 +265,16 @@ int calculate_checksums(struct objtool_file *file)
 
 		if (is_func_sym(sym)) {
 			checksum_init(sym);
-			func_for_each_insn(file, sym, insn)
-				checksum_update_insn(file, sym, insn);
+			func_for_each_insn(file, sym, insn) {
+				if (checksum_update_insn(file, sym, insn))
+					return -1;
+			}
 			checksum_finish(sym);
 
 		} else if (is_object_sym(sym)) {
 			checksum_init(sym);
-			checksum_update_object(file, sym);
+			if (checksum_update_object(file, sym))
+				return -1;
 			checksum_finish(sym);
 		}
 
diff --git a/tools/objtool/klp-diff.c b/tools/objtool/klp-diff.c
index 34da55e45609..6321f691adbb 100644
--- a/tools/objtool/klp-diff.c
+++ b/tools/objtool/klp-diff.c
@@ -1548,12 +1548,18 @@ static int clone_reloc(struct elfs *e, struct reloc *patched_reloc,
 	 * version of the string section.  Now copy the string itself.
 	 */
 	if (is_string_sec(patched_sym->sec)) {
-		const char *str = patched_sym->sec->data->d_buf + addend;
+		ssize_t len = elf_string_len(patched_sym->sec, addend);
+		const char *str;
 
-		__dbg_clone("\"%s\"", escape_str(str));
+		if (len < 0)
+			return -1;
+		str = patched_sym->sec->data->d_buf + addend;
+		if (patched_sym->sec->sh.sh_entsize == 1)
+			__dbg_clone("\"%s\"", escape_str(str));
 
-		addend = elf_add_string(e->out, out_sym->sec, str);
-		if (addend == -1)
+		addend = ALIGN(sec_size(out_sym->sec), out_sym->sec->sh.sh_addralign);
+		if (!elf_add_data(e->out, out_sym->sec, str,
+				  len + patched_sym->sec->sh.sh_entsize))
 			return -1;
 	}
 
diff --git a/tools/objtool/tests/generic/fixtures/identity/common.h b/tools/objtool/tests/generic/fixtures/identity/common.h
new file mode 100644
index 000000000000..d0f56751fcd9
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/common.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#ifndef KLP_TEST_IDENTITY_H
+#define KLP_TEST_IDENTITY_H
+
+#ifdef __clang__
+/* Use noinline on Clang to keep calls visible in these tests. */
+#define NOIPA __attribute__((noinline))
+#else
+#define NOIPA __attribute__((noipa))
+#endif
+#define MODINFO static const char modinfo[] \
+	__attribute__((used, section(".modinfo"))) = "name=vmlinux"
+
+#endif
diff --git a/tools/objtool/tests/generic/fixtures/identity/wide_string.c b/tools/objtool/tests/generic/fixtures/identity/wide_string.c
new file mode 100644
index 000000000000..4b25781e1827
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/wide_string.c
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+
+#ifdef WIDE32
+#ifdef PATCHED
+#define TEXT U"\x014a\x0113\x00dc\x00a5\x006e\x0038"
+#else
+#define TEXT U"\x014a\x0113\x00dc\x00a5\x006e\x0037"
+#endif
+#else
+#ifdef PATCHED
+#define TEXT u"\x014a\x0113\x00dc\x00a5\x006e\x0038"
+#else
+#define TEXT u"\x014a\x0113\x00dc\x00a5\x006e\x0037"
+#endif
+#endif
+
+#ifdef OBJECT
+struct descriptor {
+	const void *text;
+};
+
+const struct descriptor descriptor = { TEXT };
+
+NOIPA const struct descriptor *target(void)
+{
+	return &descriptor;
+}
+#else
+NOIPA const void *target(void)
+{
+	return TEXT;
+}
+#endif
diff --git a/tools/objtool/tests/generic/test-checksum-wide-string.sh b/tools/objtool/tests/generic/test-checksum-wide-string.sh
new file mode 100755
index 000000000000..426adc42337a
--- /dev/null
+++ b/tools/objtool/tests/generic/test-checksum-wide-string.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# Wide strings contain zero bytes before their terminating zero element.
+. "$(dirname "$0")/../identity-lib.sh"
+
+wide_section()
+{
+	in_sections "$1" | sed 's/^ *\[[ 0-9]*\] *//' |
+		awk -v width="$width" '$7 ~ /S/ && $6+0 == width { print $1; exit }'
+}
+
+setup
+for width in 2 4; do
+	flags=()
+	[ "$width" = 4 ] && flags=(-DWIDE32)
+	build_pair identity/wide_string.c "${flags[@]}"
+	input="$(wide_section patched.o)"
+	[ -n "$input" ] || fail "missing $width-byte string section"
+
+	assert_checksum_differs target
+	run_diff
+	assert_patched target
+	output="$(wide_section out.o)"
+	[ -n "$output" ] || fail "wide string was not copied"
+	$OBJCOPY -O binary --only-section="$input" "$workdir/patched.o" "$workdir/expected.bin" ||
+		fail "cannot extract input string"
+	$OBJCOPY -O binary --only-section="$output" "$workdir/out.o" "$workdir/actual.bin" ||
+		fail "cannot extract output string"
+	cmp -s "$workdir/expected.bin" "$workdir/actual.bin" || fail "wide string was truncated"
+	partial_link "$workdir/linked.o" "$workdir/out.o" || fail "wide-string output does not link"
+
+	build_pair identity/wide_string.c "${flags[@]}" -DOBJECT
+	assert_checksum_differs descriptor
+	run_diff 255
+	assert_diff_log 'changed data: descriptor'
+done
+
+pass "wide-string checksums and copies cover every element"
diff --git a/tools/objtool/tests/identity-lib.sh b/tools/objtool/tests/identity-lib.sh
new file mode 100644
index 000000000000..fc7014a88bc8
--- /dev/null
+++ b/tools/objtool/tests/identity-lib.sh
@@ -0,0 +1,65 @@
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# Shared build helpers for the multi-TU identity fixtures.
+. "$(dirname "$0")/../lib.sh"
+
+FIXTURE_CFLAGS="$FIXTURE_CFLAGS -fno-pie -fno-pic"
+
+# build_identity_pair <none|thin|full> <extra-cflags> <fixture.c...>
+build_identity_pair()
+{
+	local mode="$1" cflags="$2" side file i
+	local CC="$CC"
+	local flags=() parts=() patched=()
+	shift 2
+	if [ "$mode" != none ]; then
+		clang_only "LTO fixtures require clang"
+		find_thinlto_toolchain || probe_skip "no matching clang/lld pair"
+		flags=("-flto=$mode")
+		CC="$THIN_CC"
+	fi
+	orig_obj=orig.o
+	patched_obj=patched.o
+	for side in orig patched; do
+		patched=()
+		[ "$side" = patched ] && patched=(-DPATCHED)
+		parts=()
+		i=0
+		for file in "$@"; do
+			build_one "$file" "$side-$i.o" "${flags[@]}" "${patched[@]}" $cflags
+			parts+=("$workdir/$side-$i.o")
+			i=$((i + 1))
+		done
+		if [ "$mode" = none ]; then
+			partial_link "$workdir/$side.o" "${parts[@]}" || fail "partial link failed"
+		else
+			"$THIN_LD" -r "${parts[@]}" -o "$workdir/$side.o" || fail "LTO link failed"
+		fi
+	done
+}
+
+# identity_relocs <object> <relocation-section>
+identity_relocs()
+{
+	# readelf prints the section name in single quotes.
+	in_relocs "$1" | awk -v sec="'$2'" '
+		/Relocation section/ { active = $3 == sec }
+		active && $1 ~ /^[[:xdigit:]]+$/ { print }'
+}
+
+# The fixture's function references one static in .bss, .data or .rodata.
+identity_object_for()
+{
+	identity_relocs "$1" ".rela.text.$2" | awk '
+		$5 ~ /^\.(bss|data|rodata)\./ {
+			sub(/^\.(bss|data|rodata)\./, "", $5); print $5; exit
+		}'
+}
+
+# After post-link, check the reference to the original symbol, not just its presence.
+assert_original_reference()
+{
+	identity_relocs out.o ".klp.rela.orig..text.$1" |
+		awk -v n=".klp.sym.orig.$2," 'index($5, n) == 1' | grep -q . ||
+		fail "expected '$1' to reference original '$2'"
+}
-- 
2.55.0


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [RFC PATCH v2 02/11] objtool/klp: Hash compound literals by content
  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 ` Longjun Luo
  2026-11-04 13:31 ` [RFC PATCH v2 03/11] objtool/klp: Preserve identifier digits when stripping suffixes Longjun Luo
                   ` (8 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Longjun Luo @ 2026-11-04 13:31 UTC (permalink / raw)
  To: jpoimboe, peterz
  Cc: mbenes, pmladek, joe.lawrence, song, puranjay, nathan,
	ndesaulniers, morbo, justinstitt, live-patching, llvm,
	linux-kernel, Longjun Luo

Compiler-generated names for compound literals do not describe their
contents. Changes to an anonymous array or a callback in an anonymous
structure can leave the referring function's checksum unchanged.

Hash compound contents and relocations recursively, using the resulting
checksum in references. Preserve target offsets and stop at named
objects. Reject cycles and nesting beyond three compound objects.

Recognize GCC and Clang compound literal names in both writable and
read-only sections. Add tests for changed array elements and callbacks.

Validated the available objtool tests with GCC 16.2.1 and Clang
22.1.8. No failures.

Assisted-by: LLM
Signed-off-by: Longjun Luo <luolongjuna@gmail.com>
---
 tools/objtool/include/objtool/klp.h           |   2 +
 tools/objtool/klp-checksum.c                  | 102 +++++++++++++-----
 tools/objtool/klp-diff.c                      |  31 ++++++
 .../generic/fixtures/identity/compound.c      |  65 +++++++++++
 .../tests/generic/test-identity-compound.sh   |  13 +++
 5 files changed, 187 insertions(+), 26 deletions(-)
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/compound.c
 create mode 100755 tools/objtool/tests/generic/test-identity-compound.sh

diff --git a/tools/objtool/include/objtool/klp.h b/tools/objtool/include/objtool/klp.h
index c57775d78c71..90468e141124 100644
--- a/tools/objtool/include/objtool/klp.h
+++ b/tools/objtool/include/objtool/klp.h
@@ -51,6 +51,8 @@ struct objtool_file;
 struct elf;
 struct symbol;
 
+bool is_compound_constant(struct symbol *sym);
+
 int klp_create_symid_sections(struct objtool_file *file);
 
 int klp_sympos_init(struct elf *orig);
diff --git a/tools/objtool/klp-checksum.c b/tools/objtool/klp-checksum.c
index efaf083c03a1..2a515c7e2bf6 100644
--- a/tools/objtool/klp-checksum.c
+++ b/tools/objtool/klp-checksum.c
@@ -11,6 +11,8 @@
 #include <objtool/warn.h>
 #include <objtool/checksum.h>
 
+#define COMPOUND_MAX_DEPTH 3
+
 static int checksum_debug_init(struct objtool_file *file)
 {
 	char *dup, *s;
@@ -67,6 +69,14 @@ static bool is_anonymous_const_data(struct symbol *sym)
 	       (is_sec_sym(sym) || strstarts(sym->name, ".LC"));
 }
 
+static bool is_const_reference(struct symbol *sym)
+{
+	return is_string_sec(sym->sec) || is_anonymous_const_data(sym);
+}
+
+static int checksum_object(struct objtool_file *file, struct symbol *sym,
+			   unsigned int depth);
+
 static int checksum_update_insn(struct objtool_file *file, struct symbol *func,
 				struct instruction *insn)
 {
@@ -74,7 +84,9 @@ static int checksum_update_insn(struct objtool_file *file, struct symbol *func,
 	struct alternative *alt;
 	unsigned long offset;
 	struct symbol *sym;
+	const void *data;
 	static bool in_alt;
+	ssize_t size;
 
 	if (insn->fake)
 		return 0;
@@ -134,10 +146,9 @@ static int checksum_update_insn(struct objtool_file *file, struct symbol *func,
 	sym = reloc->sym;
 	offset = arch_insn_adjusted_addend(insn, reloc);
 
-	if (is_string_sec(sym->sec)) {
-		ssize_t size = elf_string_len(sym->sec, sym->offset + offset);
-		const void *data;
-
+	if (is_const_reference(sym)) {
+		size = is_string_sec(sym->sec) ? elf_string_len(sym->sec, sym->offset + offset) :
+					      sym->sec->sh.sh_entsize;
 		if (size < 0)
 			return -1;
 		data = sym->sec->data->d_buf + sym->offset + offset;
@@ -145,14 +156,6 @@ static int checksum_update_insn(struct objtool_file *file, struct symbol *func,
 		goto alts;
 	}
 
-	if (is_anonymous_const_data(sym)) {
-		void *cst;
-
-		cst = sym->sec->data->d_buf + sym->offset + offset;
-		__checksum_update_insn(func, insn, cst, sym->sec->sh.sh_entsize);
-		goto alts;
-	}
-
 	if (is_sec_sym(sym)) {
 		sym = find_symbol_containing(reloc->sym->sec, offset);
 		if (!sym)
@@ -161,8 +164,15 @@ static int checksum_update_insn(struct objtool_file *file, struct symbol *func,
 		offset -= sym->offset;
 	}
 
-	__checksum_update_insn(func, insn, sym->demangled_name,
-			       strlen(sym->demangled_name));
+	if (is_compound_constant(sym)) {
+		if (checksum_object(file, sym, 1))
+			return -1;
+		__checksum_update_insn(func, insn, &sym->csum.checksum,
+				       sizeof(sym->csum.checksum));
+	} else {
+		__checksum_update_insn(func, insn, sym->demangled_name,
+				       strlen(sym->demangled_name));
+	}
 	__checksum_update_insn(func, insn, &offset, sizeof(offset));
 
 alts:
@@ -198,7 +208,8 @@ static int checksum_update_insn(struct objtool_file *file, struct symbol *func,
 	return 0;
 }
 
-static int checksum_update_object(struct objtool_file *file, struct symbol *sym)
+static int checksum_update_object(struct objtool_file *file, struct symbol *sym,
+				  unsigned int depth)
 {
 	struct reloc *reloc;
 
@@ -212,38 +223,79 @@ static int checksum_update_object(struct objtool_file *file, struct symbol *sym)
 	sym_for_each_reloc(file->elf, sym, reloc) {
 		unsigned long sym_offset = reloc_offset(reloc) - sym->offset;
 		struct symbol *target = reloc->sym;
+		unsigned int type = reloc_type(reloc);
+		const void *data;
+		ssize_t size;
 		s64 offset;
 
 		offset = reloc_addend(reloc);
 
-		if (is_string_sec(target->sec)) {
-			ssize_t size = elf_string_len(target->sec, target->offset + offset);
-			const void *data;
+		__checksum_update_object(sym, sym_offset, "reloc type",
+					 &type, sizeof(type));
 
+		if (is_const_reference(target)) {
+			size = is_string_sec(target->sec) ?
+				elf_string_len(target->sec, target->offset + offset) :
+				target->sec->sh.sh_entsize;
 			if (size < 0)
 				return -1;
 			data = target->sec->data->d_buf + target->offset + offset;
-			__checksum_update_object(sym, sym_offset, "reloc string", data, size);
+			__checksum_update_object(sym, sym_offset, "reloc constant", data, size);
 			continue;
 		}
 
 		if (is_sec_sym(target)) {
 			target = find_symbol_containing(reloc->sym->sec, offset);
-			if (!target)
+			if (!target) {
+				if (is_compound_constant(sym)) {
+					ERROR("%s+%#lx: unresolved compound constant reference",
+					      sym->name, sym_offset);
+					return -1;
+				}
 				continue;
+			}
 
 			offset -= target->offset;
 		}
 
-		__checksum_update_object(sym, sym_offset, "reloc name",
-					 target->demangled_name,
-					 strlen(target->demangled_name));
+		if (is_compound_constant(target)) {
+			if (checksum_object(file, target, depth + 1))
+				return -1;
+			__checksum_update_object(sym, sym_offset, "reloc compound",
+						 &target->csum.checksum,
+						 sizeof(target->csum.checksum));
+		} else {
+			__checksum_update_object(sym, sym_offset, "reloc name",
+						 target->demangled_name,
+						 strlen(target->demangled_name));
+		}
 		__checksum_update_object(sym, sym_offset, "reloc addend",
 					 &offset, sizeof(offset));
 	}
 	return 0;
 }
 
+/* Count only consecutive compound constants; named targets use their identity. */
+static int checksum_object(struct objtool_file *file, struct symbol *sym,
+			   unsigned int depth)
+{
+	if (depth > COMPOUND_MAX_DEPTH) {
+		ERROR("%s: compound constant nesting exceeds %u levels",
+		      sym->name, COMPOUND_MAX_DEPTH);
+		return -1;
+	}
+	if (sym->csum.state) {
+		ERROR("%s: cyclic compound constant reference", sym->name);
+		return -1;
+	}
+
+	checksum_init(sym);
+	if (checksum_update_object(file, sym, depth))
+		return -1;
+	checksum_finish(sym);
+	return 0;
+}
+
 int calculate_checksums(struct objtool_file *file)
 {
 	struct instruction *insn;
@@ -272,10 +324,8 @@ int calculate_checksums(struct objtool_file *file)
 			checksum_finish(sym);
 
 		} else if (is_object_sym(sym)) {
-			checksum_init(sym);
-			if (checksum_update_object(file, sym))
+			if (checksum_object(file, sym, is_compound_constant(sym) ? 1 : 0))
 				return -1;
-			checksum_finish(sym);
 		}
 
 	}
diff --git a/tools/objtool/klp-diff.c b/tools/objtool/klp-diff.c
index 6321f691adbb..0391f91a8d31 100644
--- a/tools/objtool/klp-diff.c
+++ b/tools/objtool/klp-diff.c
@@ -438,6 +438,37 @@ static bool is_anonymous_rodata(struct symbol *sym)
 	       (!is_object_sym(sym) || strstarts(sym->sec->name, ".rodata.cst"));
 }
 
+/* GCC and Clang give compound literals generated names, not source identities. */
+bool is_compound_constant(struct symbol *sym)
+{
+	const char *name = sym->name;
+
+	if (!is_object_sym(sym) || is_undef_sym(sym) || !sym->len ||
+	    !sym->sec)
+		return false;
+
+	if (strstarts(name, ".compoundliteral"))
+		name += strlen(".compoundliteral");
+	else if (strstarts(name, "__compound_literal"))
+		name += strlen("__compound_literal");
+	else
+		return false;
+
+	while (*name == '.' && isdigit((unsigned char)name[1])) {
+		name++;
+		while (isdigit((unsigned char)*name))
+			name++;
+	}
+	if (strstarts(name, ".llvm.")) {
+		name += strlen(".llvm.");
+		if (!isdigit((unsigned char)*name))
+			return false;
+		while (isdigit((unsigned char)*name))
+			name++;
+	}
+	return !*name;
+}
+
 /*
  * These symbols should never be correlated, so their local patched versions
  * are used instead of linking to the originals.
diff --git a/tools/objtool/tests/generic/fixtures/identity/compound.c b/tools/objtool/tests/generic/fixtures/identity/compound.c
new file mode 100644
index 000000000000..bf607bdd6093
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/compound.c
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+extern void observe(int value);
+
+NOIPA int read_value(int value)
+{
+	return value + 1;
+}
+
+NOIPA int other_value(int value)
+{
+	return value + 2;
+}
+
+#ifdef WRITABLE
+#define CONST
+#else
+#define CONST const
+#endif
+
+struct config {
+	CONST unsigned int *values;
+	const char *label;
+	int (*read)(int value);
+};
+
+#if defined(PATCHED) && defined(CHANGE_LEAF)
+#define FIRST 12
+#else
+#define FIRST 11
+#endif
+#if defined(PATCHED) && defined(CHANGE_TARGET)
+#define READ other_value
+#else
+#define READ read_value
+#endif
+
+static CONST struct config *const first = &(CONST struct config) {
+	(CONST unsigned int[]) { FIRST, 22, 33, 44, 55, 66, 77, 88 }, "table", READ
+};
+static CONST struct config *const second = &(CONST struct config) {
+	(CONST unsigned int[]) { 11, 22, 33, 44, 55, 66, 77, 88 }, "table", read_value
+};
+
+NOIPA CONST struct config *target(int value)
+{
+#if defined(PATCHED) && defined(CHANGE_FUNCTION)
+	if (value < 0)
+		return (void *)0;
+#endif
+	observe(value);
+#ifdef AMBIGUOUS
+	return value ? first : second;
+#else
+	return first;
+#endif
+}
+
+NOIPA CONST struct config *unchanged(int value)
+{
+	observe(value);
+	return second;
+}
diff --git a/tools/objtool/tests/generic/test-identity-compound.sh b/tools/objtool/tests/generic/test-identity-compound.sh
new file mode 100755
index 000000000000..76b16616b32c
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-compound.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# Array contents and callbacks contribute to the referring function's checksum.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+for change in CHANGE_LEAF CHANGE_TARGET; do
+	build_pair identity/compound.c "-D$change"
+	assert_checksum_differs target
+done
+
+pass "compound contents contribute to reference checksums"
-- 
2.55.0


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [RFC PATCH v2 03/11] objtool/klp: Preserve identifier digits when stripping suffixes
  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 ` Longjun Luo
  2026-11-04 13:31 ` [RFC PATCH v2 04/11] objtool/klp: Read symbol identities from DWARF Longjun Luo
                   ` (7 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Longjun Luo @ 2026-11-04 13:31 UTC (permalink / raw)
  To: jpoimboe, peterz
  Cc: mbenes, pmladek, joe.lawrence, song, puranjay, nathan,
	ndesaulniers, morbo, justinstitt, live-patching, llvm,
	linux-kernel, Longjun Luo

The name tx.crc32 loses its trailing digits, while tx.crc32.llvm.<hash>
keeps them because of an early return. ThinLTO promotion can therefore
break correlation and copy existing static state into the patch.

Strip LLVM hashes and complete .<number> suffixes on the same path.
Keep the counter-stripping rule for __UNIQUE_ID names, including
Clang's function-prefixed names.

Add a ThinLTO test which checks that the existing crc32 state is reused.
The available objtool tests pass with GCC 16.2.1 and Clang 22.1.8.

Fixes: 0d83da43b1e1 ("objtool/klp: Add --checksum option to generate per-function checksums")
Fixes: 020b71dcafee ("objtool/klp: Remove .llvm suffix in demangle_name()")
Assisted-by: LLM
Signed-off-by: Longjun Luo <luolongjuna@gmail.com>
---
 tools/objtool/elf.c                           | 33 +++++++++++--------
 .../fixtures/identity/promotion_crc32.c       |  8 +++++
 .../fixtures/identity/promotion_crc32_user.c  | 15 +++++++++
 .../generic/test-identity-thinlto-crc32.sh    | 23 +++++++++++++
 4 files changed, 66 insertions(+), 13 deletions(-)
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/promotion_crc32.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/promotion_crc32_user.c
 create mode 100755 tools/objtool/tests/generic/test-identity-thinlto-crc32.sh

diff --git a/tools/objtool/elf.c b/tools/objtool/elf.c
index 71f1a272e468..3ddbf5143260 100644
--- a/tools/objtool/elf.c
+++ b/tools/objtool/elf.c
@@ -448,32 +448,39 @@ static int read_sections(struct elf *elf)
  */
 static ssize_t demangled_name_len(const char *name)
 {
-	ssize_t idx;
+	ssize_t idx, len;
 	const char *p;
 
 	p = strstr(name, ".llvm.");
-	if (p)
-		return p - name;
+	len = p ? p - name : strlen(name);
 
-	if (!strstarts(name, "__UNIQUE_ID_") && !strchr(name, '.'))
-		return strlen(name);
+	if (strstarts(name, "__UNIQUE_ID_") || strstr(name, ".__UNIQUE_ID_")) {
+		for (idx = len - 1; idx >= 0; idx--) {
+			char c = name[idx];
 
-	for (idx = strlen(name) - 1; idx >= 0; idx--) {
-		char c = name[idx];
+			if (!isdigit(c) && c != '.' && c != '_')
+				break;
+		}
+		return idx + 1;
+	}
+
+	while (len && isdigit(name[len - 1])) {
+		p = name + len - 1;
+		while (p > name && isdigit(*p))
+			p--;
 
-		if (!isdigit(c) && c != '.' && c != '_')
+		if (p == name || *p != '.')
 			break;
+		len = p - name;
 	}
-	if (idx <= 0)
-		return strlen(name);
-	return idx + 1;
+	return len;
 }
 
 /*
  * Remove number suffix of a symbol.
  *
- * Specifically, remove trailing numbers for "__UNIQUE_ID_" symbols and
- * symbols with '.'.
+ * Remove complete .<number> suffixes and trailing numbers from
+ * "__UNIQUE_ID_" symbols.
  *
  * With CONFIG_LTO_CLANG_THIN, it is possible to have nested __UNIQUE_ID_,
  * such as
diff --git a/tools/objtool/tests/generic/fixtures/identity/promotion_crc32.c b/tools/objtool/tests/generic/fixtures/identity/promotion_crc32.c
new file mode 100644
index 000000000000..d95914eacdc0
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/promotion_crc32.c
@@ -0,0 +1,8 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+unsigned int tx(unsigned int value)
+{
+	static volatile unsigned int crc32;
+
+	return crc32 += value;
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/promotion_crc32_user.c b/tools/objtool/tests/generic/fixtures/identity/promotion_crc32_user.c
new file mode 100644
index 000000000000..b44fa1d4a75e
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/promotion_crc32_user.c
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+
+unsigned int tx(unsigned int value);
+
+unsigned int target(unsigned int value)
+{
+#ifdef PATCHED
+	return tx(value);
+#else
+	return value;
+#endif
+}
diff --git a/tools/objtool/tests/generic/test-identity-thinlto-crc32.sh b/tools/objtool/tests/generic/test-identity-thinlto-crc32.sh
new file mode 100755
index 000000000000..66988d0b7692
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-thinlto-crc32.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# ThinLTO promotion must retain the digits in a static variable's name.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+build_identity_pair thin "" identity/promotion_crc32.c identity/promotion_crc32_user.c
+in_symbols orig.o | awk '$4 == "OBJECT" && $5 == "LOCAL" && $8 == "tx.crc32"' |
+	grep -q . || probe_skip "CRC state was not local before importing"
+in_symbols patched.o | awk '$4 == "OBJECT" && $5 == "GLOBAL" && $8 ~ /^tx\.crc32\.llvm\.[0-9]+$/' |
+	grep -q . || probe_skip "ThinLTO did not promote the CRC state"
+
+assert_checksum_matches tx
+run_diff
+assert_patched target
+assert_not_patched tx
+out_symbols | awk '$4 == "OBJECT" && $7 != "UND" && $8 ~ /^tx\.crc32(\.|$)/' |
+	grep -q . && fail "promoted CRC state was allocated again"
+run_post_link
+assert_original_reference target tx.crc32
+
+pass "digits in static names survive ThinLTO promotion"
-- 
2.55.0


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [RFC PATCH v2 04/11] objtool/klp: Read symbol identities from DWARF
  2026-11-04 13:31 [RFC PATCH v2 00/11] objtool/klp: DWARF-based cross-build symbol correlation Longjun Luo
                   ` (2 preceding siblings ...)
  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 ` Longjun Luo
  2026-11-04 13:31 ` [RFC PATCH v2 05/11] objtool/klp: Support reduced and split DWARF Longjun Luo
                   ` (6 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Longjun Luo @ 2026-11-04 13:31 UTC (permalink / raw)
  To: jpoimboe, peterz
  Cc: mbenes, pmladek, joe.lawrence, song, puranjay, nathan,
	ndesaulniers, morbo, justinstitt, live-patching, llvm,
	linux-kernel, Longjun Luo

Compiler-generated names and ELF FILE symbols can change between builds.
Read each symbol's defining translation unit and named scope from DWARF
and combine them with its suffix-stripped name.

- Prefer DIEs matching the symbol's address and use names to narrow them.
- Fall back to names and FILE, using CU ranges to constrain functions.
- Handle address pieces, shrunk storage and assembly CU ranges.
- Cross-check DWARF sources against FILE and reject conflicting identities.
- Keep the resulting identities and release the temporary DIE indexes.

Only omit source for external definitions with GLOBAL or WEAK ELF
binding. Local clones retain their CU even when their DIE has
DW_AT_external. Record the existing cold-parent ambiguity after weak
overrides in a FIXME.

Add the libdw build dependency. This introduces the reader without
changing symbol correlation or checksum calculation.

Validated the available objtool tests with GCC 16.2.1 and Clang
22.1.8. No failures.

Assisted-by: LLM
Signed-off-by: Longjun Luo <luolongjuna@gmail.com>
---
 scripts/livepatch/klp-build                   |    2 +-
 tools/objtool/Build                           |    4 +-
 .../objtool/Documentation/klp-test-design.txt |    6 +-
 tools/objtool/Makefile                        |   12 +-
 tools/objtool/elf.c                           |   38 +-
 tools/objtool/include/objtool/elf.h           |   42 +-
 tools/objtool/include/objtool/klp-dwarf.h     |   22 +
 tools/objtool/include/objtool/klp-identity.h  |   26 +
 tools/objtool/klp-checksum.c                  |   16 +-
 tools/objtool/klp-diff.c                      |   10 +-
 tools/objtool/klp-dwarf.c                     | 1181 +++++++++++++++++
 tools/objtool/klp-identity.c                  |   94 ++
 tools/objtool/tests/lib.sh                    |    2 +-
 13 files changed, 1406 insertions(+), 49 deletions(-)
 create mode 100644 tools/objtool/include/objtool/klp-dwarf.h
 create mode 100644 tools/objtool/include/objtool/klp-identity.h
 create mode 100644 tools/objtool/klp-dwarf.c
 create mode 100644 tools/objtool/klp-identity.c

diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build
index b52a8489d9f6..e373928123e8 100755
--- a/scripts/livepatch/klp-build
+++ b/scripts/livepatch/klp-build
@@ -279,7 +279,7 @@ validate_config() {
 		die "Clang assembler version < 20 not supported"
 
 	[[ -x "$OBJTOOL" ]] && "$OBJTOOL" klp 2>&1 | command grep -q "not implemented" && \
-		die "objtool not built with KLP support; install xxhash-devel/libxxhash-dev (version >= 0.8) and recompile"
+		die "objtool not built with KLP support; install xxhash-devel/libxxhash-dev (version >= 0.8) and elfutils-devel/libdw-dev and recompile"
 
 	return 0
 }
diff --git a/tools/objtool/Build b/tools/objtool/Build
index 59f948628098..84139709a2dd 100644
--- a/tools/objtool/Build
+++ b/tools/objtool/Build
@@ -13,8 +13,8 @@ objtool-$(BUILD_DISAS) += disas.o
 objtool-$(BUILD_DISAS) += trace.o
 
 objtool-$(BUILD_ORC) += orc_gen.o orc_dump.o
-objtool-$(BUILD_KLP) += builtin-klp.o klp-checksum.o klp-diff.o \
-			klp-post-link.o klp-sympos.o
+objtool-$(BUILD_KLP) += builtin-klp.o klp-checksum.o klp-diff.o klp-dwarf.o \
+			klp-post-link.o klp-sympos.o klp-identity.o
 
 objtool-y += libstring.o
 objtool-y += libctype.o
diff --git a/tools/objtool/Documentation/klp-test-design.txt b/tools/objtool/Documentation/klp-test-design.txt
index 2082c277197f..e63aad0f4324 100644
--- a/tools/objtool/Documentation/klp-test-design.txt
+++ b/tools/objtool/Documentation/klp-test-design.txt
@@ -17,8 +17,8 @@ TL;DR
 =====
 
 One run covers one compiler and one architecture; CI runs the combinations.
-Build objtool first -- it needs libelf and libxxhash -- and the same ARCH is
-used for both steps.
+Build objtool first -- it needs libelf, libxxhash and libdw -- and use the
+same ARCH for both steps.
 
 Natively, with gcc::
 
@@ -211,7 +211,7 @@ Running
 
 ::
 
-    make -C tools/objtool                 # needs libelf and libxxhash
+    make -C tools/objtool                 # needs libelf, libxxhash and libdw
     make -C tools/objtool tests
 
     CC=clang make -C tools/objtool tests  # the other toolchain
diff --git a/tools/objtool/Makefile b/tools/objtool/Makefile
index db99c4759b2c..e2fbd778bb74 100644
--- a/tools/objtool/Makefile
+++ b/tools/objtool/Makefile
@@ -14,7 +14,12 @@ endif
 ifeq ($(ARCH_HAS_KLP),y)
 	HAVE_XXHASH = $(shell printf "$(pound)include <xxhash.h>\nXXH3_state_t *state;int main() {}" | \
 		      $(HOSTCC) $(HOSTCFLAGS) -xc - -o /dev/null -lxxhash 2> /dev/null && echo y || echo n)
-	ifeq ($(HAVE_XXHASH),y)
+	LIBDW_CFLAGS := $(shell $(HOSTPKG_CONFIG) libdw --cflags 2>/dev/null)
+	LIBDW_LIBS := $(shell $(HOSTPKG_CONFIG) libdw --libs 2>/dev/null || echo -ldw)
+	HAVE_LIBDW = $(shell printf "$(pound)include <elfutils/libdwfl.h>\nint main(void) { return !dwfl_begin(0); }" | \
+		     $(HOSTCC) $(HOSTCFLAGS) $(LIBDW_CFLAGS) -xc - -o /dev/null \
+		     $(HOSTLDFLAGS) $(LIBDW_LIBS) 2>/dev/null && echo y || echo n)
+	ifeq ($(HAVE_XXHASH)$(HAVE_LIBDW),yy)
 		BUILD_KLP	 := y
 		LIBXXHASH_CFLAGS := $(shell $(HOSTPKG_CONFIG) libxxhash --cflags 2>/dev/null) \
 				    -DBUILD_KLP
@@ -65,6 +70,11 @@ OBJTOOL_CFLAGS  := -std=gnu11 -fomit-frame-pointer -O2 -g $(WARNINGS)	\
 
 OBJTOOL_LDFLAGS := $(LIBSUBCMD) $(LIBELF_LIBS) $(LIBXXHASH_LIBS) $(HOSTLDFLAGS)
 
+ifeq ($(BUILD_KLP),y)
+OBJTOOL_CFLAGS += $(LIBDW_CFLAGS)
+OBJTOOL_LDFLAGS += $(LIBDW_LIBS)
+endif
+
 # Allow old libelf to be used:
 elfshdr := $(shell echo '$(pound)include <libelf.h>' | $(HOSTCC) $(OBJTOOL_CFLAGS) -x c -E - 2>/dev/null | grep elf_getshdr)
 OBJTOOL_CFLAGS += $(if $(elfshdr),,-DLIBELF_USE_DEPRECATED)
diff --git a/tools/objtool/elf.c b/tools/objtool/elf.c
index 3ddbf5143260..ee7422364810 100644
--- a/tools/objtool/elf.c
+++ b/tools/objtool/elf.c
@@ -9,7 +9,6 @@
 
 #include <sys/types.h>
 #include <sys/stat.h>
-#include <sys/mman.h>
 #include <fcntl.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -20,7 +19,6 @@
 #include <linux/align.h>
 #include <linux/kernel.h>
 #include <linux/interval_tree_generic.h>
-#include <linux/log2.h>
 #include <objtool/builtin.h>
 #include <objtool/elf.h>
 #include <objtool/klp.h>
@@ -33,13 +31,6 @@ u32 str_hash_demangled(const char *str)
 	return jhash(str, demangled_name_len(str), 0);
 }
 
-#define elf_hash_add(name, node, key)					\
-({									\
-	struct elf_hash_node *__node = node;				\
-	__node->next = __elf_table_entry(elf, name, key);		\
-	__elf_table_entry(elf, name, key) = __node;			\
-})
-
 static inline void __elf_hash_del(struct elf_hash_node *node,
 				  struct elf_hash_node **head)
 {
@@ -61,20 +52,6 @@ static inline void __elf_hash_del(struct elf_hash_node *node,
 #define elf_hash_del(name, node, key) \
 	__elf_hash_del(node, &__elf_table_entry(elf, name, key))
 
-#define elf_alloc_hash(name, size)					\
-({									\
-	__elf_bits(elf, name) = max(10, ilog2(size));			\
-	__elf_table(elf, name) = mmap(NULL,				\
-				 sizeof(struct elf_hash_node *) << __elf_bits(elf, name), \
-				 PROT_READ|PROT_WRITE,			\
-				 MAP_PRIVATE|MAP_ANON, -1, 0);		\
-	if (__elf_table(elf, name) == (void *)-1L) {			\
-		ERROR_GLIBC("mmap fail " #name);			\
-		__elf_table(elf, name) = NULL;				\
-	}								\
-	__elf_table(elf, name);						\
-})
-
 static inline unsigned long __sym_start(struct symbol *s)
 {
 	return s->offset;
@@ -545,13 +522,13 @@ static int elf_add_symbol(struct elf *elf, struct symbol *sym)
 		entry = &sym->sec->symbol_list;
 	list_add(&sym->list, entry);
 
-	sym->demangled_name = demangle_name(sym);
-	if (!sym->demangled_name)
+	sym->identity.demangled_name = demangle_name(sym);
+	if (!sym->identity.demangled_name)
 		return -1;
 
 	list_add_tail(&sym->global_list, &elf->symbols);
 	elf_hash_add(symbol, &sym->hash, sym->idx);
-	elf_hash_add(symbol_name, &sym->name_hash, str_hash(sym->demangled_name));
+	elf_hash_add(symbol_name, &sym->name_hash, str_hash(sym->identity.demangled_name));
 
 	if (is_func_sym(sym) &&
 	    (strstarts(sym->name, "__pfx_") ||
@@ -667,6 +644,10 @@ static int read_symbols(struct elf *elf)
 		if (elf_add_symbol(elf, sym))
 			return -1;
 
+		/*
+		 * FIXME: Assembly inputs without STT_FILE can inherit the preceding
+		 * input's FILE after partial linking.
+		 */
 		if (is_file_sym(sym))
 			file = sym;
 		else if (sym->bind == STB_LOCAL && !is_sec_sym(sym))
@@ -710,6 +691,11 @@ static int read_symbols(struct elf *elf)
 				return -1;
 			}
 
+			/*
+			 * FIXME: Weak and strong definitions can leave two local foo.cold symbols.
+			 * Both resolve to the surviving foo, so the last one overwrites cfunc and
+			 * can make the checksum miss changes to the active cold fragment.
+			 */
 			sym->pfunc = pfunc->alias;
 			pfunc->cfunc = sym;
 			pfunc->alias->cfunc = sym;
diff --git a/tools/objtool/include/objtool/elf.h b/tools/objtool/include/objtool/elf.h
index 5d7ddadf9471..f84d670e48a2 100644
--- a/tools/objtool/include/objtool/elf.h
+++ b/tools/objtool/include/objtool/elf.h
@@ -7,15 +7,18 @@
 #define _OBJTOOL_ELF_H
 
 #include <stdio.h>
+#include <sys/mman.h>
 #include <gelf.h>
 #include <linux/string.h>
 #include <linux/list.h>
 #include <linux/hashtable.h>
+#include <linux/log2.h>
 #include <linux/rbtree.h>
 #include <linux/jhash.h>
 
 #include <objtool/endianness.h>
 #include <objtool/checksum_types.h>
+#include <objtool/klp-identity.h>
 #include <arch/elf.h>
 
 #define SEC_NAME_LEN		1024
@@ -70,9 +73,11 @@ struct symbol {
 	struct rb_node node;
 	struct elf_hash_node hash;
 	struct elf_hash_node name_hash;
+	struct elf_hash_node identity_hash;
 	GElf_Sym sym;
 	struct section *sec;
-	const char *name, *demangled_name;
+	const char *name;
+	struct identity identity;
 	unsigned int idx, len;
 	unsigned long offset;
 	unsigned long __subtree_last;
@@ -121,16 +126,23 @@ struct elf {
 	unsigned int num_files;
 	struct list_head sections;
 	struct list_head symbols;
+	struct identity_reader *identity_reader;
 	unsigned long num_relocs;
 
 	int symbol_bits;
 	int symbol_name_bits;
+	int symbol_identity_bits;
+	int dwarf_address_bits;
+	int dwarf_name_bits;
 	int section_bits;
 	int section_name_bits;
 	int reloc_bits;
 
 	struct elf_hash_node **symbol_hash;
 	struct elf_hash_node **symbol_name_hash;
+	struct elf_hash_node **symbol_identity_hash;
+	struct elf_hash_node **dwarf_address_hash;
+	struct elf_hash_node **dwarf_name_hash;
 	struct elf_hash_node **section_hash;
 	struct elf_hash_node **section_name_hash;
 	struct elf_hash_node **reloc_hash;
@@ -145,6 +157,27 @@ struct elf {
 #define __elf_table_entry(elf, name, key) \
 	__elf_table(elf, name)[hash_min(key, __elf_bits(elf, name))]
 
+#define elf_alloc_hash(name, size)					\
+({									\
+	__elf_bits(elf, name) = max(10, ilog2(size));			\
+	__elf_table(elf, name) = mmap(NULL,				\
+				 sizeof(struct elf_hash_node *) << __elf_bits(elf, name), \
+				 PROT_READ|PROT_WRITE,			\
+				 MAP_PRIVATE|MAP_ANON, -1, 0);		\
+	if (__elf_table(elf, name) == (void *)-1L) {			\
+		ERROR_GLIBC("mmap fail " #name);			\
+		__elf_table(elf, name) = NULL;				\
+	}								\
+	__elf_table(elf, name);						\
+})
+
+#define elf_hash_add(name, node, key)					\
+({									\
+	struct elf_hash_node *__node = node;				\
+	__node->next = __elf_table_entry(elf, name, key);		\
+	__elf_table_entry(elf, name, key) = __node;			\
+})
+
 #define elf_list_entry(ptr, type, member)				\
 ({									\
 	typeof(ptr) __ptr = (ptr);					\
@@ -317,6 +350,11 @@ static inline bool is_cold_func(struct symbol *sym)
 	return sym->cold;
 }
 
+static inline bool has_dwarf_identity(const struct symbol *sym)
+{
+	return sym->identity.scope != NULL;
+}
+
 static inline bool is_reloc_sec(struct section *sec)
 {
 	return sec->sh.sh_type == SHT_RELA || sec->sh.sh_type == SHT_REL;
@@ -517,7 +555,7 @@ static inline void set_sym_next_reloc(struct reloc *reloc, struct reloc *next)
 #define for_each_sym_by_demangled_name(elf, name, sym)			\
 	elf_hash_for_each_possible(elf, symbol_name, sym, name_hash,	\
 				   str_hash(name))			\
-		if (strcmp(sym->demangled_name, name)) {} else
+		if (strcmp(sym->identity.demangled_name, name)) {} else
 
 #define rsec_next_reloc(rsec, reloc)					\
 	reloc_idx(reloc) < sec_num_entries(rsec) - 1 ? reloc + 1 : NULL
diff --git a/tools/objtool/include/objtool/klp-dwarf.h b/tools/objtool/include/objtool/klp-dwarf.h
new file mode 100644
index 000000000000..ba6029bd2235
--- /dev/null
+++ b/tools/objtool/include/objtool/klp-dwarf.h
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+ */
+
+#ifndef _OBJTOOL_KLP_DWARF_H
+#define _OBJTOOL_KLP_DWARF_H
+
+struct elf;
+struct symbol;
+
+int init_identity_cache(struct elf *elf);
+void free_identity_cache(struct elf *elf);
+
+/*
+ * Set source, scope and external in sym->identity from DWARF.
+ * Return 0 if found, 1 if unavailable, or -1 for invalid/conflicting DWARF.
+ * Returned strings belong to elf and must not be freed by the caller.
+ */
+int get_identity_for_symbol(struct elf *elf, struct symbol *sym);
+
+#endif
diff --git a/tools/objtool/include/objtool/klp-identity.h b/tools/objtool/include/objtool/klp-identity.h
new file mode 100644
index 000000000000..ffbc2bb906b1
--- /dev/null
+++ b/tools/objtool/include/objtool/klp-identity.h
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+ */
+
+#ifndef _OBJTOOL_KLP_IDENTITY_H
+#define _OBJTOOL_KLP_IDENTITY_H
+
+#include <linux/types.h>
+
+struct elf;
+
+struct identity {
+	const char *source;		/* physical defining TU path, or "" */
+	const char *scope;		/* enclosing scopes, NULL without DWARF identity */
+	const char *demangled_name;	/* suffix-stripped symbol name */
+	const char *path;		/* source/scope/demangled_name, or NULL */
+	bool external;
+};
+
+/* The caller must set symbol->dont_correlate before preparing identities. */
+int prepare_identities(struct elf *elf);
+u32 identity_hash(const struct identity *identity);
+bool same_identity(const struct identity *a, const struct identity *b);
+
+#endif
diff --git a/tools/objtool/klp-checksum.c b/tools/objtool/klp-checksum.c
index 2a515c7e2bf6..1a5f2fa89baa 100644
--- a/tools/objtool/klp-checksum.c
+++ b/tools/objtool/klp-checksum.c
@@ -110,8 +110,8 @@ static int checksum_update_insn(struct objtool_file *file, struct symbol *func,
 			__checksum_update_insn(func, insn, buf, len);
 
 			if (call_dest) {
-				__checksum_update_insn(func, insn, call_dest->demangled_name,
-						       strlen(call_dest->demangled_name));
+				__checksum_update_insn(func, insn, call_dest->identity.demangled_name,
+						       strlen(call_dest->identity.demangled_name));
 
 			} else if (jump_dest) {
 				struct symbol *dest_sym;
@@ -127,8 +127,8 @@ static int checksum_update_insn(struct objtool_file *file, struct symbol *func,
 				if (!dest_sym)
 					goto alts;
 
-				__checksum_update_insn(func, insn, dest_sym->demangled_name,
-						       strlen(dest_sym->demangled_name));
+				__checksum_update_insn(func, insn, dest_sym->identity.demangled_name,
+						       strlen(dest_sym->identity.demangled_name));
 
 				offset = jump_dest->offset - dest_sym->offset;
 				__checksum_update_insn(func, insn, &offset, sizeof(offset));
@@ -170,8 +170,8 @@ static int checksum_update_insn(struct objtool_file *file, struct symbol *func,
 		__checksum_update_insn(func, insn, &sym->csum.checksum,
 				       sizeof(sym->csum.checksum));
 	} else {
-		__checksum_update_insn(func, insn, sym->demangled_name,
-				       strlen(sym->demangled_name));
+		__checksum_update_insn(func, insn, sym->identity.demangled_name,
+				       strlen(sym->identity.demangled_name));
 	}
 	__checksum_update_insn(func, insn, &offset, sizeof(offset));
 
@@ -266,8 +266,8 @@ static int checksum_update_object(struct objtool_file *file, struct symbol *sym,
 						 sizeof(target->csum.checksum));
 		} else {
 			__checksum_update_object(sym, sym_offset, "reloc name",
-						 target->demangled_name,
-						 strlen(target->demangled_name));
+						 target->identity.demangled_name,
+						 strlen(target->identity.demangled_name));
 		}
 		__checksum_update_object(sym, sym_offset, "reloc addend",
 					 &offset, sizeof(offset));
diff --git a/tools/objtool/klp-diff.c b/tools/objtool/klp-diff.c
index 0391f91a8d31..cd5e683d6468 100644
--- a/tools/objtool/klp-diff.c
+++ b/tools/objtool/klp-diff.c
@@ -600,7 +600,7 @@ static struct symbol *find_twin(struct elfs *e, struct symbol *sym1)
 	struct symbol *sym2, *match = NULL;
 
 	/* Count orig candidates */
-	for_each_sym_by_demangled_name(e->orig, sym1->demangled_name, sym2) {
+	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)))
 			continue;
@@ -626,7 +626,7 @@ static struct symbol *find_twin(struct elfs *e, struct symbol *sym1)
 	}
 
 	/* Count patched candidates */
-	for_each_sym_by_demangled_name(e->patched, sym1->demangled_name, sym2) {
+	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;
@@ -766,7 +766,7 @@ static struct symbol *find_twin_suffixed(struct elf *elf, struct symbol *sym1)
 		return NULL;
 
 	if (snprintf_check(name, SYM_NAME_LEN, "%s%s",
-			   sym1->demangled_name, patched_suffix))
+			   sym1->identity.demangled_name, patched_suffix))
 		return NULL;
 
 	for_each_sym_by_name(elf, name, sym2) {
@@ -809,7 +809,7 @@ static struct symbol *find_twin_positional(struct elfs *e, struct symbol *sym1)
 	unsigned int sym1_pos = 0;
 	struct symbol *sym2, *match = NULL;
 
-	for_each_sym_by_demangled_name(e->orig, sym1->demangled_name, sym2) {
+	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))
 			continue;
@@ -821,7 +821,7 @@ static struct symbol *find_twin_positional(struct elfs *e, struct symbol *sym1)
 		idx_orig++;
 	}
 
-	for_each_sym_by_demangled_name(e->patched, sym1->demangled_name, sym2) {
+	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;
diff --git a/tools/objtool/klp-dwarf.c b/tools/objtool/klp-dwarf.c
new file mode 100644
index 000000000000..99b098ee7904
--- /dev/null
+++ b/tools/objtool/klp-dwarf.c
@@ -0,0 +1,1181 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+ *
+ * Compute DWARF identity information for ELF symbols by walking the DIE
+ * tree and following abstract_origin/specification references.
+ *
+ *   - source: physical translation unit (CU) of the definition/origin
+ *   - scope: slash-delimited chain of named enclosing scopes
+ */
+#define _GNU_SOURCE
+#include <ctype.h>
+#include <dwarf.h>
+#include <elfutils/libdwfl.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <linux/list.h>
+
+#include <objtool/elf.h>
+#include <objtool/klp-dwarf.h>
+#include <objtool/warn.h>
+
+#define IDENTITY_HASH_BITS 18
+#define IDENTITY_MAX_DEPTH 256
+
+struct cu_range {
+	unsigned int section;
+	unsigned long start, end;
+};
+
+struct identity_cu {
+	struct list_head list;
+	char *path;
+	struct cu_range *ranges;
+	unsigned int nr_ranges;
+	bool assembly;
+};
+
+/* A DIE may describe multiple addresses through ranges or pieces. */
+struct die_address {
+	struct elf_hash_node hash;
+	struct identity_die *die;
+	unsigned int section;
+	unsigned long offset;
+	/* Piece or shrunk storage size; zero for an ordinary location. */
+	Dwarf_Word piece_size;
+};
+
+struct identity_die {
+	struct hlist_node hash;
+	struct elf_hash_node name_hash;
+	Dwarf_Die die;
+	struct identity_cu *cu;
+	struct identity_die *parent, *definition;
+	void *abstract_origin, *specification;
+	const char *name, *source, *scope;
+	char *parent_scope;
+	struct die_address *addresses;
+	unsigned int nr_addresses;
+	unsigned char type;
+	bool declaration, external;
+};
+
+struct identity_reader {
+	struct elf *elf;
+	Dwfl_Module *module;
+	Dwarf_Addr bias;
+	struct list_head cus;
+	DECLARE_HASHTABLE(dies, IDENTITY_HASH_BITS);
+};
+
+static int die_error(Dwarf_Die *die, const char *message)
+{
+	ERROR("DWARF identity: DIE %#llx: %s", (unsigned long long)dwarf_dieoffset(die), message);
+	return -1;
+}
+
+static bool is_unit(int tag)
+{
+	return tag == DW_TAG_compile_unit || tag == DW_TAG_partial_unit || tag == DW_TAG_type_unit;
+}
+
+static bool is_symbol(int tag)
+{
+	return tag == DW_TAG_subprogram || tag == DW_TAG_variable || tag == DW_TAG_constant;
+}
+
+static bool is_scope(Dwarf_Die *die)
+{
+	int tag = dwarf_tag(die);
+
+	return is_unit(tag) || is_symbol(tag) || tag == DW_TAG_lexical_block ||
+	       tag == DW_TAG_namespace || tag == DW_TAG_class_type ||
+	       tag == DW_TAG_structure_type || tag == DW_TAG_union_type ||
+	       tag == DW_TAG_module ||
+	       (tag == DW_TAG_member && dwarf_hasattr(die, DW_AT_declaration));
+}
+
+static struct identity_die *find_die(struct identity_reader *reader, void *address)
+{
+	struct identity_die *entry;
+
+	hash_for_each_possible(reader->dies, entry, hash, (unsigned long)address)
+		if (entry->die.addr == address)
+			return entry;
+	return NULL;
+}
+
+/* Normalize source paths for identity comparison without filesystem access. */
+static void normalize_source(char *path)
+{
+	char *root = path + (*path == '/');
+	char *read = root, *write = root, *part, *previous;
+	size_t len;
+
+	while ((part = strsep(&read, "/"))) {
+		if (!*part || !strcmp(part, "."))
+			continue;
+		if (!strcmp(part, "..")) {
+			previous = write;
+			while (previous > root && previous[-1] != '/')
+				previous--;
+			if (previous < write &&
+			    !(write - previous == 2 && !memcmp(previous, "..", 2))) {
+				write = previous > root ? previous - 1 : root;
+				continue;
+			}
+			if (root != path)
+				continue;
+		}
+		len = strlen(part);
+		if (write > root)
+			*write++ = '/';
+		memmove(write, part, len);
+		write += len;
+	}
+	*write = '\0';
+}
+
+static int read_unit_source(struct identity_cu *cu, Dwarf_Die *die)
+{
+	Dwarf_Attribute attr;
+	const char *name = NULL, *dir = NULL;
+	char *path;
+
+	if (dwarf_tag(die) != DW_TAG_compile_unit)
+		return 0;
+	if (dwarf_attr(die, DW_AT_name, &attr)) {
+		name = dwarf_formstring(&attr);
+		if (!name)
+			return die_error(die, "invalid compilation unit name");
+	}
+	if (dwarf_attr(die, DW_AT_comp_dir, &attr)) {
+		dir = dwarf_formstring(&attr);
+		if (!dir)
+			return die_error(die, "invalid compilation directory");
+	}
+	if (!name || !*name || (*name != '/' && !dir))
+		return 0;
+	if (*name == '/')
+		path = strdup(name);
+	else if (asprintf(&path, "%s/%s", dir, name) < 0)
+		return -1;
+	if (!path)
+		return -1;
+	normalize_source(path);
+	cu->path = path;
+	return 0;
+}
+
+static int read_reference(Dwarf_Die *die, unsigned int attribute, void **address)
+{
+	Dwarf_Attribute attr;
+	Dwarf_Die reference;
+
+	if (!dwarf_attr(die, attribute, &attr))
+		return 0;
+	if (!dwarf_formref_die(&attr, &reference))
+		return die_error(die, "invalid declaration reference");
+	if (dwarf_cu_getdwarf(reference.cu) != dwarf_cu_getdwarf(die->cu))
+		return die_error(die, "references to supplementary DWARF files are unsupported");
+	*address = reference.addr;
+	return 0;
+}
+
+static int read_attributes(struct identity_die *entry)
+{
+	Dwarf_Die *die = &entry->die;
+	Dwarf_Attribute attr;
+	int tag = dwarf_tag(die);
+
+	if (dwarf_attr(die, DW_AT_name, &attr)) {
+		entry->name = dwarf_formstring(&attr);
+		if (!entry->name || (!is_unit(tag) && strchr(entry->name, '/')))
+			return die_error(die, "invalid or unsupported scope name");
+	}
+	if (!is_symbol(tag) && tag != DW_TAG_member)
+		return 0;
+	if (dwarf_attr(die, DW_AT_declaration, &attr)) {
+		if (dwarf_formflag(&attr, &entry->declaration))
+			return die_error(die, "invalid declaration flag");
+		if (entry->declaration &&
+		    (dwarf_hasattr(die, DW_AT_location) || dwarf_hasattr(die, DW_AT_low_pc)))
+			return die_error(die, "declaration has a concrete address");
+	}
+	if (tag == DW_TAG_member)
+		return 0;
+	if (read_reference(die, DW_AT_abstract_origin, &entry->abstract_origin) ||
+	    read_reference(die, DW_AT_specification, &entry->specification))
+		return -1;
+	if (entry->abstract_origin && entry->specification)
+		return die_error(die, "both origin and specification are unsupported");
+	return 0;
+}
+
+static int scan_attribute(Dwarf_Attribute *attr, void *unused)
+{
+	return DWARF_CB_OK;
+}
+
+static int index_dies(struct identity_reader *reader, struct identity_cu *cu,
+		      Dwarf_Die *die, struct identity_die *parent, unsigned int depth)
+{
+	struct identity_die *entry;
+	Dwarf_Die child;
+	int ret;
+
+	if (!dwarf_tag(die) || dwarf_getattrs(die, scan_attribute, NULL, 0) < 0)
+		return die_error(die, "invalid DIE attributes");
+	if (depth > IDENTITY_MAX_DEPTH)
+		return die_error(die, "DIE nesting exceeds the supported depth");
+	if (is_scope(die)) {
+		entry = calloc(1, sizeof(*entry));
+		if (!entry)
+			return -1;
+		entry->die = *die;
+		entry->cu = cu;
+		entry->parent = parent;
+		hash_add(reader->dies, &entry->hash, (unsigned long)die->addr);
+		if (read_attributes(entry))
+			return -1;
+		if (dwarf_tag(die) != DW_TAG_member)
+			parent = entry;
+	}
+	ret = dwarf_child(die, &child);
+	if (ret)
+		return ret < 0 ? die_error(die, "cannot read child") : 0;
+	do {
+		if (index_dies(reader, cu, &child, parent, depth + 1))
+			return -1;
+		ret = dwarf_siblingof(&child, &child);
+	} while (!ret);
+	return ret < 0 ? die_error(die, "cannot read sibling") : 0;
+}
+
+static inline bool is_assembly_cu(Dwarf_Die *die, const char *source)
+{
+	return dwarf_srclang(die) == DW_LANG_Mips_Assembler ||
+	       (source && (str_ends_with(source, ".S") || str_ends_with(source, ".s")));
+}
+
+static int collect_cu_ranges(struct identity_reader *reader,
+			     struct identity_cu *cu, Dwarf_Die *die);
+
+static struct identity_cu *read_cu(struct identity_reader *reader, Dwarf_Die *die)
+{
+	struct identity_die *entry;
+	struct identity_cu *cu;
+	Dwarf_Die root;
+
+	if (!dwarf_diecu(die, &root, NULL, NULL) || !is_unit(dwarf_tag(&root)))
+		return NULL;
+	entry = find_die(reader, root.addr);
+	if (entry)
+		return entry->cu;
+	cu = calloc(1, sizeof(*cu));
+	if (!cu)
+		return NULL;
+	if (read_unit_source(cu, &root) || index_dies(reader, cu, &root, NULL, 0))
+		return NULL;
+	cu->assembly = is_assembly_cu(&root, cu->path);
+	if (collect_cu_ranges(reader, cu, &root))
+		return NULL;
+	list_add_tail(&cu->list, &reader->cus);
+	return cu;
+}
+
+/* Follow abstract_origin/specification chains to find the name and defining DIE. */
+static int resolve_definition(struct identity_reader *reader, struct identity_die *entry)
+{
+	struct identity_die *cur = entry, *definition = NULL;
+	const char *name = NULL;
+	unsigned int depth = 0;
+	void *reference;
+
+	while (cur) {
+		if (++depth > IDENTITY_MAX_DEPTH)
+			return die_error(&entry->die,
+					 "declaration lookup exceeds the supported depth");
+		if (cur->name) {
+			if (name && strcmp(name, cur->name))
+				return die_error(&entry->die,
+						 "conflicting names in declaration chain");
+			name = cur->name;
+		}
+		if (!cur->declaration && !cur->abstract_origin) {
+			if (definition)
+				return die_error(&entry->die,
+						 "multiple definitions in declaration chain");
+			definition = cur;
+		}
+		reference = cur->abstract_origin ?: cur->specification;
+		if (!reference)
+			break;
+		cur = find_die(reader, reference);
+		if (!cur)
+			return die_error(&entry->die, "unavailable declaration reference");
+	}
+	entry->definition = definition;
+	entry->name = name;
+	return 0;
+}
+
+static int resolve_parent_scope(struct identity_die *entry, unsigned int depth)
+{
+	struct identity_die *parent = entry->parent;
+	int tag;
+
+	if (entry->parent_scope)
+		return 0;
+	if (depth > IDENTITY_MAX_DEPTH)
+		return die_error(&entry->die, "scope lookup exceeds the supported depth");
+	if (!parent || is_unit(dwarf_tag(&parent->die))) {
+		entry->parent_scope = strdup("/");
+	} else {
+		if (resolve_parent_scope(parent, depth + 1))
+			return -1;
+		tag = dwarf_tag(&parent->die);
+		if (parent->name && tag != DW_TAG_lexical_block) {
+			if (asprintf(&entry->parent_scope, "%s%s/",
+				     parent->parent_scope, parent->name) < 0)
+				return -1;
+		} else {
+			entry->parent_scope = strdup(parent->parent_scope);
+		}
+	}
+	return entry->parent_scope ? 0 : -1;
+}
+
+/* If completed by a DW_AT_specification, use the declaration's enclosing scope. */
+static int resolve_definition_scope(struct identity_reader *reader, struct identity_die *entry,
+				    const char **scope)
+{
+	struct identity_die *start = entry->definition;
+
+	if (start->specification) {
+		start = find_die(reader, start->specification);
+		if (!start)
+			return die_error(&entry->die, "unavailable declaration scope");
+	}
+	if (resolve_parent_scope(start, 0))
+		return -1;
+	*scope = start->parent_scope;
+	return 0;
+}
+
+static const char *resolve_definition_source(struct identity_die *entry)
+{
+	return entry->definition->cu->path;
+}
+
+/* Translate libdwfl's relocated address back to objtool's section/offset pair. */
+static int address_section(struct identity_reader *reader, Dwarf_Addr *address,
+			   unsigned int *section)
+{
+	struct elf *elf = reader->elf;
+	struct section *sec;
+	Dwarf_Addr absolute = *address, bias;
+	GElf_Shdr sh;
+	Elf *file;
+	GElf_Word index;
+	size_t nr, i;
+	int base;
+
+	base = dwfl_module_relocate_address(reader->module, address);
+	if (base < 0)
+		return 1;
+	if (!dwfl_module_relocation_info(reader->module, base, &index))
+		return -1;
+	if (sec_size(&elf->section_data[index])) {
+		*section = index;
+		return 0;
+	}
+	/* Empty sections can share the following nonempty section's synthetic address. */
+	file = dwfl_module_getelf(reader->module, &bias);
+	if (!file || elf_getshdrnum(file, &nr))
+		return -1;
+	for (i = 1; i <= nr; i++) {
+		sec = &elf->section_data[(index + i) % nr];
+		if (!(sec->sh.sh_flags & SHF_ALLOC) || !sec_size(sec))
+			continue;
+		if (!gelf_getshdr(elf_getscn(file, sec->idx), &sh))
+			return -1;
+		if (absolute >= sh.sh_addr + bias && absolute - sh.sh_addr - bias < sh.sh_size) {
+			*section = sec->idx;
+			*address = absolute - sh.sh_addr - bias;
+			return 0;
+		}
+	}
+	return 1;
+}
+
+static int collect_cu_ranges(struct identity_reader *reader,
+			     struct identity_cu *cu, Dwarf_Die *die)
+{
+	struct cu_range *ranges, *range;
+	Dwarf_Addr base, start, end, first, last;
+	unsigned int first_section, last_section;
+	ptrdiff_t off = 0;
+	int ret;
+
+	while ((off = dwarf_ranges(die, off, &base, &start, &end)) > 0) {
+		if (start == end)
+			continue;
+		if (end < start)
+			return die_error(die, "invalid CU range");
+		first = start + reader->bias;
+		last = end - 1 + reader->bias;
+		ret = address_section(reader, &first, &first_section);
+		if (ret < 0)
+			return -1;
+		if (ret)
+			continue;
+		ret = address_section(reader, &last, &last_section);
+		if (ret < 0)
+			return -1;
+		if (ret || first_section != last_section)
+			continue;
+		ranges = realloc(cu->ranges, (cu->nr_ranges + 1) * sizeof(*ranges));
+		if (!ranges)
+			return -1;
+		cu->ranges = ranges;
+		range = &ranges[cu->nr_ranges++];
+		range->section = first_section;
+		range->start = first;
+		range->end = last + 1;
+	}
+	return off < 0 ? die_error(die, "invalid CU ranges") : 0;
+}
+
+static int add_address(struct identity_reader *reader, struct identity_die *entry,
+		       Dwarf_Addr address, Dwarf_Word piece_size)
+{
+	struct die_address *addresses, *location;
+	Dwarf_Addr offset = address;
+	unsigned int section;
+	int ret = address_section(reader, &offset, &section);
+
+	if (ret)
+		return ret < 0 ? -1 : 0;
+	addresses = realloc(entry->addresses, (entry->nr_addresses + 1) * sizeof(*addresses));
+	if (!addresses)
+		return -1;
+	entry->addresses = addresses;
+	location = &addresses[entry->nr_addresses++];
+	memset(location, 0, sizeof(*location));
+	location->die = entry;
+	location->section = section;
+	location->offset = offset;
+	location->piece_size = piece_size;
+	return 0;
+}
+
+/*
+ * The empty features[] in virtio_input.c can share an address with the
+ * nonempty features[] in trans_virtio.c. Exclude the empty object's DIE
+ * from address lookup. libdw rejects GCC's upper_bound=-1 encoding, so
+ * check array bounds too.
+ */
+static bool is_empty_type(Dwarf_Die *type)
+{
+	Dwarf_Die array, range;
+	Dwarf_Attribute attr;
+	Dwarf_Word size;
+	Dwarf_Sword lower, upper;
+
+	if (!dwarf_aggregate_size(type, &size))
+		return !size;
+	if (dwarf_peel_type(type, &array) || dwarf_tag(&array) != DW_TAG_array_type ||
+	    dwarf_child(&array, &range))
+		return false;
+	do {
+		if (dwarf_tag(&range) != DW_TAG_subrange_type)
+			continue;
+		if (dwarf_attr(&range, DW_AT_count, &attr) &&
+		    !dwarf_formudata(&attr, &size) && !size)
+			return true;
+		lower = 0;
+		if (dwarf_attr(&range, DW_AT_lower_bound, &attr) &&
+		    dwarf_formsdata(&attr, &lower))
+			continue;
+		if (dwarf_attr(&range, DW_AT_upper_bound, &attr) &&
+		    !dwarf_formsdata(&attr, &upper) && upper < lower)
+			return true;
+	} while (!dwarf_siblingof(&range, &range));
+	return false;
+}
+
+/*
+ * Optimizers can encode a variable's two values in one byte. The location
+ * expression reads that byte and reconstructs the source value.
+ */
+static bool is_shrunk_location(const Dwarf_Op *ops, size_t count)
+{
+	size_t i, depth = 1;
+
+	if (count < 3 || ops[1].atom != DW_OP_deref_size || ops[1].number != 1 ||
+	    ops[count - 1].atom != DW_OP_stack_value)
+		return false;
+	if (ops[0].atom != DW_OP_addr && ops[0].atom != DW_OP_addrx &&
+	    ops[0].atom != DW_OP_GNU_addr_index)
+		return false;
+
+	/* After the byte load, allow only constants and arithmetic. */
+	for (i = 2; i + 1 < count; i++) {
+		unsigned int op = ops[i].atom;
+
+		if ((op >= DW_OP_lit0 && op <= DW_OP_lit31) ||
+		    (op >= DW_OP_const1u && op <= DW_OP_consts)) {
+			depth++;
+			continue;
+		}
+		switch (op) {
+		case DW_OP_nop:
+		case DW_OP_abs:
+		case DW_OP_neg:
+		case DW_OP_not:
+		case DW_OP_plus_uconst:
+			break;
+		case DW_OP_plus:
+		case DW_OP_minus:
+		case DW_OP_mul:
+		case DW_OP_div:
+		case DW_OP_mod:
+		case DW_OP_and:
+		case DW_OP_or:
+		case DW_OP_xor:
+		case DW_OP_shl:
+		case DW_OP_shr:
+		case DW_OP_shra:
+		case DW_OP_eq:
+		case DW_OP_ge:
+		case DW_OP_gt:
+		case DW_OP_le:
+		case DW_OP_lt:
+		case DW_OP_ne:
+			if (depth < 2)
+				return false;
+			depth--;
+			break;
+		default:
+			return false;
+		}
+	}
+	return depth == 1;
+}
+
+/*
+ * Limit location decoding to fixed addresses. Other location descriptions
+ * rely on name lookup to keep the reader manageable.
+ */
+static int collect_location(struct identity_reader *reader, struct identity_die *entry,
+			    Dwarf_Attribute *attr, Dwarf_Op *ops, size_t count,
+			    Dwarf_Word piece_size)
+{
+	Dwarf_Attribute address_attr;
+	Dwarf_Addr address;
+	size_t i;
+
+	for (i = 0; i < count; i++)
+		if (ops[i].atom == DW_OP_piece || ops[i].atom == DW_OP_bit_piece)
+			return die_error(&entry->die, "unterminated location piece");
+	/* The decoded piece can be wider than its physical storage. */
+	if (is_shrunk_location(ops, count))
+		piece_size = ops[1].number;
+	else if (count != 1)
+		return 0;
+	switch (ops[0].atom) {
+	case DW_OP_addr:
+		address = ops[0].number;
+		break;
+	case DW_OP_addrx:
+	case DW_OP_GNU_addr_index:
+		if (dwarf_getlocation_attr(attr, ops, &address_attr) ||
+		    dwarf_formaddr(&address_attr, &address))
+			return die_error(&entry->die, "invalid location address");
+		break;
+	default:
+		/* Values and locations requiring runtime state are not addresses. */
+		return 0;
+	}
+	return add_address(reader, entry, address + reader->bias, piece_size);
+}
+
+static int collect_pieces(struct identity_reader *reader, struct identity_die *entry,
+			  Dwarf_Attribute *attr, Dwarf_Op *ops, size_t count)
+{
+	size_t first = 0, i;
+
+	for (i = 0; i < count; i++) {
+		if (ops[i].atom != DW_OP_piece && ops[i].atom != DW_OP_bit_piece)
+			continue;
+		/* Index only nonempty, byte-sized storage pieces. */
+		if (ops[i].atom == DW_OP_piece && ops[i].number &&
+		    collect_location(reader, entry, attr, ops + first, i - first, ops[i].number))
+			return -1;
+		first = i + 1;
+	}
+	return 0;
+}
+
+static int resolve_identity(struct identity_reader *reader, struct identity_die *entry)
+{
+	Dwarf_Attribute attr;
+	int tag = dwarf_tag(&entry->die);
+
+	if (!entry->definition || !is_symbol(tag))
+		return 0;
+	if (dwarf_attr_integrate(&entry->die, DW_AT_external, &attr) &&
+	    dwarf_formflag(&attr, &entry->external))
+		return die_error(&entry->die, "invalid external flag");
+	entry->source = resolve_definition_source(entry);
+	entry->type = tag == DW_TAG_subprogram ? STT_FUNC : STT_OBJECT;
+	return resolve_definition_scope(reader, entry, &entry->scope);
+}
+
+static int collect_addresses(struct identity_reader *reader, struct identity_die *entry)
+{
+	Dwarf_Die *die = &entry->die, type;
+	Dwarf_Attribute attr, address_attr;
+	Dwarf_Addr base, start, end;
+	Dwarf_Op *ops;
+	size_t count;
+	ptrdiff_t off = 0;
+	int tag = dwarf_tag(die);
+
+	if (!entry->definition || (tag != DW_TAG_subprogram && tag != DW_TAG_variable))
+		return 0;
+	if (tag == DW_TAG_subprogram) {
+		if (!dwarf_hasattr(die, DW_AT_low_pc) && !dwarf_hasattr(die, DW_AT_ranges))
+			return 0;
+		while ((off = dwarf_ranges(die, off, &base, &start, &end)) > 0)
+			if (start != end && add_address(reader, entry, start + reader->bias, 0))
+				return -1;
+		return off < 0 ? die_error(die, "invalid code ranges") : 0;
+	}
+	if (!dwarf_attr(die, DW_AT_location, &attr))
+		return 0;
+	/* Location lists may vary with the PC; index only fixed storage. */
+	if (dwarf_whatform(&attr) == DW_FORM_sec_offset ||
+	    dwarf_whatform(&attr) == DW_FORM_loclistx)
+		return 0;
+	if (dwarf_getlocation(&attr, &ops, &count))
+		return die_error(die, "invalid location expression");
+	if (!count)
+		return 0;
+
+	if (dwarf_attr_integrate(die, DW_AT_type, &address_attr)) {
+		if (!dwarf_formref_die(&address_attr, &type))
+			return die_error(die, "invalid variable type reference");
+		if (is_empty_type(&type))
+			return 0;
+	}
+	if (ops[count - 1].atom == DW_OP_piece || ops[count - 1].atom == DW_OP_bit_piece)
+		return collect_pieces(reader, entry, &attr, ops, count);
+	return collect_location(reader, entry, &attr, ops, count, 0);
+}
+
+/*
+ * libdwfl permits partial relocation, leaving unapplied entries in place.
+ * Check only sections read for identities; unused indexes may retain relocations.
+ */
+static int check_relocations(Dwarf *dwarf)
+{
+	static const char * const sections[] = {
+		".debug_info", ".debug_types", ".debug_abbrev",
+		".debug_str", ".debug_line_str", ".debug_str_offsets",
+		".debug_addr", ".debug_ranges", ".debug_rnglists",
+	};
+	Elf *elf = dwarf_getelf(dwarf);
+	Elf_Scn *scn = NULL;
+	GElf_Shdr sh, target;
+	const char *name;
+	size_t strings, i;
+
+	if (!elf || elf_getshdrstrndx(elf, &strings))
+		return -1;
+	while ((scn = elf_nextscn(elf, scn))) {
+		if (!gelf_getshdr(scn, &sh))
+			return -1;
+		if (!sh.sh_size || (sh.sh_type != SHT_REL && sh.sh_type != SHT_RELA))
+			continue;
+		if (!gelf_getshdr(elf_getscn(elf, sh.sh_info), &target))
+			return -1;
+		name = elf_strptr(elf, strings, target.sh_name);
+		if (!name)
+			return -1;
+		for (i = 0; i < ARRAY_SIZE(sections); i++)
+			if (!strcmp(name, sections[i])) {
+				ERROR("DWARF identity: unapplied relocations for %s", name);
+				return -1;
+			}
+	}
+	return 0;
+}
+
+static int index_identities(struct identity_reader *reader)
+{
+	struct elf *elf = reader->elf;
+	struct identity_die *entry;
+	struct die_address *address;
+	unsigned int bucket, i;
+
+	hash_for_each(reader->dies, bucket, entry, hash) {
+		if (!entry->scope)
+			continue;
+		/* Names must outlive libdw; source and scope are already owned. */
+		if (entry->name) {
+			entry->name = strdup(entry->name);
+			if (!entry->name)
+				return -1;
+		}
+		/* Build the address index after the arrays have stopped growing. */
+		for (i = 0; i < entry->nr_addresses; i++) {
+			address = &entry->addresses[i];
+			elf_hash_add(dwarf_address, &address->hash,
+				     address->offset ^ address->section);
+		}
+		/*
+		 * Index named definitions, not instances referring to them.
+		 * External definitions do not require a source path.
+		 */
+		if (entry->definition == entry && entry->name && (entry->source || entry->external))
+			elf_hash_add(dwarf_name, &entry->name_hash, str_hash(entry->name));
+	}
+	return 0;
+}
+
+static int build_identities(struct identity_reader *reader)
+{
+	static const Dwfl_Callbacks callbacks = { .section_address = dwfl_offline_section_address };
+	Dwfl *dwfl = dwfl_begin(&callbacks);
+	Dwarf_CU *cu = NULL, *next;
+	Dwarf_Die die;
+	Dwarf *dwarf;
+	struct identity_die *entry;
+	unsigned int bucket;
+	uint8_t unit_type;
+	int ret = -1;
+
+	if (!dwfl)
+		return -1;
+	reader->module = dwfl_report_offline(dwfl, reader->elf->name, reader->elf->name, -1);
+	if (!reader->module || dwfl_report_end(dwfl, NULL, NULL))
+		goto out;
+	dwarf = dwfl_module_getdwarf(reader->module, &reader->bias);
+	if (!dwarf || check_relocations(dwarf))
+		goto out;
+	while (!(ret = dwarf_get_units(dwarf, cu, &next, NULL, &unit_type, &die, NULL))) {
+		if (unit_type == DW_UT_skeleton ||
+		    unit_type == DW_UT_split_compile || unit_type == DW_UT_split_type) {
+			ret = die_error(&die, "split DWARF is not supported yet");
+			goto out;
+		}
+		if (!is_unit(dwarf_tag(&die))) {
+			ret = die_error(&die, "invalid unit root");
+			goto out;
+		}
+		if (!read_cu(reader, &die)) {
+			ret = -1;
+			goto out;
+		}
+		cu = next;
+	}
+	if (ret < 0)
+		goto out;
+	ret = -1;
+	hash_for_each(reader->dies, bucket, entry, hash)
+		if (is_symbol(dwarf_tag(&entry->die)) && resolve_definition(reader, entry))
+			goto out;
+	hash_for_each(reader->dies, bucket, entry, hash)
+		if (resolve_identity(reader, entry) || collect_addresses(reader, entry))
+			goto out;
+	ret = index_identities(reader);
+out:
+	dwfl_end(dwfl);
+	return ret;
+}
+
+int init_identity_cache(struct elf *elf)
+{
+	struct identity_reader *reader;
+	const char *saved_objname = objname;
+	int ret;
+
+	if (elf->dwarf_address_hash)
+		return 0;
+
+	if (elf->ehdr.e_type != ET_REL) {
+		ERROR("%s: DWARF identity requires an ET_REL object", elf->name);
+		return -1;
+	}
+	if (!find_section_by_name(elf, ".debug_info")) {
+		ERROR("%s: missing DWARF debug information; build with -g", elf->name);
+		return -1;
+	}
+	reader = calloc(1, sizeof(*reader));
+	if (!reader)
+		return -1;
+	if (!elf_alloc_hash(dwarf_address, 1UL << IDENTITY_HASH_BITS) ||
+	    !elf_alloc_hash(dwarf_name, 1UL << IDENTITY_HASH_BITS))
+		return -1;
+	reader->elf = elf;
+	INIT_LIST_HEAD(&reader->cus);
+	elf->identity_reader = reader;
+	objname = elf->name;
+	ret = build_identities(reader);
+	if (ret)
+		ERROR("DWARF identity: input cannot be parsed");
+	objname = saved_objname;
+	return ret;
+}
+
+void free_identity_cache(struct elf *elf)
+{
+	struct identity_reader *reader = elf->identity_reader;
+	struct identity_die *entry;
+	struct identity_cu *cu, *next;
+	struct hlist_node *tmp;
+	unsigned int bucket;
+
+	hash_for_each_safe(reader->dies, bucket, tmp, entry, hash) {
+		if (entry->scope)
+			free((char *)entry->name);
+		free(entry->parent_scope);
+		free(entry->addresses);
+		free(entry);
+	}
+	list_for_each_entry_safe(cu, next, &reader->cus, list) {
+		free(cu->path);
+		free(cu->ranges);
+		free(cu);
+	}
+	munmap(elf->dwarf_address_hash,
+	       sizeof(*elf->dwarf_address_hash) << elf->dwarf_address_bits);
+	munmap(elf->dwarf_name_hash, sizeof(*elf->dwarf_name_hash) << elf->dwarf_name_bits);
+	free(reader);
+	elf->dwarf_address_hash = NULL;
+	elf->dwarf_name_hash = NULL;
+	elf->identity_reader = NULL;
+}
+
+/* Match DW_AT_name verbatim; accept compiler suffixes only on the ELF name. */
+static bool matches_name(const char *name, const char *identifier)
+{
+	size_t len = strlen(identifier);
+
+	if (strncmp(name, identifier, len))
+		return false;
+	name += len;
+	while (*name) {
+		if (strstarts(name, ".llvm."))
+			name += 5;
+		if (*name++ != '.' || !isdigit((unsigned char)*name))
+			return false;
+		while (isdigit((unsigned char)*name))
+			name++;
+	}
+	return true;
+}
+
+static bool matches_dwarf_name(struct identity_die *entry, const char *name)
+{
+	const char *scope;
+
+	if (!entry->name)
+		return false;
+	if (matches_name(name, entry->name))
+		return true;
+	/*
+	 * Clang names function-local statics as func.var[.suffix]. Constant merging
+	 * can leave one such ELF symbol for several variable DIEs, as with oldpiix's
+	 * timings tables. Match the encoded function scope as well as the variable.
+	 */
+	for (scope = entry->scope + 1; *scope; scope++, name++)
+		if (*name != (*scope == '/' ? '.' : *scope))
+			return false;
+	return matches_name(name, entry->name);
+}
+
+static bool address_matches_symbol(struct die_address *entry, struct symbol *sym)
+{
+	/* Assembly code labels can be STT_NOTYPE when no .type is emitted. */
+	if (sym->type == STT_NOTYPE && !entry->die->cu->assembly)
+		return false;
+	if (entry->section != sym->sec->idx || entry->offset != sym->offset)
+		return false;
+	/* The recorded storage must cover the whole ELF symbol. */
+	return !entry->piece_size || sym->len <= entry->piece_size;
+}
+
+static bool known_file_name_mismatch(const char *file, const char *source)
+{
+	const char *file_ext, *source_ext;
+
+	/* Full LTO combines TUs under one synthetic FILE name. */
+	if (!strcmp(file, "ld-temp.o"))
+		return true;
+
+	/* objcopy prefixes startup FILE names without changing their DWARF CUs. */
+	if (strstarts(file, "__pi_") && !strcmp(file + 5, source))
+		return true;
+
+	/* Ignore extension differences, e.g. foo.o versus foo.S. */
+	file_ext = strrchr(file, '.');
+	source_ext = strrchr(source, '.');
+	if (!file_ext || !source_ext)
+		return false;
+	return file_ext - file == source_ext - source &&
+	       !strncmp(file, source, file_ext - file);
+}
+
+/* Fill STT_FILE paths from DWARF, never the reverse. */
+static int complete_file_source(struct symbol *sym, const char *source, bool assembly)
+{
+	struct symbol *file = sym->file;
+	const char *file_name, *source_name;
+
+	if (!file || !*source)
+		return 0;
+
+	file_name = basename(file->name);
+	source_name = basename(source);
+	if (strcmp(file_name, source_name)) {
+		/* Assembly CUs can name an included file instead of the input. */
+		if (assembly) {
+			dbg_correlate("FILE %s disagrees with assembly CU %s for %s; "
+				      "skipping source completion",
+				      file->name, source, sym->name);
+			return 0;
+		}
+		if (!known_file_name_mismatch(file_name, source_name) && !file->warned) {
+			WARN("FILE %s symtab[%u] disagrees with DWARF source %s for %s; skipping source completion",
+			     file->name, file->idx, source, sym->name);
+			file->warned = true;
+		}
+		return 0;
+	}
+
+	/* Symbols sharing an STT_FILE must agree on the CU path. */
+	if (file->identity.source != file->name) {
+		if (strcmp(file->identity.source, source)) {
+			ERROR("FILE %s symtab[%u] has conflicting DWARF sources: %s and %s",
+			      file->name, file->idx, file->identity.source, source);
+			return -1;
+		}
+		return 0;
+	}
+	file->identity.source = strdup(source);
+	if (!file->identity.source)
+		return -1;
+	return 0;
+}
+
+/*
+ * Omit source for external definitions which retain global or weak binding.
+ * They can move between CUs, and DWARF relocations for overridden weak
+ * definitions can bind to the selected definition:
+ * https://sourceware.org/bugzilla/show_bug.cgi?id=34741
+ *
+ * Local symbols keep their CU, including foo.cold and foo.part.0 which
+ * inherit DW_AT_external from their source definition.
+ */
+static bool needs_identity_source(struct symbol *sym, bool external)
+{
+	return !external || (!is_global_sym(sym) && !is_weak_sym(sym));
+}
+
+static void get_die_identity(struct identity_die *entry, struct symbol *sym,
+			     struct identity *identity)
+{
+	const char *source = entry->source;
+
+	identity->scope = entry->scope;
+	identity->external = entry->external;
+	if (entry->cu->assembly) {
+		source = entry->cu->path;
+		identity->scope = "/";
+		/* Assembly linkage is not subject to LTO promotion/internalization. */
+		identity->external = sym->bind != STB_LOCAL;
+	}
+	identity->source = needs_identity_source(sym, identity->external) ? source : "";
+}
+
+static bool cu_contains_symbol(struct identity_cu *cu, struct symbol *sym)
+{
+	struct cu_range *range;
+	unsigned int i;
+
+	for (i = 0; i < cu->nr_ranges; i++) {
+		range = &cu->ranges[i];
+		if (range->section == sym->sec->idx && sym->offset >= range->start &&
+		    sym->offset < range->end && sym->len <= range->end - sym->offset)
+			return true;
+	}
+	return false;
+}
+
+static int check_if_assembly(struct elf *elf, struct symbol *sym, struct identity_cu **matched_cu)
+{
+	struct identity_cu *cu, *match = NULL;
+
+	if (!(sym->sec->sh.sh_flags & SHF_EXECINSTR) ||
+	    (!is_func_sym(sym) && sym->type != STT_NOTYPE))
+		return 1;
+	list_for_each_entry(cu, &elf->identity_reader->cus, list) {
+		if (!cu->assembly || !cu->path || !cu_contains_symbol(cu, sym))
+			continue;
+		if (match && strcmp(match->path, cu->path)) {
+			ERROR("%s: conflicting assembly CUs for %s: %s and %s",
+			      elf->name, sym->name, match->path, cu->path);
+			return -1;
+		}
+		match = cu;
+	}
+	if (!match)
+		return 1;
+	*matched_cu = match;
+	sym->identity.scope = "/";
+	sym->identity.external = sym->bind != STB_LOCAL;
+	sym->identity.source = needs_identity_source(sym, sym->identity.external) ?
+			       match->path : "";
+	return 0;
+}
+
+/* GCC may omit address attributes for IPA-ICF wrappers such as msr_event_del. */
+static int fallback_by_name(struct elf *elf, struct symbol *sym, struct identity_cu **matched_cu)
+{
+	const char *name = sym->pfunc->identity.demangled_name;
+	const char *file = sym->file ? basename(sym->file->name) : NULL;
+	struct identity_die *entry, *match = NULL;
+	struct identity candidate, matched;
+
+	if (!is_func_sym(sym) && !(is_object_sym(sym) && is_rodata_sec(sym->sec)))
+		return 1;
+	elf_hash_for_each_possible(elf, dwarf_name, entry, name_hash, str_hash(name)) {
+		if (entry->type != sym->type || strcmp(name, entry->name))
+			continue;
+		/* Different CUs can have the same FILE basename and function name. */
+		if (is_func_sym(sym) && !cu_contains_symbol(entry->cu, sym))
+			continue;
+		get_die_identity(entry, sym, &candidate);
+		if (!candidate.source)
+			return 1;
+		if (!entry->cu->assembly && !candidate.external && file &&
+		    strcmp(file, basename(candidate.source)) &&
+		    !known_file_name_mismatch(file, basename(candidate.source)))
+			continue;
+		if (match) {
+			get_die_identity(match, sym, &matched);
+			if (strcmp(matched.source, candidate.source) ||
+			    strcmp(matched.scope, candidate.scope) ||
+			    matched.external != candidate.external)
+				return 1;
+		}
+		match = entry;
+	}
+	if (!match)
+		return 1;
+	*matched_cu = match->cu;
+	get_die_identity(match, sym, &sym->identity);
+	return 0;
+}
+
+int get_identity_for_symbol(struct elf *elf, struct symbol *sym)
+{
+	struct identity *identity = &sym->identity, candidate;
+	struct section *sec = sym->sec;
+	struct identity_cu *cu = NULL;
+	struct die_address *address;
+	struct identity_die *entry;
+	unsigned int nr = 0, nr_both_matched = 0;
+	bool source_unavailable = false;
+	const char *error, *saved_objname = objname;
+
+	identity->source = "";
+	identity->scope = NULL;
+	identity->external = false;
+	elf_hash_for_each_possible(elf, dwarf_address, address, hash, sym->offset ^ sec->idx) {
+		if (!address_matches_symbol(address, sym))
+			continue;
+		entry = address->die;
+		nr++;
+		if (matches_dwarf_name(entry, sym->name))
+			nr_both_matched++;
+	}
+	if (!nr) {
+		int ret = fallback_by_name(elf, sym, &cu);
+
+		if (ret == 1)
+			ret = check_if_assembly(elf, sym, &cu);
+		if (ret)
+			return ret;
+		goto found;
+	}
+
+	/*
+	 * ICF/constant merging can leave several definitions after name
+	 * filtering. FIXME: Folded objects can remain uncorrelated when
+	 * their checksums change.
+	 */
+	if (nr_both_matched > 1 && strstarts(sec->name, ".rodata"))
+		return 1;
+
+	/*
+	 * Prefer address and name. Compiler clones, syscall/weak aliases and
+	 * objcopy's __pi_ prefix can differ from DW_AT_name. If no name matches,
+	 * accept address candidates only when their identities agree below.
+	 */
+	objname = elf->name;
+	elf_hash_for_each_possible(elf, dwarf_address, address, hash, sym->offset ^ sec->idx) {
+		if (!address_matches_symbol(address, sym))
+			continue;
+		entry = address->die;
+		if (nr_both_matched && !matches_dwarf_name(entry, sym->name))
+			continue;
+		get_die_identity(entry, sym, &candidate);
+		error = "conflicting declarations";
+		if (identity->scope && strcmp(identity->scope, candidate.scope))
+			goto failed;
+		error = "conflicting external declarations";
+		if (identity->scope && identity->external != candidate.external)
+			goto failed;
+		/* The CU may omit its source path. */
+		if (!candidate.source) {
+			source_unavailable = true;
+		} else {
+			error = "conflicting source CUs";
+			if (*identity->source && strcmp(identity->source, candidate.source))
+				goto failed;
+			identity->source = candidate.source;
+		}
+		identity->scope = candidate.scope;
+		identity->external = candidate.external;
+		cu = entry->cu;
+	}
+	if (source_unavailable) {
+		identity->source = "";
+		identity->scope = NULL;
+	}
+	objname = saved_objname;
+	if (!identity->scope)
+		return 1;
+found:
+	if (complete_file_source(sym, identity->source, cu->assembly))
+		return -1;
+	/* Symbol identities outlive the DWARF lookup indexes. */
+	identity->source = strdup(identity->source);
+	identity->scope = strdup(identity->scope);
+	if (!identity->source || !identity->scope)
+		return -1;
+	return 0;
+failed:
+	ERROR("DWARF identity: %s at %s+%#lx", error, sec->name, sym->offset);
+	identity->source = "";
+	identity->scope = NULL;
+	identity->external = false;
+	objname = saved_objname;
+	return -1;
+}
diff --git a/tools/objtool/klp-identity.c b/tools/objtool/klp-identity.c
new file mode 100644
index 000000000000..4b777571c715
--- /dev/null
+++ b/tools/objtool/klp-identity.c
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+ */
+#define _GNU_SOURCE
+#include <string.h>
+
+#include <linux/jhash.h>
+
+#include <objtool/elf.h>
+#include <objtool/klp-dwarf.h>
+#include <objtool/klp-identity.h>
+#include <objtool/warn.h>
+
+static bool is_dwarf_identity_candidate(struct symbol *sym)
+{
+	return !sym->dont_correlate && sym->len && !is_undef_sym(sym) &&
+	       (is_func_sym(sym) || is_object_sym(sym) ||
+		(sym->type == STT_NOTYPE && (sym->sec->sh.sh_flags & SHF_EXECINSTR)));
+}
+
+u32 identity_hash(const struct identity *identity)
+{
+	return jhash_3words(str_hash(identity->source), str_hash(identity->scope),
+			   str_hash(identity->demangled_name), 0);
+}
+
+bool same_identity(const struct identity *a, const struct identity *b)
+{
+	return !strcmp(a->source, b->source) &&
+	       !strcmp(a->scope, b->scope) &&
+	       !strcmp(a->demangled_name, b->demangled_name);
+}
+
+/* Only accept identities from DWARF for uniqueness validation. */
+static int validate_dwarf_identity(struct elf *elf, struct symbol *sym)
+{
+	struct symbol *other;
+	u32 key = identity_hash(&sym->identity);
+
+	elf_hash_for_each_possible(elf, symbol_identity, other, identity_hash, key) {
+		if (same_identity(&sym->identity, &other->identity) &&
+		    !strcmp(sym->name, other->name)) {
+			ERROR("%s: duplicate symbol identity (%s, %s, %s): symtab[%u] %s+%#lx (%u bytes) and symtab[%u] %s+%#lx (%u bytes)",
+			      elf->name, sym->identity.source, sym->identity.scope, sym->name,
+			      other->idx, other->sec->name, other->offset, other->len,
+			      sym->idx, sym->sec->name, sym->offset, sym->len);
+			return -1;
+		}
+	}
+	elf_hash_add(symbol_identity, &sym->identity_hash, key);
+	return 0;
+}
+
+int prepare_identities(struct elf *elf)
+{
+	struct symbol *sym;
+	int ret;
+
+	if (init_identity_cache(elf) ||
+	    !elf_alloc_hash(symbol_identity, 1UL << elf->symbol_bits))
+		return -1;
+
+	for_each_sym(elf, sym) {
+		struct identity *identity = &sym->identity;
+
+		/* complete_file_source() replaces this FILE name with its DWARF path. */
+		identity->source = is_file_sym(sym) ? sym->name : "";
+		if (!is_dwarf_identity_candidate(sym))
+			continue;
+		ret = get_identity_for_symbol(elf, sym);
+		if (ret < 0)
+			return -1;
+	}
+
+	for_each_sym(elf, sym) {
+		struct identity *identity = &sym->identity;
+		char *path;
+
+		if (!has_dwarf_identity(sym))
+			continue;
+		if (asprintf(&path, "%s%s%s", identity->source, identity->scope,
+			     identity->demangled_name) < 0) {
+			ERROR_GLIBC("asprintf");
+			return -1;
+		}
+		identity->path = path;
+		if (validate_dwarf_identity(elf, sym))
+			return -1;
+	}
+
+	free_identity_cache(elf);
+	return 0;
+}
diff --git a/tools/objtool/tests/lib.sh b/tools/objtool/tests/lib.sh
index 4da168d0ddca..8e1aa7614dc6 100644
--- a/tools/objtool/tests/lib.sh
+++ b/tools/objtool/tests/lib.sh
@@ -57,7 +57,7 @@ klp_preflight()
 	OBJTOOL="$(realpath "$OBJTOOL")"
 
 	"$OBJTOOL" klp 2>&1 | grep -q checksum ||
-		bail "objtool was built without klp support; install libxxhash (>= 0.8) and rebuild"
+		bail "objtool was built without klp support; install libxxhash (>= 0.8) and libdw development files and rebuild"
 
 	command -v "${CC%% *}" >/dev/null || bail "compiler not found: $CC"
 
-- 
2.55.0


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [RFC PATCH v2 05/11] objtool/klp: Support reduced and split DWARF
  2026-11-04 13:31 [RFC PATCH v2 00/11] objtool/klp: DWARF-based cross-build symbol correlation Longjun Luo
                   ` (3 preceding siblings ...)
  2026-11-04 13:31 ` [RFC PATCH v2 04/11] objtool/klp: Read symbol identities from DWARF Longjun Luo
@ 2026-11-04 13:31 ` Longjun Luo
  2026-11-04 13:31 ` [RFC PATCH v2 06/11] klp-build: Require CONFIG_DEBUG_INFO Longjun Luo
                   ` (5 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Longjun Luo @ 2026-11-04 13:31 UTC (permalink / raw)
  To: jpoimboe, peterz
  Cc: mbenes, pmladek, joe.lawrence, song, puranjay, nathan,
	ndesaulniers, morbo, justinstitt, live-patching, llvm,
	linux-kernel, Longjun Luo

Read split CUs through libdw and save matching DWO files in a DWP before
another build overwrites them. Keep the DWP beside each saved object.
Reading saved DWP files requires libdw 0.191 or newer.

Read fixed-width array bounds as unsigned so reduced type information
does not make nonempty arrays appear empty.

Full LTO with --plugin-opt=dwo_dir requires
-mllvm -split-dwarf-cross-cu-references=true to preserve separate CUs
and their source paths. Kbuild does not enable this DWO output mode.

Validated the available objtool tests with GCC 16.2.1 and Clang
22.1.8. No failures.

Assisted-by: LLM
Signed-off-by: Longjun Luo <luolongjuna@gmail.com>
---
 scripts/livepatch/klp-build | 18 +++++++++++++++
 tools/objtool/klp-dwarf.c   | 45 ++++++++++++++++++++++++++++---------
 2 files changed, 53 insertions(+), 10 deletions(-)

diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build
index e373928123e8..95bea5b2057e 100755
--- a/scripts/livepatch/klp-build
+++ b/scripts/livepatch/klp-build
@@ -34,6 +34,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 FIX_PATCH_LINES="$SCRIPT_DIR/fix-patch-lines"
 
 OBJTOOL="$PWD/tools/objtool/objtool"
+DWP="${DWP:-llvm-dwp}"
 CONFIG="$PWD/.config"
 TMP_DIR="$PWD/klp-tmp"
 
@@ -265,6 +266,11 @@ validate_config() {
 	[[ -v CONFIG_KLP_BUILD ]] ||			\
 		die "CONFIG_KLP_BUILD not enabled"
 
+	if [[ -v CONFIG_DEBUG_INFO_SPLIT ]]; then
+		command -v "$DWP" &>/dev/null ||
+			die "split DWARF requires llvm-dwp (or set DWP)"
+	fi
+
 	[[ -v CONFIG_GCC_PLUGIN_LATENT_ENTROPY ]] &&	\
 		die "kernel option 'CONFIG_GCC_PLUGIN_LATENT_ENTROPY' not supported"
 
@@ -588,6 +594,15 @@ find_objects() {
 		    -printf '%P\n'
 }
 
+# Save the matching DWOs before the next build overwrites them.
+save_split_dwarf() {
+	local file="$1"
+	local dest="$2"
+
+	[[ -v CONFIG_DEBUG_INFO_SPLIT ]] || return 0
+	"$DWP" -e "$file" -o "$dest.dwp" || die "cannot save split DWARF for $file"
+}
+
 # Copy all .o archives to $ORIG_DIR
 copy_orig_objects() {
 	local files=()
@@ -608,6 +623,7 @@ copy_orig_objects() {
 
 		mkdir -p "$orig_dir"
 		cp -f "$file" "$orig_dir"
+		save_split_dwarf "$file" "$orig_file"
 	done
 	xtrace_restore
 
@@ -647,6 +663,7 @@ copy_patched_objects() {
 
 		mkdir -p "$patched_dir"
 		cp -f "$file" "$patched_dir"
+		save_split_dwarf "$file" "$patched_file"
 		found=1
 	done
 	xtrace_restore
@@ -680,6 +697,7 @@ generate_checksums() {
 
 		mkdir -p "$(dirname "$dest")"
 		cp -f "$file" "$dest"
+		[[ -f "$file.dwp" ]] && cp -f "$file.dwp" "$dest.dwp"
 		"$OBJTOOL" klp checksum "$dest"
 	done
 
diff --git a/tools/objtool/klp-dwarf.c b/tools/objtool/klp-dwarf.c
index 99b098ee7904..98d00ea19b7c 100644
--- a/tools/objtool/klp-dwarf.c
+++ b/tools/objtool/klp-dwarf.c
@@ -138,6 +138,10 @@ static void normalize_source(char *path)
 	*write = '\0';
 }
 
+/*
+ * Full LTO with --plugin-opt=dwo_dir can merge CUs and lose TU source
+ * paths. Kbuild does not enable this output mode.
+ */
 static int read_unit_source(struct identity_cu *cu, Dwarf_Die *die)
 {
 	Dwarf_Attribute attr;
@@ -146,12 +150,12 @@ static int read_unit_source(struct identity_cu *cu, Dwarf_Die *die)
 
 	if (dwarf_tag(die) != DW_TAG_compile_unit)
 		return 0;
-	if (dwarf_attr(die, DW_AT_name, &attr)) {
+	if (dwarf_attr_integrate(die, DW_AT_name, &attr)) {
 		name = dwarf_formstring(&attr);
 		if (!name)
 			return die_error(die, "invalid compilation unit name");
 	}
-	if (dwarf_attr(die, DW_AT_comp_dir, &attr)) {
+	if (dwarf_attr_integrate(die, DW_AT_comp_dir, &attr)) {
 		dir = dwarf_formstring(&attr);
 		if (!dir)
 			return die_error(die, "invalid compilation directory");
@@ -472,6 +476,23 @@ static int add_address(struct identity_reader *reader, struct identity_die *entr
 	return 0;
 }
 
+/*
+ * Reduced DWARF can omit the element size of idt_table. Reading its
+ * DW_FORM_data1 upper bound (255) as -1 would misidentify it as empty.
+ */
+static int read_array_bound(Dwarf_Attribute *attr, Dwarf_Sword *bound)
+{
+	Dwarf_Word value;
+
+	if (dwarf_whatform(attr) == DW_FORM_sdata ||
+	    dwarf_whatform(attr) == DW_FORM_implicit_const)
+		return dwarf_formsdata(attr, bound);
+	if (dwarf_formudata(attr, &value) || value > INT64_MAX)
+		return -1;
+	*bound = value;
+	return 0;
+}
+
 /*
  * The empty features[] in virtio_input.c can share an address with the
  * nonempty features[] in trans_virtio.c. Exclude the empty object's DIE
@@ -498,10 +519,10 @@ static bool is_empty_type(Dwarf_Die *type)
 			return true;
 		lower = 0;
 		if (dwarf_attr(&range, DW_AT_lower_bound, &attr) &&
-		    dwarf_formsdata(&attr, &lower))
+		    read_array_bound(&attr, &lower))
 			continue;
 		if (dwarf_attr(&range, DW_AT_upper_bound, &attr) &&
-		    !dwarf_formsdata(&attr, &upper) && upper < lower)
+		    !read_array_bound(&attr, &upper) && upper < lower)
 			return true;
 	} while (!dwarf_siblingof(&range, &range));
 	return false;
@@ -752,7 +773,7 @@ static int build_identities(struct identity_reader *reader)
 	static const Dwfl_Callbacks callbacks = { .section_address = dwfl_offline_section_address };
 	Dwfl *dwfl = dwfl_begin(&callbacks);
 	Dwarf_CU *cu = NULL, *next;
-	Dwarf_Die die;
+	Dwarf_Die die, split;
 	Dwarf *dwarf;
 	struct identity_die *entry;
 	unsigned int bucket;
@@ -767,11 +788,15 @@ static int build_identities(struct identity_reader *reader)
 	dwarf = dwfl_module_getdwarf(reader->module, &reader->bias);
 	if (!dwarf || check_relocations(dwarf))
 		goto out;
-	while (!(ret = dwarf_get_units(dwarf, cu, &next, NULL, &unit_type, &die, NULL))) {
-		if (unit_type == DW_UT_skeleton ||
-		    unit_type == DW_UT_split_compile || unit_type == DW_UT_split_type) {
-			ret = die_error(&die, "split DWARF is not supported yet");
-			goto out;
+	while (!(ret = dwarf_get_units(dwarf, cu, &next, NULL, &unit_type, &die, &split))) {
+		if (unit_type == DW_UT_skeleton) {
+			/* libdw checks the DWO ID and resolves addresses through the skeleton. */
+			if (!split.addr) {
+				ret = die_error(&die,
+						"missing or mismatched split DWARF file (saved .dwp files require libdw >= 0.191)");
+				goto out;
+			}
+			die = split;
 		}
 		if (!is_unit(dwarf_tag(&die))) {
 			ret = die_error(&die, "invalid unit root");
-- 
2.55.0


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [RFC PATCH v2 06/11] klp-build: Require CONFIG_DEBUG_INFO
  2026-11-04 13:31 [RFC PATCH v2 00/11] objtool/klp: DWARF-based cross-build symbol correlation Longjun Luo
                   ` (4 preceding siblings ...)
  2026-11-04 13:31 ` [RFC PATCH v2 05/11] objtool/klp: Support reduced and split DWARF Longjun Luo
@ 2026-11-04 13:31 ` Longjun Luo
  2026-11-04 13:31 ` [RFC PATCH v2 07/11] objtool/klp: Correlate symbols using DWARF identities Longjun Luo
                   ` (4 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Longjun Luo @ 2026-11-04 13:31 UTC (permalink / raw)
  To: jpoimboe, peterz
  Cc: mbenes, pmladek, joe.lawrence, song, puranjay, nathan,
	ndesaulniers, morbo, justinstitt, live-patching, llvm,
	linux-kernel, Longjun Luo

Require the kernel's debug information configuration for DWARF-based
symbol correlation. Use the C and assembly flags supplied by Kbuild
instead of adding separate -g flags in klp-build.

Reduced and split DWARF are supported by the reader and saved-object
handling introduced earlier in this series.

Validated the available objtool tests with GCC 16.2.1 and Clang
22.1.8. No failures.

Suggested-by: Josh Poimboeuf <jpoimboe@kernel.org>
Assisted-by: LLM
Signed-off-by: Longjun Luo <luolongjuna@gmail.com>
---
 scripts/livepatch/klp-build | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build
index 95bea5b2057e..da55e1028fe3 100755
--- a/scripts/livepatch/klp-build
+++ b/scripts/livepatch/klp-build
@@ -266,6 +266,9 @@ validate_config() {
 	[[ -v CONFIG_KLP_BUILD ]] ||			\
 		die "CONFIG_KLP_BUILD not enabled"
 
+	[[ -v CONFIG_DEBUG_INFO ]] ||			\
+		die "CONFIG_DEBUG_INFO not enabled"
+
 	if [[ -v CONFIG_DEBUG_INFO_SPLIT ]]; then
 		command -v "$DWP" &>/dev/null ||
 			die "split DWARF requires llvm-dwp (or set DWP)"
-- 
2.55.0


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [RFC PATCH v2 07/11] objtool/klp: Correlate symbols using DWARF identities
  2026-11-04 13:31 [RFC PATCH v2 00/11] objtool/klp: DWARF-based cross-build symbol correlation Longjun Luo
                   ` (5 preceding siblings ...)
  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
  2026-11-04 13:31 ` [RFC PATCH v2 08/11] objtool/klp: Include symbol identities in reference checksums Longjun Luo
                   ` (3 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Longjun Luo @ 2026-11-04 13:31 UTC (permalink / raw)
  To: jpoimboe, peterz
  Cc: mbenes, pmladek, joe.lawrence, song, puranjay, nathan,
	ndesaulniers, morbo, justinstitt, live-patching, llvm,
	linux-kernel, Longjun Luo

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


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [RFC PATCH v2 08/11] objtool/klp: Include symbol identities in reference checksums
  2026-11-04 13:31 [RFC PATCH v2 00/11] objtool/klp: DWARF-based cross-build symbol correlation Longjun Luo
                   ` (6 preceding siblings ...)
  2026-11-04 13:31 ` [RFC PATCH v2 07/11] objtool/klp: Correlate symbols using DWARF identities Longjun Luo
@ 2026-11-04 13:31 ` Longjun Luo
  2026-11-04 13:31 ` [RFC PATCH v2 09/11] objtool/klp: Extend twins through unchanged function references Longjun Luo
                   ` (2 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Longjun Luo @ 2026-11-04 13:31 UTC (permalink / raw)
  To: jpoimboe, peterz
  Cc: mbenes, pmladek, joe.lawrence, song, puranjay, nathan,
	ndesaulniers, morbo, justinstitt, live-patching, llvm,
	linux-kernel, Longjun Luo

Hashing only the target's suffix-stripped name can hide a reference change
between different objects with the same name.

Prepare DWARF identities for checksum calculation and hash the identity
path for named references. Keep the existing checksum section format and
use suffix-stripped ELF names when no DWARF identity is available.

The three tests rejected by policy in earlier patches remain rejected.

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/klp-checksum.c | 31 ++++++++++++++++++++++---------
 1 file changed, 22 insertions(+), 9 deletions(-)

diff --git a/tools/objtool/klp-checksum.c b/tools/objtool/klp-checksum.c
index 1a5f2fa89baa..44c0eb784e57 100644
--- a/tools/objtool/klp-checksum.c
+++ b/tools/objtool/klp-checksum.c
@@ -77,6 +77,12 @@ static bool is_const_reference(struct symbol *sym)
 static int checksum_object(struct objtool_file *file, struct symbol *sym,
 			   unsigned int depth);
 
+/* Use suffix-stripped ELF names for references without DWARF identities. */
+static const char *checksum_name(struct symbol *sym)
+{
+	return sym->identity.path ?: sym->identity.demangled_name;
+}
+
 static int checksum_update_insn(struct objtool_file *file, struct symbol *func,
 				struct instruction *insn)
 {
@@ -110,8 +116,8 @@ static int checksum_update_insn(struct objtool_file *file, struct symbol *func,
 			__checksum_update_insn(func, insn, buf, len);
 
 			if (call_dest) {
-				__checksum_update_insn(func, insn, call_dest->identity.demangled_name,
-						       strlen(call_dest->identity.demangled_name));
+				__checksum_update_insn(func, insn, checksum_name(call_dest),
+						       strlen(checksum_name(call_dest)));
 
 			} else if (jump_dest) {
 				struct symbol *dest_sym;
@@ -127,8 +133,8 @@ static int checksum_update_insn(struct objtool_file *file, struct symbol *func,
 				if (!dest_sym)
 					goto alts;
 
-				__checksum_update_insn(func, insn, dest_sym->identity.demangled_name,
-						       strlen(dest_sym->identity.demangled_name));
+				__checksum_update_insn(func, insn, checksum_name(dest_sym),
+						       strlen(checksum_name(dest_sym)));
 
 				offset = jump_dest->offset - dest_sym->offset;
 				__checksum_update_insn(func, insn, &offset, sizeof(offset));
@@ -170,8 +176,8 @@ static int checksum_update_insn(struct objtool_file *file, struct symbol *func,
 		__checksum_update_insn(func, insn, &sym->csum.checksum,
 				       sizeof(sym->csum.checksum));
 	} else {
-		__checksum_update_insn(func, insn, sym->identity.demangled_name,
-				       strlen(sym->identity.demangled_name));
+		__checksum_update_insn(func, insn, checksum_name(sym),
+				       strlen(checksum_name(sym)));
 	}
 	__checksum_update_insn(func, insn, &offset, sizeof(offset));
 
@@ -265,9 +271,9 @@ static int checksum_update_object(struct objtool_file *file, struct symbol *sym,
 						 &target->csum.checksum,
 						 sizeof(target->csum.checksum));
 		} else {
-			__checksum_update_object(sym, sym_offset, "reloc name",
-						 target->identity.demangled_name,
-						 strlen(target->identity.demangled_name));
+			__checksum_update_object(sym, sym_offset, "reloc identity",
+						 checksum_name(target),
+						 strlen(checksum_name(target)));
 		}
 		__checksum_update_object(sym, sym_offset, "reloc addend",
 					 &offset, sizeof(offset));
@@ -387,6 +393,7 @@ static const char * const klp_checksum_usage[] = {
 int cmd_klp_checksum(int argc, const char **argv)
 {
 	struct objtool_file *file;
+	struct symbol *sym;
 	int ret;
 
 	const struct option options[] = {
@@ -411,6 +418,12 @@ int cmd_klp_checksum(int argc, const char **argv)
 	if (ret)
 		goto out;
 
+	for_each_sym(file->elf, sym)
+		sym->dont_correlate = dont_correlate(sym);
+	ret = prepare_identities(file->elf);
+	if (ret)
+		goto out;
+
 	ret = calculate_checksums(file);
 	if (ret)
 		goto out;
-- 
2.55.0


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [RFC PATCH v2 09/11] objtool/klp: Extend twins through unchanged function references
  2026-11-04 13:31 [RFC PATCH v2 00/11] objtool/klp: DWARF-based cross-build symbol correlation Longjun Luo
                   ` (7 preceding siblings ...)
  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 ` 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
  10 siblings, 0 replies; 12+ messages in thread
From: Longjun Luo @ 2026-11-04 13:31 UTC (permalink / raw)
  To: jpoimboe, peterz
  Cc: mbenes, pmladek, joe.lawrence, song, puranjay, nathan,
	ndesaulniers, morbo, justinstitt, live-patching, llvm,
	linux-kernel, Longjun Luo

Repeated statics and optimized variable pieces can share an identity
and checksum, leaving them uncorrelated after identity matching.

For paired functions with equal checksums, compare their relocation
entries and extend twins to the referenced symbols. Require matching
reference layouts and compatible targets, and reject twin conflicts.

This also checks the correlation against the function checksum: equal
checksums can hide a switch between instances sharing one identity, and
a conflicting reference must stop patch generation.

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/klp-diff.c | 132 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 132 insertions(+)

diff --git a/tools/objtool/klp-diff.c b/tools/objtool/klp-diff.c
index 468b8654b687..e8252e586260 100644
--- a/tools/objtool/klp-diff.c
+++ b/tools/objtool/klp-diff.c
@@ -826,6 +826,8 @@ static int validate_symbol_twins(struct elfs *e)
 	return ret;
 }
 
+static int extend_twins_by_relocations(struct elfs *e);
+
 /*
  * Correlate symbols between the orig and patched objects.  This is a
  * prerequisite for detecting changed functions, as well as for properly
@@ -845,6 +847,7 @@ static int correlate_symbols(struct elfs *e)
 
 	if (correlate_by_identity(e) ||
 	    extend_same_twins_by_checksum(e) ||
+	    extend_twins_by_relocations(e) ||
 	    validate_symbol_twins(e))
 		return -1;
 
@@ -1294,6 +1297,135 @@ static int convert_reloc_sym(struct elf *elf, struct reloc *reloc)
 	return convert_reloc_secsym_to_sym(elf, reloc);
 }
 
+static int correlate_reloc(struct elfs *e, struct symbol *func,
+			   struct reloc *orig_reloc, struct reloc *patched_reloc)
+{
+	struct symbol *orig_sym, *patched_sym;
+	unsigned long offset = reloc_offset(orig_reloc) - func->offset;
+
+	if (offset != reloc_offset(patched_reloc) - func->twin->offset ||
+	    reloc_type(orig_reloc) != reloc_type(patched_reloc)) {
+		ERROR("%s+%#lx: relocation mismatch in unchanged function", func->name, offset);
+		return -1;
+	}
+
+	if (reloc_type(orig_reloc) == R_NONE ||
+	    is_uncorrelated_section(orig_reloc->sym->sec) ||
+	    is_uncorrelated_section(patched_reloc->sym->sec))
+		return 0;
+
+	/* Section symbols have no twins; resolve the target symbol first. */
+	if (convert_reloc_secsym_to_sym(e->orig, orig_reloc) ||
+	    convert_reloc_secsym_to_sym(e->patched, patched_reloc))
+		return 0;
+
+	orig_sym = orig_reloc->sym;
+	patched_sym = patched_reloc->sym;
+	if (orig_sym->dont_correlate || patched_sym->dont_correlate)
+		return 0;
+
+	if (reloc_addend(orig_reloc) != reloc_addend(patched_reloc)) {
+		ERROR("%s+%#lx: reference offset mismatch: %s[%u]%+lld and %s[%u]%+lld",
+		      func->name, offset,
+		      orig_sym->name, orig_sym->idx, (long long)reloc_addend(orig_reloc),
+		      patched_sym->name, patched_sym->idx, (long long)reloc_addend(patched_reloc));
+		return -1;
+	}
+
+	if (orig_sym->twin == patched_sym && patched_sym->twin == orig_sym)
+		return 0;
+
+	if (orig_sym->twin || patched_sym->twin) {
+		ERROR("%s+%#lx: twin conflict: orig %s[%u]->%s[%u], patched %s[%u]->%s[%u]",
+		      func->name, offset,
+		      orig_sym->name, orig_sym->idx,
+		      orig_sym->twin ? orig_sym->twin->name : "<none>",
+		      orig_sym->twin ? orig_sym->twin->idx : 0,
+		      patched_sym->name, patched_sym->idx,
+		      patched_sym->twin ? patched_sym->twin->name : "<none>",
+		      patched_sym->twin ? patched_sym->twin->idx : 0);
+		return -1;
+	}
+
+	/* Compound target checksums are independent of their defining TU. */
+	if (is_compound_constant(orig_sym) && is_compound_constant(patched_sym)) {
+		if (!orig_sym->csum.checksum || orig_sym->len != patched_sym->len ||
+		    orig_sym->csum.checksum != patched_sym->csum.checksum) {
+			ERROR("%s+%#lx: compound content mismatch in unchanged function",
+			      func->name, offset);
+			return -1;
+		}
+	} else if (!is_twin_candidate(orig_sym, patched_sym)) {
+		ERROR("%s+%#lx: incompatible reference targets: %s[%u] and %s[%u]",
+		      func->name, offset, orig_sym->name, orig_sym->idx,
+		      patched_sym->name, patched_sym->idx);
+		return -1;
+	}
+
+	orig_sym->twin = patched_sym;
+	patched_sym->twin = orig_sym;
+	dbg_correlate("%s(): %s+%#lx: %s[%u] -> %s[%u]",
+		      __func__, func->name, offset,
+		      orig_sym->name, orig_sym->idx, patched_sym->name, patched_sym->idx);
+	return 0;
+}
+
+static struct reloc *get_func_relocs(struct elf *elf, struct symbol *func, unsigned int *nr)
+{
+	struct reloc *first, *reloc;
+
+	first = find_reloc_by_dest_range(elf, func->sec, func->offset, func->len);
+	*nr = 0;
+	for (reloc = first; reloc && reloc_offset(reloc) < func->offset + func->len;
+	     reloc = rsec_next_reloc(func->sec->rsec, reloc))
+		(*nr)++;
+	return first;
+}
+
+/* Use unchanged functions' references to resolve ambiguous symbol identities. */
+static int extend_twin_by_relocations(struct elfs *e, struct symbol *func)
+{
+	struct reloc *orig_relocs, *patched_relocs;
+	unsigned int nr_orig, nr_patched, i;
+
+	orig_relocs = get_func_relocs(e->orig, func, &nr_orig);
+	patched_relocs = get_func_relocs(e->patched, func->twin, &nr_patched);
+	if (nr_orig != nr_patched) {
+		ERROR("%s: relocation count mismatch in unchanged function: %u != %u",
+		      func->name, nr_orig, nr_patched);
+		return -1;
+	}
+
+	for (i = 0; i < nr_orig; i++)
+		if (correlate_reloc(e, func, &orig_relocs[i], &patched_relocs[i]))
+			return -1;
+	return 0;
+}
+
+static bool same_function_checksum(struct symbol *func)
+{
+	/* Cold subfunctions have no separate checksum; use their parent's. */
+	struct symbol *parent = func->pfunc;
+	struct symbol *parent_twin = func->twin->pfunc;
+
+	return parent->twin == parent_twin && parent->csum.checksum &&
+	       parent->csum.checksum == parent_twin->csum.checksum;
+}
+
+static int extend_twins_by_relocations(struct elfs *e)
+{
+	struct symbol *sym;
+
+	for_each_sym(e->orig, sym) {
+		if (!is_func_sym(sym) || sym->dont_correlate || !sym->twin ||
+		    !same_function_checksum(sym))
+			continue;
+		if (extend_twin_by_relocations(e, sym))
+			return -1;
+	}
+	return 0;
+}
+
 /*
  * Check if the original module already has a dependency on dep_mod, i.e. it
  * already references at least one export from that module.
-- 
2.55.0


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [RFC PATCH v2 10/11] objtool/klp: Check unmatched data references
  2026-11-04 13:31 [RFC PATCH v2 00/11] objtool/klp: DWARF-based cross-build symbol correlation Longjun Luo
                   ` (8 preceding siblings ...)
  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 ` Longjun Luo
  2026-11-04 13:31 ` [RFC PATCH v2 11/11] objtool/klp: Test symbol identity and state preservation Longjun Luo
  10 siblings, 0 replies; 12+ messages in thread
From: Longjun Luo @ 2026-11-04 13:31 UTC (permalink / raw)
  To: jpoimboe, peterz
  Cc: mbenes, pmladek, joe.lawrence, song, puranjay, nathan,
	ndesaulniers, morbo, justinstitt, live-patching, llvm,
	linux-kernel, Longjun Luo

An unmatched data reference can still refer to existing state. Treating
it as new can give old and patched code separate copies of that state.

For ordinary data, allow new storage only when no original identity
candidate exists. For compound literals, reuse a unique unchanged object
in both functions' references. Reject ambiguous or changed compounds;
without a source identity, additions cannot be distinguished from changes.

Report the referencing function and offset for unresolved ordinary data.
Exclude fake special-section symbols created by objtool after correlation.

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/klp-diff.c | 134 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 134 insertions(+)

diff --git a/tools/objtool/klp-diff.c b/tools/objtool/klp-diff.c
index e8252e586260..19f5161420cd 100644
--- a/tools/objtool/klp-diff.c
+++ b/tools/objtool/klp-diff.c
@@ -1426,6 +1426,135 @@ static int extend_twins_by_relocations(struct elfs *e)
 	return 0;
 }
 
+static int find_unique_compound_reference(struct elf *elf, struct reloc *relocs,
+					  unsigned int nr, struct symbol *sym,
+					  struct symbol **match)
+{
+	unsigned int i;
+
+	*match = NULL;
+	for (i = 0; i < nr; i++) {
+		struct reloc *reloc = &relocs[i];
+		struct symbol *target;
+
+		if (reloc_type(reloc) == R_NONE || convert_reloc_secsym_to_sym(elf, reloc))
+			continue;
+		target = reloc->sym;
+		if (!is_compound_constant(target) || target->len != sym->len ||
+		    target->csum.checksum != sym->csum.checksum)
+			continue;
+		if (*match && *match != target) {
+			ERROR("unresolved compound reference: %s matches %s[%u] and %s[%u]",
+			      sym->name, (*match)->name, (*match)->idx, target->name, target->idx);
+			return -1;
+		}
+		*match = target;
+	}
+	return 0;
+}
+
+static int resolve_compound_reference(struct elfs *e, struct symbol *sym,
+				      struct reloc *orig_relocs, unsigned int nr_orig,
+				      struct reloc *patched_relocs, unsigned int nr_patched)
+{
+	struct symbol *orig_sym, *patched_sym;
+
+	if (!sym->csum.checksum) {
+		ERROR("%s: missing compound checksum", sym->name);
+		return -1;
+	}
+
+	if (find_unique_compound_reference(e->orig, orig_relocs, nr_orig, sym, &orig_sym))
+		return -1;
+	if (!orig_sym) {
+		/*
+		 * Data changes are unsupported. With no source identity, new and
+		 * changed compound literals cannot be distinguished here.
+		 */
+		ERROR("unresolved compound reference: %s has no unchanged match", sym->name);
+		return -1;
+	}
+
+	if (find_unique_compound_reference(e->patched, patched_relocs, nr_patched,
+					   sym, &patched_sym))
+		return -1;
+	if (patched_sym != sym) {
+		ERROR("unresolved compound reference: %s is not a unique reference in its function",
+		      sym->name);
+		return -1;
+	}
+	if (orig_sym->twin) {
+		ERROR("compound twin conflict: %s[%u] paired with %s[%u]",
+		      orig_sym->name, orig_sym->idx,
+		      orig_sym->twin->name, orig_sym->twin->idx);
+		return -1;
+	}
+	sym->twin = orig_sym;
+	orig_sym->twin = sym;
+	dbg_correlate("%s(): %s[%u] -> %s[%u]", __func__,
+		      orig_sym->name, orig_sym->idx, sym->name, sym->idx);
+	return 0;
+}
+
+/*
+ * Check references to symbols without twins for ambiguity:
+ *
+ * 1. Ordinary data: allow as new only if no original identity candidates exist.
+ *    We assume DWARF identity information is stable across builds with the
+ *    same compiler.
+ * 2. Compound literals (anonymous data with static storage duration):
+ *    - delete: allowed.
+ *    - same: require a unique unchanged match in both functions' references.
+ *    - change: unsupported, to keep old and patched code's data views consistent.
+ *    - new: reject for now; it cannot be distinguished from change.
+ *      Supporting additions requires an explicit mechanism.
+ */
+static int check_unmatched_reference(struct elfs *e, struct reloc *reloc)
+{
+	struct symbol *patched_sym = reloc->sym, *orig_sym, *func;
+	struct reloc *orig_relocs = NULL, *patched_relocs = NULL;
+	unsigned int nr_orig = 0, nr_patched = 0;
+	u32 key;
+
+	if (reference_twin(patched_sym) || patched_sym->dont_correlate ||
+	    !is_object_sym(patched_sym) || is_undef_sym(patched_sym) || !patched_sym->len)
+		return 0;
+
+	if (!is_compound_constant(patched_sym)) {
+		if (!has_dwarf_identity(patched_sym)) {
+			ERROR_FUNC(reloc->sec->base, reloc_offset(reloc),
+				   "unresolved data reference: %s has no DWARF identity",
+				   patched_sym->name);
+			return -1;
+		}
+		key = identity_hash(&patched_sym->identity);
+		/*
+		 * With one old count.N and two patched candidates, the unmatched one
+		 * may still refer to old state, even if the original already has a twin.
+		 */
+		elf_hash_for_each_possible(e->orig, symbol_identity, orig_sym, identity_hash, key) {
+			if (!same_identity(&patched_sym->identity, &orig_sym->identity))
+				continue;
+			ERROR_FUNC(reloc->sec->base, reloc_offset(reloc),
+				   "unresolved data reference: %s (%s, %s, %s), old candidate %s[%u]",
+				   patched_sym->name, patched_sym->identity.source,
+				   patched_sym->identity.scope,
+				   patched_sym->identity.demangled_name,
+				   orig_sym->name, orig_sym->idx);
+			return -1;
+		}
+		return 0;
+	}
+
+	func = find_func_containing(reloc->sec->base, reloc_offset(reloc));
+	if (func && func->included && func->twin && !same_function_checksum(func)) {
+		orig_relocs = get_func_relocs(e->orig, func->twin, &nr_orig);
+		patched_relocs = get_func_relocs(e->patched, func, &nr_patched);
+	}
+	return resolve_compound_reference(e, patched_sym, orig_relocs, nr_orig,
+					  patched_relocs, nr_patched);
+}
+
 /*
  * Check if the original module already has a dependency on dep_mod, i.e. it
  * already references at least one export from that module.
@@ -1629,6 +1758,9 @@ static int clone_reloc(struct elfs *e, struct reloc *patched_reloc,
 	bool klp;
 
 	/* Reject ambiguous data references before treating them as new. */
+	if (check_unmatched_reference(e, patched_reloc))
+		return -1;
+
 	klp = klp_reloc_needed(patched_reloc);
 
 	dbg_clone_reloc(sec, offset, patched_sym, addend, export, klp);
@@ -1781,6 +1913,8 @@ static int create_fake_symbol(struct elf *elf, struct section *sec,
 		return -1;
 
 	sym->fake = 1;
+	/* Special-section entries are copied with the patched code. */
+	sym->dont_correlate = 1;
 	return 0;
 }
 
-- 
2.55.0


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [RFC PATCH v2 11/11] objtool/klp: Test symbol identity and state preservation
  2026-11-04 13:31 [RFC PATCH v2 00/11] objtool/klp: DWARF-based cross-build symbol correlation Longjun Luo
                   ` (9 preceding siblings ...)
  2026-11-04 13:31 ` [RFC PATCH v2 10/11] objtool/klp: Check unmatched data references Longjun Luo
@ 2026-11-04 13:31 ` Longjun Luo
  10 siblings, 0 replies; 12+ messages in thread
From: Longjun Luo @ 2026-11-04 13:31 UTC (permalink / raw)
  To: jpoimboe, peterz
  Cc: mbenes, pmladek, joe.lawrence, song, puranjay, nathan,
	ndesaulniers, morbo, justinstitt, live-patching, llvm,
	linux-kernel, Longjun Luo

Exercise local state across source changes, inlining, compiler renaming
and LTO. Check that patches reference the original objects and allocate
only new state. Cover aliases, assembly, metadata, ICF, DWARF pieces,
shrunk storage, reduced information and saved DWP files. Cover separate
cold-fragment identities left by weak overrides.

Check rejection of ambiguous repeated statics, conflicting references
and changed data. Complete compound-reference tests and add same-named
target switches for the identity-based reference checksum.

Skip the saved-DWP test when the linked libdw lacks DWP support.

The full suite has 68 passes with GCC 16.2.1 and 75 with 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>
---
 .../tests/generic/fixtures/identity/alias.c   | 25 ++++++
 .../generic/fixtures/identity/block_data.c    | 22 ++++++
 .../generic/fixtures/identity/branch_hint.c   | 28 +++++++
 .../generic/fixtures/identity/cold_strong.c   | 13 +++
 .../generic/fixtures/identity/cold_user.c     | 15 ++++
 .../generic/fixtures/identity/cold_weak.c     | 10 +++
 .../generic/fixtures/identity/consumer.c      | 30 +++++++
 .../tests/generic/fixtures/identity/entries.c | 10 +++
 .../generic/fixtures/identity/full_left.c     | 16 ++++
 .../generic/fixtures/identity/full_right.c    |  6 ++
 .../generic/fixtures/identity/icf_left/same.c | 16 ++++
 .../fixtures/identity/icf_right/same.c        | 16 ++++
 .../fixtures/identity/inline_helpers.c        | 27 +++++++
 .../tests/generic/fixtures/identity/lookup.c  | 13 +++
 .../generic/fixtures/identity/metadata.c      | 24 ++++++
 .../generic/fixtures/identity/move_left.c     |  9 +++
 .../generic/fixtures/identity/move_right.c    |  8 ++
 .../generic/fixtures/identity/new/same.c      |  6 ++
 .../generic/fixtures/identity/new_driver.c    | 15 ++++
 .../generic/fixtures/identity/new_provider.c  |  4 +
 .../generic/fixtures/identity/old/same.c      |  6 ++
 .../tests/generic/fixtures/identity/once.h    | 16 ++++
 .../generic/fixtures/identity/once_inline.c   | 28 +++++++
 .../generic/fixtures/identity/once_insert.c   | 21 +++++
 .../tests/generic/fixtures/identity/pieces.c  | 28 +++++++
 .../fixtures/identity/promotion_bounds_a.c    | 12 +++
 .../fixtures/identity/promotion_bounds_b.c    | 12 +++
 .../fixtures/identity/promotion_bounds_d.c    | 12 +++
 .../generic/fixtures/identity/provider.c      |  9 +++
 .../generic/fixtures/identity/readonly.c      | 30 +++++++
 .../tests/generic/fixtures/identity/record.h  |  8 ++
 .../tests/generic/fixtures/identity/rx.c      |  4 +
 .../tests/generic/fixtures/identity/shrink.c  | 19 +++++
 .../generic/fixtures/identity/source_driver.c | 11 +++
 .../fixtures/identity/static_replace.c        | 29 +++++++
 .../generic/fixtures/identity/static_scope.c  | 31 ++++++++
 .../fixtures/identity/static_scope_reorder.c  | 31 ++++++++
 .../tests/generic/fixtures/identity/tx.c      |  4 +
 .../generic/test-checksum-identity-source.sh  | 24 ++++++
 .../tests/generic/test-checksum-identity.sh   | 32 ++++++++
 .../tests/generic/test-identity-alias.sh      | 26 ++++++
 .../tests/generic/test-identity-block-data.sh | 12 +++
 .../generic/test-identity-branch-hint.sh      | 18 +++++
 .../tests/generic/test-identity-compound.sh   | 24 +++++-
 .../tests/generic/test-identity-dwarf.sh      | 34 ++++++++
 .../tests/generic/test-identity-full-lto.sh   | 30 +++++++
 .../tests/generic/test-identity-icf.sh        | 23 ++++++
 .../generic/test-identity-inline-helpers.sh   | 25 ++++++
 .../generic/test-identity-inline-state.sh     | 21 +++++
 .../tests/generic/test-identity-metadata.sh   | 20 +++++
 .../tests/generic/test-identity-new-tu.sh     | 24 ++++++
 .../generic/test-identity-once-insert.sh      | 19 +++++
 .../tests/generic/test-identity-pieces.sh     | 27 +++++++
 .../generic/test-identity-readonly-address.sh | 23 ++++++
 .../tests/generic/test-identity-shrink.sh     | 19 +++++
 .../tests/generic/test-identity-source.sh     | 29 +++++++
 .../tests/generic/test-identity-split.sh      | 34 ++++++++
 .../generic/test-identity-static-replace.sh   | 20 +++++
 .../generic/test-identity-static-scope.sh     | 35 ++++++++
 .../test-identity-thinlto-name-drift.sh       | 79 +++++++++++++++++++
 .../test-identity-thinlto-promotion.sh        | 26 ++++++
 .../tests/generic/test-identity-weak-cold.sh  | 35 ++++++++
 .../tests/x86/fixtures/identity_assembly.S    | 33 ++++++++
 .../tests/x86/fixtures/warn_reference.c       | 36 +++++++++
 .../tests/x86/test-identity-assembly.sh       | 19 +++++
 .../objtool/tests/x86/test-warn-reference.sh  | 21 +++++
 66 files changed, 1390 insertions(+), 2 deletions(-)
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/alias.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/block_data.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/branch_hint.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/cold_strong.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/cold_user.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/cold_weak.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/consumer.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/entries.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/full_left.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/full_right.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/icf_left/same.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/icf_right/same.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/inline_helpers.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/lookup.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/metadata.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/move_left.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/move_right.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/new/same.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/new_driver.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/new_provider.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/old/same.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/once.h
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/once_inline.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/once_insert.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/pieces.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/promotion_bounds_a.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/promotion_bounds_b.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/promotion_bounds_d.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/provider.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/readonly.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/record.h
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/rx.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/shrink.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/source_driver.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/static_replace.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/static_scope.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/static_scope_reorder.c
 create mode 100644 tools/objtool/tests/generic/fixtures/identity/tx.c
 create mode 100755 tools/objtool/tests/generic/test-checksum-identity-source.sh
 create mode 100755 tools/objtool/tests/generic/test-checksum-identity.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-alias.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-block-data.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-branch-hint.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-dwarf.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-full-lto.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-icf.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-inline-helpers.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-inline-state.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-metadata.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-new-tu.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-once-insert.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-pieces.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-readonly-address.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-shrink.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-source.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-split.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-static-replace.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-static-scope.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-thinlto-name-drift.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-thinlto-promotion.sh
 create mode 100755 tools/objtool/tests/generic/test-identity-weak-cold.sh
 create mode 100644 tools/objtool/tests/x86/fixtures/identity_assembly.S
 create mode 100644 tools/objtool/tests/x86/fixtures/warn_reference.c
 create mode 100755 tools/objtool/tests/x86/test-identity-assembly.sh
 create mode 100755 tools/objtool/tests/x86/test-warn-reference.sh

diff --git a/tools/objtool/tests/generic/fixtures/identity/alias.c b/tools/objtool/tests/generic/fixtures/identity/alias.c
new file mode 100644
index 000000000000..ab1f67d29d01
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/alias.c
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+
+#if defined(PATCHED) && defined(CHANGE_DATA)
+long state[2] = { 12, 22 };
+#else
+long state[2] = { 11, 22 };
+#endif
+extern long state_alias[2] __attribute__((alias("state")));
+
+NOIPA long *target(unsigned int index)
+{
+#ifdef PATCHED
+	if (index >= 2)
+		return (void *)0;
+#endif
+	return &state_alias[index];
+}
+
+NOIPA long *unchanged(void)
+{
+	return state;
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/block_data.c b/tools/objtool/tests/generic/fixtures/identity/block_data.c
new file mode 100644
index 000000000000..bb64ba670d3e
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/block_data.c
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+
+NOIPA int target(int which)
+{
+	if (which) {
+		static volatile int budget = 5;
+
+		return --budget;
+	}
+	{
+#ifdef PATCHED
+		static volatile int budget = 10;
+#else
+		static volatile int budget = 5;
+#endif
+
+		return --budget;
+	}
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/branch_hint.c b/tools/objtool/tests/generic/fixtures/identity/branch_hint.c
new file mode 100644
index 000000000000..a605426047cb
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/branch_hint.c
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+extern void consume(int value);
+
+#ifdef PATCHED
+#define FIRST_LIKELY 1
+#define SECOND_LIKELY 0
+#else
+#define FIRST_LIKELY 0
+#define SECOND_LIKELY 1
+#endif
+
+NOIPA void target(int first, int second)
+{
+	if (__builtin_expect(first, FIRST_LIKELY)) {
+		static volatile int count;
+
+		consume(count++);
+	}
+	consume(0);
+	if (__builtin_expect(second, SECOND_LIKELY)) {
+		static volatile int count;
+
+		consume(count++);
+	}
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/cold_strong.c b/tools/objtool/tests/generic/fixtures/identity/cold_strong.c
new file mode 100644
index 000000000000..d2049c417499
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/cold_strong.c
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+extern void strong_slow_path(int value) __attribute__((cold));
+
+__attribute__((noinline)) int foo(int value)
+{
+#if defined(PATCHED) && defined(CHANGE_STRONG)
+	value++;
+#endif
+	if (__builtin_expect(value < 0, 0))
+		strong_slow_path(value);
+	return value + 2;
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/cold_user.c b/tools/objtool/tests/generic/fixtures/identity/cold_user.c
new file mode 100644
index 000000000000..c88d9aff1a97
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/cold_user.c
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+
+int foo(int value);
+
+int user(int value)
+{
+#ifdef PATCHED
+	return foo(value) + 1;
+#else
+	return foo(value);
+#endif
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/cold_weak.c b/tools/objtool/tests/generic/fixtures/identity/cold_weak.c
new file mode 100644
index 000000000000..15981d530be5
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/cold_weak.c
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+extern void weak_slow_path(int value) __attribute__((cold));
+
+__attribute__((weak, noinline)) int foo(int value)
+{
+	if (__builtin_expect(value < 0, 0))
+		weak_slow_path(value);
+	return value + 1;
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/consumer.c b/tools/objtool/tests/generic/fixtures/identity/consumer.c
new file mode 100644
index 000000000000..4aad0e941bc3
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/consumer.c
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+#ifdef PATCHED
+static NOIPA int helper(int uid) { return uid == 0 ? 0 : -1; }
+#else
+int helper(int uid);
+#endif
+#ifdef DATA
+#ifdef PATCHED
+static volatile int limit = 1500;
+#else
+extern volatile int limit;
+#endif
+#endif
+#ifdef TABLE
+struct operations { int (*check)(int); };
+struct operations ops = { .check = helper };
+#endif
+NOIPA int caller(int uid)
+{
+#ifdef DATA
+	return uid <= limit ? 0 : -90;
+#elif defined(TABLE)
+	return ops.check(uid);
+#else
+	return helper(uid);
+#endif
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/entries.c b/tools/objtool/tests/generic/fixtures/identity/entries.c
new file mode 100644
index 000000000000..53e809957c1c
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/entries.c
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+unsigned int tx(unsigned int n);
+unsigned int rx(unsigned int n);
+unsigned int entry_tx(unsigned int n) { return tx(n); }
+#ifdef PATCHED
+unsigned int entry_rx(unsigned int n) { return rx(n); }
+#endif
diff --git a/tools/objtool/tests/generic/fixtures/identity/full_left.c b/tools/objtool/tests/generic/fixtures/identity/full_left.c
new file mode 100644
index 000000000000..1a75df588712
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/full_left.c
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+static volatile int state;
+#ifdef PATCHED
+static NOIPA int helper(void) { return ++state; }
+#endif
+NOIPA int left_fn(void)
+{
+#ifdef PATCHED
+	return helper();
+#else
+	return ++state;
+#endif
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/full_right.c b/tools/objtool/tests/generic/fixtures/identity/full_right.c
new file mode 100644
index 000000000000..17583384cf32
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/full_right.c
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+static volatile int state;
+static NOIPA int helper(void) { return ++state; }
+NOIPA int right_fn(void) { return helper(); }
diff --git a/tools/objtool/tests/generic/fixtures/identity/icf_left/same.c b/tools/objtool/tests/generic/fixtures/identity/icf_left/same.c
new file mode 100644
index 000000000000..25312a0ee0c0
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/icf_left/same.c
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+extern long process(long value);
+
+static long base(long value)
+{
+	return process(value) + process(value + 7);
+}
+
+static long helper(long value)
+{
+	return process(value) + process(value + 7);
+}
+
+long (*left_base)(long) = base;
+long (*left_helper)(long) = helper;
diff --git a/tools/objtool/tests/generic/fixtures/identity/icf_right/same.c b/tools/objtool/tests/generic/fixtures/identity/icf_right/same.c
new file mode 100644
index 000000000000..e4357fe8b475
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/icf_right/same.c
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+extern long process(long value);
+
+static long base(long value)
+{
+	return process(value) + process(value + 9);
+}
+
+static long helper(long value)
+{
+	return process(value) + process(value + 9);
+}
+
+long (*right_base)(long) = base;
+long (*right_helper)(long) = helper;
diff --git a/tools/objtool/tests/generic/fixtures/identity/inline_helpers.c b/tools/objtool/tests/generic/fixtures/identity/inline_helpers.c
new file mode 100644
index 000000000000..41ee7326f8a1
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/inline_helpers.c
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+#include "once.h"
+MODINFO;
+
+static volatile long rx_seed, tx_seed;
+
+static inline __attribute__((always_inline)) long initialize_rx(long value)
+{
+	ONCE_INIT(rx_seed, 1);
+	return value ^ rx_seed;
+}
+
+static inline __attribute__((always_inline)) long initialize_tx(long value)
+{
+	ONCE_INIT(tx_seed, 2);
+	return value ^ tx_seed;
+}
+
+NOIPA long target(long value)
+{
+#ifdef PATCHED
+	value = initialize_tx(value);
+#endif
+	return initialize_rx(value);
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/lookup.c b/tools/objtool/tests/generic/fixtures/identity/lookup.c
new file mode 100644
index 000000000000..b72d8d01815d
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/lookup.c
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+
+NOIPA long target(long value)
+{
+#ifdef PATCHED
+	return value + 2;
+#else
+	return value + 1;
+#endif
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/metadata.c b/tools/objtool/tests/generic/fixtures/identity/metadata.c
new file mode 100644
index 000000000000..53f6785b2593
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/metadata.c
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+extern void *get_symbol(const char *name);
+
+#define SYMBOL_GET(name) ({ \
+	static const char __notrim[] __attribute__((used, section(".no_trim_symbol"))) = #name; \
+	get_symbol(#name); \
+})
+#define TRACEPOINT_CHECK(name) \
+	static const char __trace_check_##name[] \
+		__attribute__((used, section("__tracepoint_check"))) = #name
+
+NOIPA void *target(void)
+{
+	TRACEPOINT_CHECK(start);
+	SYMBOL_GET(first);
+#ifdef PATCHED
+	TRACEPOINT_CHECK(finish);
+	SYMBOL_GET(extra);
+#endif
+	return SYMBOL_GET(second);
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/move_left.c b/tools/objtool/tests/generic/fixtures/identity/move_left.c
new file mode 100644
index 000000000000..7d3b03bc87e5
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/move_left.c
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+static NOIPA int helper(int n) { return n <= 1500 ? 0 : -90; }
+NOIPA int keep_left(int n) { return helper(n); }
+#ifndef PATCHED
+NOIPA int caller(int n) { return helper(n); }
+#endif
diff --git a/tools/objtool/tests/generic/fixtures/identity/move_right.c b/tools/objtool/tests/generic/fixtures/identity/move_right.c
new file mode 100644
index 000000000000..9a5f82a7d9ba
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/move_right.c
@@ -0,0 +1,8 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+static NOIPA int helper(int n) { return n <= 4096 ? 0 : -90; }
+NOIPA int keep_right(int n) { return helper(n); }
+#ifdef PATCHED
+NOIPA int caller(int n) { return helper(n); }
+#endif
diff --git a/tools/objtool/tests/generic/fixtures/identity/new/same.c b/tools/objtool/tests/generic/fixtures/identity/new/same.c
new file mode 100644
index 000000000000..54550f20fe79
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/new/same.c
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "../common.h"
+static volatile int bytes;
+static NOIPA int helper(int n) { return bytes += n; }
+NOIPA int account_new(int n) { return helper(n); }
diff --git a/tools/objtool/tests/generic/fixtures/identity/new_driver.c b/tools/objtool/tests/generic/fixtures/identity/new_driver.c
new file mode 100644
index 000000000000..c06a1ac27609
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/new_driver.c
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+#ifdef PATCHED
+int clamp(int value);
+#endif
+NOIPA int target(int value)
+{
+#ifdef PATCHED
+	return clamp(value);
+#else
+	return value;
+#endif
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/new_provider.c b/tools/objtool/tests/generic/fixtures/identity/new_provider.c
new file mode 100644
index 000000000000..69c1b3d0b6a7
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/new_provider.c
@@ -0,0 +1,4 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+NOIPA int clamp(int value) { return value < 0 ? 0 : value; }
diff --git a/tools/objtool/tests/generic/fixtures/identity/old/same.c b/tools/objtool/tests/generic/fixtures/identity/old/same.c
new file mode 100644
index 000000000000..e03f14f16214
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/old/same.c
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "../common.h"
+static volatile int bytes;
+static NOIPA int helper(int n) { return bytes += n; }
+NOIPA int account_old(int n) { return helper(n); }
diff --git a/tools/objtool/tests/generic/fixtures/identity/once.h b/tools/objtool/tests/generic/fixtures/identity/once.h
new file mode 100644
index 000000000000..c31b2911ce3e
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/once.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#ifndef KLP_TEST_ONCE_H
+#define KLP_TEST_ONCE_H
+
+extern long generate(long seed);
+
+#define ONCE_INIT(state, seed) do { \
+	static volatile int done; \
+	if (!done) { \
+		done = 1; \
+		(state) = generate(seed); \
+	} \
+} while (0)
+
+#endif
diff --git a/tools/objtool/tests/generic/fixtures/identity/once_inline.c b/tools/objtool/tests/generic/fixtures/identity/once_inline.c
new file mode 100644
index 000000000000..607f81ca4dd1
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/once_inline.c
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+#include "once.h"
+MODINFO;
+
+static volatile long first, second;
+
+static inline __attribute__((always_inline)) long initialize(long value)
+{
+	ONCE_INIT(first, 1);
+	ONCE_INIT(second, 2);
+	return value ^ first ^ second;
+}
+
+NOIPA long unchanged(long value)
+{
+	return initialize(value);
+}
+
+NOIPA long target(long value)
+{
+#ifdef PATCHED
+	return initialize(value + 1);
+#else
+	return initialize(value);
+#endif
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/once_insert.c b/tools/objtool/tests/generic/fixtures/identity/once_insert.c
new file mode 100644
index 000000000000..62f4215210ae
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/once_insert.c
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+#include "once.h"
+MODINFO;
+
+static volatile long first, second, extra;
+
+NOIPA long target(long value)
+{
+	ONCE_INIT(first, 1);
+#ifdef PATCHED
+	ONCE_INIT(extra, 3);
+#endif
+	ONCE_INIT(second, 2);
+#ifdef PATCHED
+	return value ^ first ^ second ^ extra;
+#else
+	return value ^ first ^ second;
+#endif
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/pieces.c b/tools/objtool/tests/generic/fixtures/identity/pieces.c
new file mode 100644
index 000000000000..4cd3ffbacbae
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/pieces.c
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+
+static struct { long first, second; } state;
+
+NOIPA void set(long first, long second)
+{
+	state.first = first;
+	state.second = second;
+}
+
+NOIPA long sum(void)
+{
+	return state.first + state.second;
+}
+
+NOIPA long target(void)
+{
+#if defined(PATCHED) && defined(SWITCH_FIELD)
+	return state.second;
+#elif defined(PATCHED)
+	return state.first + 1;
+#else
+	return state.first;
+#endif
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/promotion_bounds_a.c b/tools/objtool/tests/generic/fixtures/identity/promotion_bounds_a.c
new file mode 100644
index 000000000000..74eb8ac0bfc3
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/promotion_bounds_a.c
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+static volatile int state[4];
+
+int a_get(unsigned int index)
+{
+#ifdef PATCHED
+	if (index >= 4)
+		return 0;
+#endif
+	return state[index];
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/promotion_bounds_b.c b/tools/objtool/tests/generic/fixtures/identity/promotion_bounds_b.c
new file mode 100644
index 000000000000..2b96569ce6cd
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/promotion_bounds_b.c
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+
+int a_get(unsigned int index);
+int d_get(unsigned int index);
+
+int target(unsigned int index)
+{
+	return a_get(index) + d_get(index);
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/promotion_bounds_d.c b/tools/objtool/tests/generic/fixtures/identity/promotion_bounds_d.c
new file mode 100644
index 000000000000..2e2a2691df38
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/promotion_bounds_d.c
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+static volatile int state[4];
+
+int d_get(unsigned int index)
+{
+#ifdef PATCHED
+	return state[index & 3];
+#else
+	return state[index];
+#endif
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/provider.c b/tools/objtool/tests/generic/fixtures/identity/provider.c
new file mode 100644
index 000000000000..412c5cb67341
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/provider.c
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+NOIPA int helper(int uid)
+{
+	return uid == 0 || uid == 1000 ? 0 : -1;
+}
+
+volatile int limit = 4096;
diff --git a/tools/objtool/tests/generic/fixtures/identity/readonly.c b/tools/objtool/tests/generic/fixtures/identity/readonly.c
new file mode 100644
index 000000000000..f145d3344d60
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/readonly.c
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+struct protocol { int kind; };
+NOIPA const struct protocol *tcp_type(int valid)
+{
+#if defined(PATCHED) && defined(CHANGE_VALUE)
+	static const struct protocol type = { 4 };
+#else
+	static const struct protocol type = { 1 };
+#endif
+#ifdef PATCHED
+	if (!valid)
+		return (void *)0;
+#endif
+	return &type;
+}
+NOIPA const struct protocol *udp_type(int valid)
+{
+	static const struct protocol type = { 1 };
+	return &type;
+}
+#ifdef PATCHED
+NOIPA const struct protocol *unix_type(int valid)
+{
+	static const struct protocol type = { 1 };
+	return &type;
+}
+#endif
diff --git a/tools/objtool/tests/generic/fixtures/identity/record.h b/tools/objtool/tests/generic/fixtures/identity/record.h
new file mode 100644
index 000000000000..5af00486c5f4
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/record.h
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+static inline unsigned int record(unsigned int n)
+{
+	static volatile unsigned int state;
+
+	return state += n;
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/rx.c b/tools/objtool/tests/generic/fixtures/identity/rx.c
new file mode 100644
index 000000000000..5d9871527b08
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/rx.c
@@ -0,0 +1,4 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "record.h"
+unsigned int rx(unsigned int n) { return record(n); }
diff --git a/tools/objtool/tests/generic/fixtures/identity/shrink.c b/tools/objtool/tests/generic/fixtures/identity/shrink.c
new file mode 100644
index 000000000000..1a43964f719b
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/shrink.c
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+extern void initialize(void);
+extern void finish(void);
+
+NOIPA void target(void)
+{
+	static int done;
+
+	if (!done) {
+		done = 1;
+		initialize();
+#ifdef PATCHED
+		finish();
+#endif
+	}
+}
diff --git a/tools/objtool/tests/generic/fixtures/identity/source_driver.c b/tools/objtool/tests/generic/fixtures/identity/source_driver.c
new file mode 100644
index 000000000000..0ebcb01ced02
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/source_driver.c
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+#ifdef PATCHED
+int account_new(int n);
+NOIPA int account(int n) { return account_new(n); }
+#else
+int account_old(int n);
+NOIPA int account(int n) { return account_old(n); }
+#endif
diff --git a/tools/objtool/tests/generic/fixtures/identity/static_replace.c b/tools/objtool/tests/generic/fixtures/identity/static_replace.c
new file mode 100644
index 000000000000..5745738d5cbd
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/static_replace.c
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+
+#ifndef PATCHED
+NOIPA long retired(long value)
+{
+	static volatile long count;
+
+	return count += value;
+}
+#endif
+
+NOIPA long unchanged(long value)
+{
+	static volatile long count;
+
+	return count += value;
+}
+
+#ifdef PATCHED
+NOIPA long added(long value)
+{
+	static volatile long count;
+
+	return count += value;
+}
+#endif
diff --git a/tools/objtool/tests/generic/fixtures/identity/static_scope.c b/tools/objtool/tests/generic/fixtures/identity/static_scope.c
new file mode 100644
index 000000000000..a467f24f1a14
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/static_scope.c
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+
+NOIPA int tx(int n)
+{
+	static volatile int bytes;
+
+#ifdef PATCHED
+	if (n < 0)
+		return 0;
+#endif
+	return bytes += n;
+}
+
+NOIPA int rx(int n)
+{
+	static volatile int bytes;
+
+	return bytes += n;
+}
+
+#ifdef PATCHED
+NOIPA int extra(int n)
+{
+	static volatile int bytes;
+
+	return bytes += n;
+}
+#endif
diff --git a/tools/objtool/tests/generic/fixtures/identity/static_scope_reorder.c b/tools/objtool/tests/generic/fixtures/identity/static_scope_reorder.c
new file mode 100644
index 000000000000..a451e632a6cb
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/static_scope_reorder.c
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "common.h"
+MODINFO;
+
+#ifdef PATCHED
+NOIPA int rx(int n)
+{
+	static volatile int bytes;
+
+	if (n < 0)
+		return 0;
+	return bytes += n;
+}
+#endif
+
+NOIPA int tx(int n)
+{
+	static volatile int bytes;
+
+	return bytes += n;
+}
+
+#ifndef PATCHED
+NOIPA int rx(int n)
+{
+	static volatile int bytes;
+
+	return bytes += n;
+}
+#endif
diff --git a/tools/objtool/tests/generic/fixtures/identity/tx.c b/tools/objtool/tests/generic/fixtures/identity/tx.c
new file mode 100644
index 000000000000..d536c3c63549
--- /dev/null
+++ b/tools/objtool/tests/generic/fixtures/identity/tx.c
@@ -0,0 +1,4 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+#include "record.h"
+unsigned int tx(unsigned int n) { return record(n); }
diff --git a/tools/objtool/tests/generic/test-checksum-identity-source.sh b/tools/objtool/tests/generic/test-checksum-identity-source.sh
new file mode 100755
index 000000000000..b320a57c6a76
--- /dev/null
+++ b/tools/objtool/tests/generic/test-checksum-identity-source.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# Moving an external caller changes which TU-local helper it calls.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+modes=(none)
+case "$($CC --version | head -1)" in *clang*) modes+=(thin) ;; esac
+
+for mode in "${modes[@]}"; do
+	build_identity_pair "$mode" "" identity/move_left.c identity/move_right.c
+	[ "$(in_symbols orig.o | awk '$4 == "FUNC" && $8 ~ /^helper($|.llvm.)/' | wc -l)" = 2 ] ||
+		fail "fixture lacks two TU-local helpers"
+	assert_checksum_differs caller
+	assert_checksum_matches keep_left
+	assert_checksum_matches keep_right
+	run_diff
+	assert_patched caller
+	assert_not_patched keep_left
+	assert_not_patched keep_right
+done
+
+pass "moving a caller between TUs detects the changed local target"
diff --git a/tools/objtool/tests/generic/test-checksum-identity.sh b/tools/objtool/tests/generic/test-checksum-identity.sh
new file mode 100755
index 000000000000..cc8a70bf31d2
--- /dev/null
+++ b/tools/objtool/tests/generic/test-checksum-identity.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# Checksums for calls, data references and function-pointer tables must
+# distinguish same-named static and external targets.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+modes=(none)
+case "$($CC --version | head -1)" in *clang*) modes+=(thin) ;; esac
+
+for mode in "${modes[@]}"; do
+	build_identity_pair "$mode" "" identity/provider.c identity/consumer.c
+	[ "$(in_symbols patched.o |
+		awk '$4 == "FUNC" && $8 ~ /^helper($|.llvm.)/' | wc -l)" = 2 ] ||
+		fail "fixture lacks the local and external helpers"
+	assert_checksum_differs caller
+	run_diff
+	assert_patched caller
+
+	build_identity_pair "$mode" -DTABLE identity/provider.c identity/consumer.c
+	assert_checksum_differs ops
+	run_diff 255
+	assert_diff_log 'changed data: ops'
+
+	build_identity_pair "$mode" -DDATA identity/provider.c identity/consumer.c
+	assert_checksum_differs caller
+	run_diff
+	assert_patched caller
+done
+
+pass "call, data and function-pointer targets use the full identity"
diff --git a/tools/objtool/tests/generic/test-identity-alias.sh b/tools/objtool/tests/generic/test-identity-alias.sh
new file mode 100755
index 000000000000..84a3829b00e8
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-alias.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# Access through an alias must retain the original shared storage.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+build_pair identity/alias.c
+assert_input_symbol state
+assert_input_symbol state_alias
+
+run_diff
+assert_patched target
+assert_not_patched unchanged
+assert_no_symbol state
+assert_no_symbol state_alias
+run_post_link
+identity_relocs out.o .klp.rela.orig..text.target |
+	awk '$5 ~ /^\.klp\.sym\.orig\.state(_alias)?,/' | grep -q . ||
+	fail "target does not reference original state"
+
+build_pair identity/alias.c -DCHANGE_DATA
+run_diff 255
+assert_diff_log 'changed data: state'
+
+pass "data aliases reuse the original object and reject changed storage"
diff --git a/tools/objtool/tests/generic/test-identity-block-data.sh b/tools/objtool/tests/generic/test-identity-block-data.sh
new file mode 100755
index 000000000000..439b2fe7cc61
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-block-data.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# Changing a block-local counter's initial value remains a data change.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+build_pair identity/block_data.c
+run_diff 255
+assert_diff_log 'changed data:.*budget'
+
+pass "changed block-local data is rejected"
diff --git a/tools/objtool/tests/generic/test-identity-branch-hint.sh b/tools/objtool/tests/generic/test-identity-branch-hint.sh
new file mode 100755
index 000000000000..68a49aa12e96
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-branch-hint.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# Branch hints can reorder symbols for two existing block-local counters.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+build_pair identity/branch_hint.c
+for obj in orig.o patched.o; do
+	nr="$(in_symbols "$obj" | awk '
+		$4 == "OBJECT" && $8 ~ /(^|\.)count(\.[0-9]+)?$/ { n++ } END { print n+0 }')"
+	[ "$nr" = 2 ] || fail "expected two counters in $obj"
+done
+
+run_diff 255
+assert_diff_log 'unresolved data reference:.*count'
+
+pass "branch hint changes report ambiguous counters"
diff --git a/tools/objtool/tests/generic/test-identity-compound.sh b/tools/objtool/tests/generic/test-identity-compound.sh
index 76b16616b32c..fc58d2fad17a 100755
--- a/tools/objtool/tests/generic/test-identity-compound.sh
+++ b/tools/objtool/tests/generic/test-identity-compound.sh
@@ -1,13 +1,33 @@
 #!/bin/bash
 # SPDX-License-Identifier: GPL-2.0
 # Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
-# Array contents and callbacks contribute to the referring function's checksum.
+# A changed function keeps the address of an unchanged anonymous table.
 . "$(dirname "$0")/../identity-lib.sh"
 
 setup
+for flags in "" -DWRITABLE; do
+	build_pair identity/compound.c -DCHANGE_FUNCTION $flags
+	old="$(identity_object_for orig.o target)"
+	[ -n "$old" ] || fail "missing compound reference"
+
+	run_diff
+	assert_patched target
+	assert_not_patched unchanged
+	run_post_link
+	assert_original_reference target "$old"
+	assert_no_symbol "$old"
+done
+
+# Array contents and callbacks contribute to the referring function's checksum.
 for change in CHANGE_LEAF CHANGE_TARGET; do
 	build_pair identity/compound.c "-D$change"
 	assert_checksum_differs target
+	run_diff 255
+	assert_diff_log 'unresolved compound reference:.*has no unchanged match'
 done
 
-pass "compound contents contribute to reference checksums"
+build_pair identity/compound.c -DCHANGE_FUNCTION -DAMBIGUOUS
+run_diff 255
+assert_diff_log 'unresolved compound reference:.*matches .* and '
+
+pass "compound references retain old storage or report ambiguity"
diff --git a/tools/objtool/tests/generic/test-identity-dwarf.sh b/tools/objtool/tests/generic/test-identity-dwarf.sh
new file mode 100755
index 000000000000..2b18110f243b
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-dwarf.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# Kernel DWARF 4/5 and reduced debug information describe the same static state.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+reduced=(-fno-var-tracking)
+case "$($CC --version | head -1)" in
+*clang*) ;;
+*) reduced+=(-femit-struct-debug-baseonly) ;;
+esac
+
+for version in 4 5; do
+	for mode in full reduced; do
+		flags=("-gdwarf-$version")
+		[ "$mode" = reduced ] && flags+=("${reduced[@]}")
+		build_pair identity/once_inline.c "${flags[@]}"
+		states="$(in_symbols orig.o | awk '
+			$4 == "OBJECT" && $8 ~ /(^|\.)done(\.[0-9]+)?$/ { print $8 }')"
+		[ "$(echo "$states" | wc -w)" = 2 ] || fail "expected two initialization flags"
+
+		run_diff
+		assert_patched target
+		assert_not_patched unchanged
+		run_post_link
+		for state in $states; do
+			assert_original_reference target "$state"
+			assert_no_symbol "$state"
+		done
+	done
+done
+
+pass "full and reduced DWARF preserve initialization state"
diff --git a/tools/objtool/tests/generic/test-identity-full-lto.sh b/tools/objtool/tests/generic/test-identity-full-lto.sh
new file mode 100755
index 000000000000..08aa67a444a3
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-full-lto.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# Full LTO merges FILE entries; a new helper must not replace an existing TU's helper.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+build_identity_pair full "" identity/full_left.c identity/full_right.c
+
+[ "$(in_symbols orig.o | awk '$4 == "FUNC" && $8 ~ /^helper(\.[0-9]+)?$/' | wc -l)" = 1 ] &&
+[ "$(in_symbols patched.o | awk '$4 == "FUNC" && $8 ~ /^helper(\.[0-9]+)?$/' | wc -l)" = 2 ] ||
+	probe_skip "Full LTO did not emit two same-base helper names"
+left_helper="$(identity_relocs patched.o .rela.text.left_fn |
+	awk '{ sub(/^\.text\./, "", $5); print $5 }')"
+right_helper="$(identity_relocs patched.o .rela.text.right_fn |
+	awk '{ sub(/^\.text\./, "", $5); print $5 }')"
+[ -n "$left_helper" ] && [ -n "$right_helper" ] || fail "missing helper references"
+left_state="$(identity_object_for orig.o left_fn)"
+[ -n "$left_state" ] || fail "missing left state reference"
+
+run_diff
+assert_patched left_fn
+assert_not_patched right_fn
+assert_patched "$left_helper"
+assert_not_patched "$right_helper"
+assert_reloc_sym .text.left_fn "$left_helper"
+run_post_link
+assert_original_reference "$left_helper" "$left_state"
+
+pass "Full LTO helpers retain their defining TUs"
diff --git a/tools/objtool/tests/generic/test-identity-icf.sh b/tools/objtool/tests/generic/test-identity-icf.sh
new file mode 100755
index 000000000000..bad6acccd0c1
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-icf.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# Same-named ICF wrappers in different CUs must remain separate functions.
+. "$(dirname "$0")/../identity-lib.sh"
+
+gcc_only "GCC IPA-ICF emits wrappers without code ranges in DWARF"
+setup
+build_identity_pair none -fipa-icf \
+	identity/icf_left/same.c identity/icf_right/same.c identity/lookup.c
+for obj in orig.o patched.o; do
+	nr="$(in_symbols "$obj" | awk '$4 == "FUNC" && $8 == "helper" { n++ } END { print n+0 }')"
+	[ "$nr" = 2 ] || fail "expected two helper functions in $obj"
+done
+
+run_diff
+assert_patched target
+assert_not_patched helper
+assert_not_patched base
+assert_checksum_matches left_helper
+assert_checksum_matches right_helper
+
+pass "same FILE basenames do not confuse ICF wrapper identities"
diff --git a/tools/objtool/tests/generic/test-identity-inline-helpers.sh b/tools/objtool/tests/generic/test-identity-inline-helpers.sh
new file mode 100755
index 000000000000..21641c8dc975
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-inline-helpers.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# A new inline initializer gets its own flag; the existing initializer keeps its state.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+build_pair identity/inline_helpers.c
+old="$(in_symbols orig.o | awk '$4 == "OBJECT" && $8 ~ /(^|\.)done(\.[0-9]+)?$/ { print $8 }')"
+[ "$(echo "$old" | wc -w)" = 1 ] || fail "expected one original initialization flag"
+nr="$(in_symbols patched.o |
+	awk '$4 == "OBJECT" && $8 ~ /(^|\.)done(\.[0-9]+)?$/ { n++ } END { print n+0 }')"
+[ "$nr" = 2 ] ||
+	fail "expected two patched initialization flags"
+
+run_diff
+assert_patched target
+new="$(out_symbols |
+	awk '$4 == "OBJECT" && $7 != "UND" && $8 ~ /(^|\.)done(\.[0-9]+)?$/ { print $8 }')"
+[ "$(echo "$new" | wc -w)" = 1 ] || fail "expected only the new initializer's flag to be copied"
+assert_reloc_sym .text.target "$new"
+run_post_link
+assert_original_reference target "$old"
+
+pass "inline helpers retain separate initialization state without an unchanged caller"
diff --git a/tools/objtool/tests/generic/test-identity-inline-state.sh b/tools/objtool/tests/generic/test-identity-inline-state.sh
new file mode 100755
index 000000000000..2cff1d3c0645
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-inline-state.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# Both callers of an inline initializer must keep using its original flags.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+build_pair identity/once_inline.c
+states="$(in_symbols orig.o | awk '$4 == "OBJECT" && $8 ~ /(^|\.)done(\.[0-9]+)?$/ { print $8 }')"
+[ "$(echo "$states" | wc -w)" = 2 ] || fail "expected two initialization flags"
+
+run_diff
+assert_patched target
+assert_not_patched unchanged
+run_post_link
+for state in $states; do
+	assert_original_reference target "$state"
+	assert_no_symbol "$state"
+done
+
+pass "changed caller retains both initialization flags"
diff --git a/tools/objtool/tests/generic/test-identity-metadata.sh b/tools/objtool/tests/generic/test-identity-metadata.sh
new file mode 100755
index 000000000000..fe575d710c56
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-metadata.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# symbol_get and tracepoint checks leave build-time markers that the linker discards.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+build_pair identity/metadata.c
+assert_input_section .no_trim_symbol
+assert_input_section __tracepoint_check
+
+run_diff
+assert_patched target
+assert_no_section .no_trim_symbol
+assert_no_section __tracepoint_check
+if diff_log | grep -qE 'no correlation:.*(__notrim|__trace_check_)'; then
+	fail "build-time markers reported as unmatched state"
+fi
+
+pass "build-time markers do not enter state correlation"
diff --git a/tools/objtool/tests/generic/test-identity-new-tu.sh b/tools/objtool/tests/generic/test-identity-new-tu.sh
new file mode 100755
index 000000000000..9675af836b46
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-new-tu.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# A patch may extract validation into a newly added source file.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+build_pair identity/new_driver.c
+build_one identity/new_provider.c provider.o
+mv "$workdir/patched.o" "$workdir/driver.o"
+partial_link "$workdir/patched.o" "$workdir/driver.o" "$workdir/provider.o" ||
+	fail "partial link failed"
+
+[ "$(in_symbols patched.o | awk '$4 == "FILE"' | wc -l)" \
+	-gt "$(in_symbols orig.o | awk '$4 == "FILE"' | wc -l)" ] ||
+	fail "fixture did not add a FILE entry"
+
+run_diff
+assert_patched target
+assert_patched clamp
+assert_diff_log 'changed function: target'
+assert_diff_log 'new function: clamp'
+
+pass "new translation unit does not block existing symbol correlation"
diff --git a/tools/objtool/tests/generic/test-identity-once-insert.sh b/tools/objtool/tests/generic/test-identity-once-insert.sh
new file mode 100755
index 000000000000..f1accb98d1e5
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-once-insert.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# Inserting an initializer must not reset the two existing once flags.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+build_pair identity/once_insert.c
+for spec in "orig.o 2" "patched.o 3"; do
+	set -- $spec
+	nr="$(in_symbols "$1" | awk '
+		$4 == "OBJECT" && $8 ~ /(^|\.)done(\.[0-9]+)?$/ { n++ } END { print n+0 }')"
+	[ "$nr" = "$2" ] || fail "expected $2 initialization flags in $1"
+done
+
+run_diff 255
+assert_diff_log 'unresolved data reference:.*done'
+
+pass "ambiguous initialization flags are rejected"
diff --git a/tools/objtool/tests/generic/test-identity-pieces.sh b/tools/objtool/tests/generic/test-identity-pieces.sh
new file mode 100755
index 000000000000..c42a98ad0ef1
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-pieces.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# GlobalOpt can split a struct into equal-sized objects with the same DWARF name.
+. "$(dirname "$0")/../identity-lib.sh"
+
+clang_only "Clang GlobalOpt splits this state into separate objects"
+setup
+build_pair identity/pieces.c
+nr="$(in_symbols orig.o | awk '$4 == "OBJECT" && $8 ~ /^state\.[0-9]+$/ { n++ } END { print n+0 }')"
+[ "$nr" = 2 ] || probe_skip "compiler did not split state into two objects"
+old="$(identity_object_for orig.o target)"
+[ -n "$old" ] || fail "missing state reference"
+
+run_diff
+assert_patched target
+assert_not_patched set
+assert_not_patched sum
+run_post_link
+assert_original_reference target "$old"
+assert_no_symbol "$old"
+
+build_pair identity/pieces.c -DSWITCH_FIELD
+run_diff 255
+assert_diff_log 'target.*twin conflict:'
+
+pass "unchanged references preserve pieces and conflicting field switches are rejected"
diff --git a/tools/objtool/tests/generic/test-identity-readonly-address.sh b/tools/objtool/tests/generic/test-identity-readonly-address.sh
new file mode 100755
index 000000000000..18c49856833b
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-readonly-address.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# Read-only descriptors still need their original pointer identity.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+build_pair identity/readonly.c
+old="$(identity_object_for orig.o tcp_type)"
+[ -n "$old" ] || fail "missing TCP descriptor reference"
+
+run_diff
+assert_patched tcp_type
+assert_patched unix_type
+assert_not_patched udp_type
+run_post_link
+assert_original_reference tcp_type "$old"
+
+build_pair identity/readonly.c -DCHANGE_VALUE
+run_diff 255
+assert_diff_log 'changed data:'
+
+pass "changed function retains the original read-only descriptor address"
diff --git a/tools/objtool/tests/generic/test-identity-shrink.sh b/tools/objtool/tests/generic/test-identity-shrink.sh
new file mode 100755
index 000000000000..d9f007fc56f3
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-shrink.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# A compressed once flag keeps its state when the initialization code changes.
+. "$(dirname "$0")/../identity-lib.sh"
+
+clang_only "Clang shrinks the int flag to one byte"
+setup
+build_pair identity/shrink.c
+old="$(in_symbols orig.o | awk '$4 == "OBJECT" && $3 == 1 && $8 ~ /\.done$/ { print $8 }')"
+[ -n "$old" ] || probe_skip "compiler did not shrink the once flag"
+
+run_diff
+assert_patched target
+run_post_link
+assert_original_reference target "$old"
+assert_no_symbol "$old"
+
+pass "compressed once flag retains its original storage"
diff --git a/tools/objtool/tests/generic/test-identity-source.sh b/tools/objtool/tests/generic/test-identity-source.sh
new file mode 100755
index 000000000000..bbb2e729571f
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-source.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# old/same.c and new/same.c share the FILE name "same.c"; static state
+# from the new file must still be treated as new.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+build_pair identity/source_driver.c
+build_one identity/old/same.c old.o
+build_one identity/new/same.c new.o
+mv "$workdir/orig.o" "$workdir/driver-orig.o"
+mv "$workdir/patched.o" "$workdir/driver-patched.o"
+partial_link "$workdir/orig.o" "$workdir/driver-orig.o" "$workdir/old.o" ||
+	fail "original link failed"
+partial_link "$workdir/patched.o" "$workdir/driver-patched.o" "$workdir/new.o" ||
+	fail "patched link failed"
+for obj in orig.o patched.o; do
+	in_symbols "$obj" | awk '$4 == "FILE" && $8 == "same.c"' | grep -q . ||
+		fail "missing same.c FILE"
+done
+
+run_diff
+assert_patched account
+assert_patched helper
+assert_no_klp_sym bytes
+assert_section .bss.bytes
+
+pass "private state from a different TU is new even with the same basename"
diff --git a/tools/objtool/tests/generic/test-identity-split.sh b/tools/objtool/tests/generic/test-identity-split.sh
new file mode 100755
index 000000000000..0af15583fe08
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-split.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# klp-build saves the original DWOs in a DWP before the patched build overwrites them.
+. "$(dirname "$0")/../identity-lib.sh"
+
+DWP="${DWP:-llvm-dwp}"
+command -v "$DWP" >/dev/null || probe_skip "split DWARF requires llvm-dwp (or set DWP)"
+setup
+# libdw 0.191 added DWP lookup and dwarf_cu_dwp_section_info().
+libdw="$(ldd "$OBJTOOL" 2>/dev/null | awk '$1 == "libdw.so.1" { print $3 }')"
+if [ -n "$libdw" ] && $READELF --dyn-syms -W "$libdw" > "$workdir/libdw.symbols"; then
+	grep -q 'dwarf_cu_dwp_section_info' "$workdir/libdw.symbols" ||
+		probe_skip "saved DWP files require libdw >= 0.191"
+fi
+# CONFIG_DEBUG_INFO_SPLIT with CONFIG_DEBUG_INFO_DWARF4.
+build_pair identity/once_inline.c -gdwarf-4 -gsplit-dwarf
+states="$(in_symbols orig.o | awk '$4 == "OBJECT" && $8 ~ /(^|\.)done(\.[0-9]+)?$/ { print $8 }')"
+[ "$(echo "$states" | wc -w)" = 2 ] || fail "expected two initialization flags"
+for side in orig patched; do
+	"$DWP" -e "$workdir/$side.o" -o "$workdir/$side.o.dwp" || fail "DWP packaging failed"
+	mv "$workdir/$side.dwo" "$workdir/$side.dwo.saved" || fail "missing $side.dwo"
+done
+
+run_diff
+assert_patched target
+assert_not_patched unchanged
+run_post_link
+for state in $states; do
+	assert_original_reference target "$state"
+	assert_no_symbol "$state"
+done
+
+pass "saved DWP files retain the original initialization state"
diff --git a/tools/objtool/tests/generic/test-identity-static-replace.sh b/tools/objtool/tests/generic/test-identity-static-replace.sh
new file mode 100755
index 000000000000..987555f925a7
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-static-replace.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# A new function's counter must not share state with an unchanged function.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+build_pair identity/static_replace.c
+new="$(identity_object_for patched.o added)"
+[ -n "$new" ] || fail "missing new counter reference"
+
+run_diff
+assert_patched added
+assert_not_patched unchanged
+assert_not_patched retired
+assert_section ".bss.$new"
+assert_reloc_sym .text.added "$new"
+assert_no_klp_sym "$new"
+
+pass "replacement function gets its own new counter"
diff --git a/tools/objtool/tests/generic/test-identity-static-scope.sh b/tools/objtool/tests/generic/test-identity-static-scope.sh
new file mode 100755
index 000000000000..fc0372100886
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-static-scope.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# Reordering functions or adding a same-named static must retain old state.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+
+build_pair identity/static_scope.c
+tx_state="$(identity_object_for orig.o tx)"
+extra_state="$(identity_object_for patched.o extra)"
+[ -n "$tx_state" ] && [ -n "$extra_state" ] || fail "missing static reference"
+
+run_diff
+assert_patched tx
+assert_not_patched rx
+assert_patched extra
+assert_section ".bss.$extra_state"
+assert_reloc_sym .text.extra "$extra_state"
+run_post_link
+assert_original_reference tx "$tx_state"
+
+build_pair identity/static_scope_reorder.c
+rx_state="$(identity_object_for orig.o rx)"
+[ -n "$rx_state" ] || fail "missing RX static reference"
+
+run_diff
+assert_patched rx
+assert_not_patched tx
+out_symbols | awk '$4 == "OBJECT" && $7 != "UND" && $8 ~ /bytes/' | grep -q . &&
+	fail "existing static state was copied"
+run_post_link
+assert_original_reference rx "$rx_state"
+
+pass "named scopes preserve state through renumbering and additions"
diff --git a/tools/objtool/tests/generic/test-identity-thinlto-name-drift.sh b/tools/objtool/tests/generic/test-identity-thinlto-name-drift.sh
new file mode 100755
index 000000000000..75374bcbb046
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-thinlto-name-drift.sh
@@ -0,0 +1,79 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# A bounds check stops importing a_get(), leaving its state local.
+# With -always-rename-promoted-locals=false (used by the kernel when
+# supported), the plain promoted name moves to d.c and ELF-only matching
+# silently redirects a_get() to d.c's state. Older LLVM loses correlation
+# and copies the state instead. Both must retain a.c's state.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+clang_only "ThinLTO requires clang"
+find_thinlto_toolchain || probe_skip "no matching clang/lld pair"
+CC="$THIN_CC"
+FIXTURE_CFLAGS="$FIXTURE_CFLAGS -flto=thin -fsplit-lto-unit -fvisibility=hidden"
+
+build_one identity/promotion_bounds_a.c orig-a.o
+build_one identity/promotion_bounds_a.c patched-a.o -DPATCHED
+build_one identity/promotion_bounds_b.c b.o
+build_one identity/promotion_bounds_d.c orig-d.o
+
+# Match the kernel's ThinLTO import limit and promotion naming policy.
+link_flags=(--lto-whole-program-visibility -mllvm -import-instr-limit=5)
+rename_flags=()
+if "$THIN_LD" -r -mllvm -always-rename-promoted-locals=false \
+	"$workdir/orig-a.o" -o "$workdir/rename-probe.o" \
+	2>"$workdir/rename-probe.log"; then
+	rename_flags=(-mllvm -always-rename-promoted-locals=false)
+fi
+# Change d.c as well so neither TU keeps its original ThinLTO module hash.
+for change in one both; do
+	flags=()
+	[ "$change" = both ] && flags=(-DPATCHED)
+	build_one identity/promotion_bounds_d.c patched-d.o "${flags[@]}"
+	for side in orig patched; do
+		"$THIN_LD" -r "${link_flags[@]}" "${rename_flags[@]}" \
+			"$workdir/$side-a.o" "$workdir/b.o" "$workdir/$side-d.o" \
+			-o "$workdir/$side.o" 2>"$workdir/$side-link.log" ||
+			fail "ThinLTO link failed: $(tail -1 "$workdir/$side-link.log")"
+	done
+
+	state_a="$(identity_relocs orig.o .rela.text.a_get |
+		awk '$5 ~ /^state(\.llvm\.[0-9]+)?$/ { print $5; exit }')"
+	state_d="$(identity_relocs orig.o .rela.text.d_get |
+		awk '$5 ~ /^state(\.llvm\.[0-9]+)?$/ { print $5; exit }')"
+	[ -n "$state_a" ] && [ -n "$state_d" ] && [ "$state_a" != "$state_d" ] ||
+		probe_skip "ThinLTO did not emit distinct promoted state references"
+	[ "$(identity_object_for patched.o a_get)" = state ] ||
+		probe_skip "the bounds check did not leave a_get's state local"
+	in_symbols patched.o | awk '$4 == "OBJECT" && $5 == "LOCAL" && $8 == "state"' |
+		grep -q . || probe_skip "missing local state after the bounds check"
+
+	if [ "${#rename_flags[@]}" -ne 0 ]; then
+		[ "$state_a" = state ] && [[ "$state_d" = state.llvm.* ]] ||
+			probe_skip "a_get's state did not claim the original plain name"
+		in_symbols patched.o | awk '$4 == "OBJECT" && $5 == "GLOBAL" && $8 == "state"' |
+			grep -q . || probe_skip "the plain promoted name did not move to d.c"
+	fi
+
+	run_diff
+	assert_patched a_get
+	assert_patched target
+	if [ "$change" = both ]; then
+		assert_patched d_get
+	else
+		assert_not_patched d_get
+	fi
+	out_symbols | awk '$4 == "OBJECT" && $7 != "UND" && $8 ~ /^state(\.|$)/' |
+		grep -q . && fail "unchanged state was allocated again"
+	run_post_link
+	assert_original_reference a_get "$state_a"
+	assert_original_reference target "$state_d"
+
+	if [ "$change" = both ]; then
+		assert_original_reference d_get "$state_d"
+	fi
+done
+
+pass "ThinLTO import changes preserve both TUs' original state"
diff --git a/tools/objtool/tests/generic/test-identity-thinlto-promotion.sh b/tools/objtool/tests/generic/test-identity-thinlto-promotion.sh
new file mode 100755
index 000000000000..f4e9418817a0
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-thinlto-promotion.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# A new caller makes ThinLTO import rx() and promote rx.c's copy of
+# record()'s static state; the patch must keep using the original state.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+build_identity_pair thin "" identity/tx.c identity/rx.c identity/entries.c
+
+in_symbols orig.o | awk '$4 == "OBJECT" && $5 == "LOCAL" && $8 == "record.state"' |
+	grep -q . || probe_skip "RX state was not local before importing"
+[ "$(in_symbols patched.o |
+	awk '$4 == "OBJECT" && $5 == "GLOBAL" && $8 ~ /^record.state.llvm./' | wc -l)" = 2 ] ||
+	probe_skip "ThinLTO did not promote both state instances"
+rx_state="$(identity_object_for orig.o rx)"
+[ -n "$rx_state" ] || fail "missing RX static reference"
+
+run_diff
+assert_patched entry_rx
+out_symbols | awk '$4 == "OBJECT" && $7 != "UND" && $8 ~ /record.state/' | grep -q . &&
+	fail "promoted state was allocated again"
+run_post_link
+assert_original_reference entry_rx "$rx_state"
+
+pass "header state retains its defining TU across promotion"
diff --git a/tools/objtool/tests/generic/test-identity-weak-cold.sh b/tools/objtool/tests/generic/test-identity-weak-cold.sh
new file mode 100755
index 000000000000..a21c522d5702
--- /dev/null
+++ b/tools/objtool/tests/generic/test-identity-weak-cold.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# Overridden weak functions can leave cold fragments with the strong function's name.
+. "$(dirname "$0")/../identity-lib.sh"
+
+gcc_only "GCC splits functions into cold fragments"
+setup
+split_flag=-freorder-blocks-and-partition
+cc_supports "$split_flag" || split_flag=
+
+for flags in "" "-DCHANGE_STRONG"; do
+	build_identity_pair none "$split_flag $flags" \
+		identity/cold_weak.c identity/cold_strong.c identity/cold_user.c
+	for obj in orig.o patched.o; do
+		nr="$(in_symbols "$obj" | awk '
+			$4 == "FUNC" && $8 ~ /^foo\.cold(\.[0-9]+)?$/ { n++ }
+			END { print n+0 }')"
+		[ "$nr" = 2 ] || probe_skip "compiler did not emit both cold fragments"
+	done
+
+	run_diff
+	assert_patched user
+	if [ -n "$flags" ]; then
+		assert_patched foo
+		out_symbols | awk '$4 == "FUNC" && $7 != "UND" && $8 ~ /^foo\.cold(\.[0-9]+)?$/' |
+			grep -q . || fail "strong cold fragment was not copied"
+		assert_symbol strong_slow_path
+		assert_no_symbol weak_slow_path
+	else
+		assert_not_patched foo
+	fi
+done
+
+pass "weak and strong cold fragments retain separate source identities"
diff --git a/tools/objtool/tests/x86/fixtures/identity_assembly.S b/tools/objtool/tests/x86/fixtures/identity_assembly.S
new file mode 100644
index 000000000000..eb2c1ca9d71f
--- /dev/null
+++ b/tools/objtool/tests/x86/fixtures/identity_assembly.S
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+	.file "identity_assembly.S"
+	.section .modinfo,"a"
+	.asciz "name=vmlinux"
+
+	.section .data.asm_state,"aw"
+	.type asm_state, @object
+asm_state:
+	.quad 11
+	.size asm_state, .-asm_state
+
+	.section .text.asm_unchanged,"ax"
+	.globl asm_unchanged
+	.type asm_unchanged, @function
+asm_unchanged:
+	mov asm_state(%rip), %rax
+	ret
+	.size asm_unchanged, .-asm_unchanged
+
+	.section .text.asm_target,"ax"
+	.globl asm_target
+	.type asm_target, @function
+asm_target:
+	mov asm_state(%rip), %rax
+#ifdef PATCHED
+	add $2, %rax
+#else
+	add $1, %rax
+#endif
+	ret
+	.size asm_target, .-asm_target
+	.section .note.GNU-stack,"",@progbits
diff --git a/tools/objtool/tests/x86/fixtures/warn_reference.c b/tools/objtool/tests/x86/fixtures/warn_reference.c
new file mode 100644
index 000000000000..1d5ffda88c75
--- /dev/null
+++ b/tools/objtool/tests/x86/fixtures/warn_reference.c
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com> */
+static const char modinfo[] __attribute__((used, section(".modinfo"))) = "name=vmlinux";
+extern void report(void *entry);
+
+/* WARN passes its own bug-table entry to the reporting code. */
+#define WARN_ENTRY() do { \
+	void *entry; \
+	asm volatile("1: nop\n\t" \
+		     ".pushsection __bug_table,\"aw\"\n\t" \
+		     "2:\n\t" \
+		     ".pushsection .discard.annotate_data,\"M\",@progbits,8\n\t" \
+		     ".long 2b - ., 1\n\t" \
+		     ".popsection\n\t" \
+		     ".long 1b - .\n\t" \
+		     ".fill 8,1,0\n\t" \
+		     ".popsection\n\t" \
+		     "leaq 2b(%%rip), %0" : "=r" (entry)); \
+	report(entry); \
+} while (0)
+
+int other(int value)
+{
+	WARN_ENTRY();
+	return value;
+}
+
+int target(int value)
+{
+	WARN_ENTRY();
+#ifdef PATCHED
+	return value + 2;
+#else
+	return value + 1;
+#endif
+}
diff --git a/tools/objtool/tests/x86/test-identity-assembly.sh b/tools/objtool/tests/x86/test-identity-assembly.sh
new file mode 100755
index 000000000000..4aee7ee7f279
--- /dev/null
+++ b/tools/objtool/tests/x86/test-identity-assembly.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# Assembly functions without their own DIEs still reuse existing data.
+. "$(dirname "$0")/../identity-lib.sh"
+
+setup
+build_pair identity_assembly.S
+assert_input_symbol asm_state
+assert_input_symbol asm_target
+
+run_diff
+assert_patched asm_target
+assert_not_patched asm_unchanged
+assert_no_symbol asm_state
+run_post_link
+assert_original_reference asm_target asm_state
+
+pass "assembly function retains the original data reference"
diff --git a/tools/objtool/tests/x86/test-warn-reference.sh b/tools/objtool/tests/x86/test-warn-reference.sh
new file mode 100755
index 000000000000..48c8042e0cbd
--- /dev/null
+++ b/tools/objtool/tests/x86/test-warn-reference.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Longjun Luo <luolongjuna@gmail.com>
+# A patched WARN site needs the bug-table entry it passes to the reporter.
+. "$(dirname "$0")/../lib.sh"
+
+setup
+build_pair warn_reference.c
+assert_input_section __bug_table
+in_relocs patched.o | grep -q 'R_X86_64_PC32.*__bug_table' ||
+	fail "missing function reference to __bug_table"
+
+run_diff
+assert_patched target
+assert_not_patched other
+assert_section __bug_table
+assert_reloc_sym __bug_table target
+assert_no_reloc_sym __bug_table other
+assert_reloc_count __bug_table 1
+
+pass "patched WARN site retains its own bug-table entry"
-- 
2.55.0


^ permalink raw reply	[flat|nested] 12+ messages in thread

end of thread, other threads:[~2026-11-04 13:33 UTC | newest]

Thread overview: 12+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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 ` [RFC PATCH v2 07/11] objtool/klp: Correlate symbols using DWARF identities Longjun Luo
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

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®