All of lore.kernel.org
 help / color / mirror / Atom feed
From: Alan Derk <linux@alanderk.com>
To: Arnaldo Carvalho de Melo <acme@kernel.org>,
	Alan Maguire <alan.maguire@oracle.com>
Cc: dwarves@vger.kernel.org, bpf@vger.kernel.org,
	rust-for-linux@vger.kernel.org
Subject: [PATCH dwarves v2 2/2] dwarf_loader: Load types that kept CUs use from excluded CUs
Date: Sat, 10 Oct 2026 00:52:27 +0000	[thread overview]
Message-ID: <20261010005227.976-3-linux@alanderk.com> (raw)
In-Reply-To: <20261010005227.976-1-linux@alanderk.com>

With clang LTO, LLVM emits a type such as int once, in the first CU
that uses it, and other CUs refer to it with DW_FORM_ref_addr. Since
commit b98565e7b17ec24d ("dwarf_loader: Honour --lang_exclude when
merging LTO built CUs") the DIEs of the CUs that --lang_exclude filters
out are skipped when merging, so a kept CU that uses a type from one of
them is left with a dangling reference. Members that use such a type
end up typed void: pahole crashes printing them, and -J encodes them
with type_id 0 and exits 0.

  $ head zero.c first.cpp second.c
  ==> zero.c <==
  char zero_c;

  ==> first.cpp <==
  int cpp_int;

  ==> second.c <==
  struct c_only { int b; } c_var;
  $ clang -flto -g -c zero.c second.c; clang++ -flto -g -c first.cpp
  $ clang -flto -fuse-ld=lld -shared -o three.so zero.o first.o second.o
  $ pahole --lang_exclude=c++14 three.so
  namespace__recode_dwarf_types: couldn't find 0x57 type for 0x8a (member)!
  struct c_only {
  	<ERROR(__class__fprintf:1838): 0 not found!>
  Segmentation fault

Remember the offsets of the CUs that the filter skips, and when a type
lookup misses an offset in one of them, load that DIE if it is a type.
The types it refers to are looked up when it is recoded, so they are
loaded too. As this adds types while recoding, cu__recode_dwarf_types()
now repeats its passes until no new tags show up.

After:

  $ pahole --lang_exclude=c++14 three.so
  struct c_only {
  	int                        b;                    /*     0     4 */
  ...

Only types are loaded. References to functions and variables in
excluded CUs, such as the abstract origin of a function inlined across
CUs or the function that a C++ using declaration names, still print
"couldn't find", as loading them would add functions in the excluded
language to the output. A nested C++ type that a kept CU refers to
directly is loaded as a top-level type.

Without --lang_exclude, or when no kept CU refers to an excluded one,
nothing is loaded and the output does not change.

Add tests/lang_exclude_lto_refs.sh for this.

Tested with clang 21 full LTO and ThinLTO builds of a C++/C program
in both link orders. Only base types (int, char, long) were loaded, and
apart from the order of the types the output matches that of the other
link order: text and BTF with C++ excluded, text with C excluded. Two
C++ objects built with -std=c++11 and -std=c++17 that include <cstdio>
give the same text with c++11 excluded in either order; there the C
library types they share, such as FILE, are loaded too. ASan reports no
errors on these apart from a leak with -J that predates this series.
gcc 16 LTO, Rust+C ThinLTO and Debian debuginfo processed by dwz load
nothing and do not change, and neither does the BTF of 7.2.9 x86_64 and
arm64 ThinLTO kernels with Rust enabled.

Fixes: b98565e7b17ec24d ("dwarf_loader: Honour --lang_exclude when merging LTO built CUs")
Assisted-by: LLM
Signed-off-by: Alan Derk <linux@alanderk.com>
---
 dwarf_loader.c                 | 95 +++++++++++++++++++++++++++++++---
 tests/lang_exclude_lto_refs.sh | 76 +++++++++++++++++++++++++++
 2 files changed, 164 insertions(+), 7 deletions(-)
 create mode 100755 tests/lang_exclude_lto_refs.sh

diff --git a/dwarf_loader.c b/dwarf_loader.c
index fbed36e..10ab37a 100644
--- a/dwarf_loader.c
+++ b/dwarf_loader.c
@@ -169,6 +169,11 @@ struct alt_pu {
 	bool	  hit;		/* referenced by main CUs? */
 };
 
+struct excluded_cu {
+	Dwarf_Off start;
+	Dwarf_Off end;
+};
+
 struct dwarf_cu {
 	struct hlist_head *hash_tags;	/* tags (functions, etc.) keyed by DWARF offset */
 	struct hlist_head *hash_types;	/* types keyed by DWARF offset */
@@ -183,6 +188,10 @@ struct dwarf_cu {
 	uint32_t  nr_alt_pus;
 	uint32_t  allocated_alt_pus;
 	bool	  processing_alt;	/* true while processing alt-file DIEs */
+	struct excluded_cu *excluded_cus;	/* skipped by early_cu_filter when merging */
+	uint32_t  nr_excluded_cus;
+	Dwarf	  *dw;
+	struct conf_load *conf;
 };
 
 static int dwarf_cu__init(struct dwarf_cu *dcu, struct cu *cu)
@@ -215,6 +224,8 @@ static int dwarf_cu__init(struct dwarf_cu *dcu, struct cu *cu)
 	dcu->alt_pus = NULL;
 	dcu->nr_alt_pus = 0;
 	dcu->allocated_alt_pus = 0;
+	dcu->excluded_cus = NULL;
+	dcu->nr_excluded_cus = 0;
 	/*
 	 * processing_alt is per-merged-CU state, not thread-safe.
 	 * The merged-CU path (cus__merge_and_process_cu) is
@@ -249,6 +260,7 @@ static void dwarf_cu__delete(struct cu *cu)
 
 	zfree(&dcu->imported_units);
 	zfree(&dcu->alt_pus);
+	zfree(&dcu->excluded_cus);
 	if (dcu->alt) {
 		cu__free(cu, dcu->alt->hash_tags);
 		cu__free(cu, dcu->alt->hash_types);
@@ -475,6 +487,8 @@ static struct dwarf_tag *__dwarf_cu__find_tag_by_ref(const struct dwarf_cu *cu,
 				    dtag->from_types_section.field, \
 				    dtag->from_alt.field)
 
+static struct dwarf_tag *dwarf_cu__load_excluded_type(struct dwarf_cu *dcu, Dwarf_Off ref);
+
 /* Same as __dwarf_cu__find_tag_by_ref but for type lookups (hash_types). */
 static struct dwarf_tag *__dwarf_cu__find_type_by_ref(struct dwarf_cu *dcu,
 						      const Dwarf_Off ref,
@@ -497,6 +511,8 @@ static struct dwarf_tag *__dwarf_cu__find_type_by_ref(struct dwarf_cu *dcu,
 
 	struct dwarf_tag *dtag = hashtags__find(dcu->hash_types, ref);
 
+	if (dtag == NULL)
+		dtag = dwarf_cu__load_excluded_type(dcu, ref);
 	if (dtag)
 		dcu->last_type_lookup = dtag;
 
@@ -3693,6 +3709,62 @@ static int die__process_unit(Dwarf_Die *die, struct cu *cu, struct conf_load *co
 	return 0;
 }
 
+static int dwarf_cu__add_excluded_cu(struct dwarf_cu *dcu, Dwarf_Off start, Dwarf_Off end)
+{
+	struct excluded_cu *cus = realloc(dcu->excluded_cus,
+					  (dcu->nr_excluded_cus + 1) * sizeof(*cus));
+
+	if (cus == NULL)
+		return -ENOMEM;
+
+	cus[dcu->nr_excluded_cus++] = (struct excluded_cu){ .start = start, .end = end };
+	dcu->excluded_cus = cus;
+	return 0;
+}
+
+static bool dwarf_cu__in_excluded_cu(const struct dwarf_cu *dcu, Dwarf_Off offset)
+{
+	for (uint32_t i = 0; i < dcu->nr_excluded_cus; i++)
+		if (offset >= dcu->excluded_cus[i].start && offset < dcu->excluded_cus[i].end)
+			return true;
+	return false;
+}
+
+/*
+ * With LTO, LLVM emits a type such as int once, in the first CU that uses it,
+ * and other CUs refer to it with DW_FORM_ref_addr. So the CUs kept when merging
+ * can refer to types in CUs that early_cu_filter excluded. Load those types
+ * when they are looked up, and nothing else from the excluded CUs.
+ */
+static struct dwarf_tag *dwarf_cu__load_excluded_type(struct dwarf_cu *dcu, Dwarf_Off ref)
+{
+	struct tag *tag, die_tag = {};
+	uint32_t id = 0;
+	Dwarf_Die die;
+
+	if (!dwarf_cu__in_excluded_cu(dcu, ref) || dwarf_offdie(dcu->dw, ref, &die) == NULL)
+		return NULL;
+
+	/*
+	 * A using declaration can name a function, and processing a function
+	 * adds its parameters to the CU's tables, so check the tag first.
+	 */
+	die_tag.tag = dwarf_tag(&die);
+	if (!tag__is_tag_type(&die_tag))
+		return NULL;
+
+	tag = die__process_tag(&die, dcu->cu, 1, dcu->conf);
+	if (tag == NULL || tag == &unsupported_tag)
+		return NULL;
+
+	/* As in die__process_unit(). */
+	if (tag->tag != DW_TAG_unspecified_type)
+		cu__add_tag(dcu->cu, tag, &id);
+	cu__hash(dcu->cu, tag);
+	tag__dwarf(tag)->small_id = id;
+	return tag__dwarf(tag);
+}
+
 static void ftype__recode_dwarf_types(struct tag *tag, struct cu *cu);
 
 static int namespace__recode_dwarf_types(struct tag *tag, struct cu *cu)
@@ -4497,10 +4569,10 @@ static int cu__resolve_func_ret_types_optimized(struct cu *cu, struct conf_load
 
 static int cu__recode_dwarf_types_table(struct cu *cu,
 					struct ptr_table *pt,
-					uint32_t i)
+					uint32_t *i)
 {
-	for (; i < pt->nr_entries; ++i) {
-		struct tag *tag = pt->entries[i];
+	for (; *i < pt->nr_entries; ++*i) {
+		struct tag *tag = pt->entries[*i];
 
 		if (tag != NULL) /* void, see cu__new */
 			if (tag__recode_dwarf_type(tag, cu))
@@ -4512,10 +4584,15 @@ static int cu__recode_dwarf_types_table(struct cu *cu,
 
 static int cu__recode_dwarf_types(struct cu *cu)
 {
-	if (cu__recode_dwarf_types_table(cu, &cu->types_table, 1) ||
-	    cu__recode_dwarf_types_table(cu, &cu->tags_table, 0) ||
-	    cu__recode_dwarf_types_table(cu, &cu->functions_table, 0))
-		return -1;
+	uint32_t types = 1, tags = 0, functions = 0;
+
+	/* Recoding can add tags, see dwarf_cu__load_excluded_type(). */
+	do {
+		if (cu__recode_dwarf_types_table(cu, &cu->types_table, &types) ||
+		    cu__recode_dwarf_types_table(cu, &cu->tags_table, &tags) ||
+		    cu__recode_dwarf_types_table(cu, &cu->functions_table, &functions))
+			return -1;
+	} while (types < cu->types_table.nr_entries || tags < cu->tags_table.nr_entries);
 	return 0;
 }
 
@@ -5366,6 +5443,8 @@ static int cus__merge_and_process_cu(struct cus *cus, struct conf_load *conf,
 
 			dcu->cu = cu;
 			dcu->type_unit = type_dcu;
+			dcu->dw = dw;
+			dcu->conf = conf;
 			cu->priv = dcu;
 			cu->dfops = &dwarf__ops;
 
@@ -5451,6 +5530,8 @@ static int cus__merge_and_process_cu(struct cus *cus, struct conf_load *conf,
 			};
 
 			filtered = conf->early_cu_filter(&unmerged_cu) == NULL;
+			if (filtered && dwarf_cu__add_excluded_cu(dcu, off, noff))
+				goto out_abort;
 		}
 
 		/*
diff --git a/tests/lang_exclude_lto_refs.sh b/tests/lang_exclude_lto_refs.sh
new file mode 100755
index 0000000..56bcb7b
--- /dev/null
+++ b/tests/lang_exclude_lto_refs.sh
@@ -0,0 +1,76 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Test --lang_exclude on clang LTO objects, where a kept CU refers to a type
+# in an excluded CU: LLVM emits int once, in the first CU that uses it.
+
+. "$(dirname "$0")/test_lib.sh"
+outdir=$(make_tmpdir)
+
+trap cleanup EXIT
+
+title_log "Language filtering of LTO CUs that refer to excluded CUs."
+
+for tool in clang clang++ ld.lld; do
+	if ! command -v $tool > /dev/null 2>&1; then
+		info_log "skip: $tool not available"
+		test_skip
+	fi
+done
+
+echo 'char zero_c;' > "$outdir/zero.c"
+
+cat > "$outdir/first.cpp" << 'EOF'
+struct cpp_only {
+	int	a;
+};
+
+struct cpp_only cpp_var;
+EOF
+
+cat > "$outdir/second.c" << 'EOF'
+struct c_only {
+	int	b;
+};
+
+struct c_only c_var;
+EOF
+
+obj="$outdir/lto.so"
+
+if ! clang -flto -g -c -o "$outdir/zero.o" "$outdir/zero.c" 2>/dev/null ||
+   ! clang++ -flto -g -c -o "$outdir/first.o" "$outdir/first.cpp" 2>/dev/null ||
+   ! clang -flto -g -c -o "$outdir/second.o" "$outdir/second.c" 2>/dev/null ||
+   ! clang -flto -fuse-ld=lld -shared -o "$obj" "$outdir/zero.o" \
+	"$outdir/first.o" "$outdir/second.o" 2>/dev/null; then
+	error_log "FAIL: compilation failed"
+	test_fail
+fi
+
+lang_exclude=c++,c++03,c++11,c++14
+btf="$outdir/lto.btf"
+
+check()
+{
+	if [ $1 -ne 0 ]; then
+		error_log "FAIL: $2: pahole failed"
+		test_fail
+	fi
+	if echo "$3" | grep -q "struct cpp_only"; then
+		error_log "FAIL: $2: --lang_exclude=c++ kept the C++ CU"
+		test_fail
+	fi
+	if ! echo "$3" | grep -Eq "^[[:space:]]+int[[:space:]]+b;"; then
+		error_log "FAIL: $2: c_only.b lost its type, int from the C++ CU"
+		test_fail
+	fi
+}
+
+output=$(pahole -F dwarf --lang_exclude=$lang_exclude "$obj" 2>/dev/null)
+check $? "DWARF" "$output"
+
+pahole -J --btf_encode_detached="$btf" --lang_exclude=$lang_exclude "$obj" > /dev/null 2>&1
+output=$(pahole -F btf "$btf" 2>/dev/null)
+check $? "BTF" "$output"
+
+test_pass
-- 
2.55.0


      parent reply	other threads:[~2026-10-10  0:52 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-10  0:52 [PATCH dwarves v2 0/2] dwarf_loader: Fix --lang_exclude on merged CUs Alan Derk
2026-10-10  0:52 ` [PATCH dwarves v2 1/2] dwarf_loader: Take the merged CU language from a CU that is kept Alan Derk
2026-10-10  0:52 ` Alan Derk [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261010005227.976-3-linux@alanderk.com \
    --to=linux@alanderk.com \
    --cc=acme@kernel.org \
    --cc=alan.maguire@oracle.com \
    --cc=bpf@vger.kernel.org \
    --cc=dwarves@vger.kernel.org \
    --cc=rust-for-linux@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.