Skip to content

Commit b568ea5

Browse files
committed
tests: validate USDT note integrity under linker GC and COMDAT dedup
Add two regression tests for USDT note integrity when the linker removes probe site code: - gc_sections defines probes in an uncalled function and is built with -ffunction-sections and -Wl,--gc-sections, validating that garbage-collected probe code doesn't leave dangling notes behind (and that live probes' base/semaphore references survive, which is asserted by the BASE*/SEMA* stubs in USDT_SPECS); - cxx_comdat triggers a USDT in a C++ inline function instantiated in two translation units, validating that COMDAT deduplication discards the corresponding note copy instead of failing the link (BFD ld) or leaving a dangling note (LLD). Detection is implemented in check-usdt-locs.awk, which run_test.sh now runs for every test binary (and shared library): each USDT note's location must fall inside an executable PT_LOAD segment of its ELF file, mirroring the lookup tracing tools perform at attach time (e.g., libbpf's find_elf_seg()); a location outside any executable segment means the note's relocation was resolved against a section discarded by the linker.
1 parent 33dff85 commit b568ea5

7 files changed

Lines changed: 194 additions & 5 deletions

File tree

‎tests/Makefile‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ $(OBJDIR)/long_nop_usdt.o: CFLAGS += -w
8080
$(OBJDIR)/cxx_arg_types.opp: CXXFLAGS += -w
8181
$(OBJDIR)/cxx_long_nop_usdt.opp: CXXFLAGS += -w
8282

83+
# gc_sections validates USDT note integrity under linker section GC
84+
$(OBJDIR)/gc_sections.o: CFLAGS += -ffunction-sections -fdata-sections
85+
$(OBJDIR)/gc_sections: LDFLAGS += -Wl,--gc-sections
86+
8387
$(OBJDIR)/%.o: %.c $(wildcard *.h) | $(OBJDIR)
8488
$(call msg,CC,$@)
8589
$(Q)$(CC) $(CFLAGS) -g -c $< -o $@

‎tests/check-usdt-locs.awk‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# SPDX-License-Identifier: BSD-2-Clause
2+
#
3+
# Validate that every USDT note's location falls inside an executable
4+
# PT_LOAD segment of its ELF file, mirroring the lookup tracing tools
5+
# perform (e.g., libbpf's find_elf_seg()). A location that lands in no
6+
# segment means the note's relocation was resolved against a section the
7+
# linker discarded (garbage-collected or deduplicated COMDAT), leaving a
8+
# dangling note that breaks probe attachment.
9+
#
10+
# Input is the concatenated output of `readelf -lW <file>` and
11+
# `readelf -n <file>` for a single ELF file:
12+
#
13+
# LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x0004f8 0x0004f8 R E 0x1000
14+
# ...
15+
# Provider: test
16+
# Name: kept
17+
# Location: 0x0000000000401234, Base: ..., Semaphore: ...
18+
#
19+
# Exits non-zero listing each dangling note.
20+
21+
BEGIN { nsegs = 0; }
22+
23+
# readelf -lW prints the flag column with a space inside it ("R E"), so it is
24+
# split across fields; the trailing align column is always last. Reconstruct
25+
# the flags from everything between memsz ($6) and align ($NF).
26+
$1 == "LOAD" {
27+
flags = "";
28+
for (i = 7; i < NF; i++)
29+
flags = flags $i;
30+
if (flags ~ /E/) {
31+
seg_start[nsegs] = strtonum($3);
32+
seg_end[nsegs] = seg_start[nsegs] + strtonum($6);
33+
nsegs++;
34+
}
35+
}
36+
37+
/Provider:/ { grp = $2; }
38+
/Name:/ { name = $2; }
39+
40+
/Location:/ {
41+
loc = strtonum($2);
42+
for (i = 0; i < nsegs; i++) {
43+
if (loc >= seg_start[i] && loc < seg_end[i])
44+
next;
45+
}
46+
printf "%s:%s location 0x%x is in no executable segment (dangling note)\n",
47+
grp, name, loc;
48+
bad = 1;
49+
}
50+
51+
END {
52+
exit bad;
53+
}

‎tests/cxx_comdat.h‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/* SPDX-License-Identifier: BSD-2-Clause */
2+
#ifndef __CXX_COMDAT_H__
3+
#define __CXX_COMDAT_H__
4+
5+
#include "../usdt.h"
6+
#include "common.h"
7+
8+
/*
9+
* A USDT inside a C++ inline function defined in a header: every TU that
10+
* uses it emits its own out-of-line copy in a COMDAT group, the linker keeps
11+
* one copy and discards the rest. The .note.stapsdt entry emitted next to
12+
* each copy is not part of that COMDAT group, so the notes of the discarded
13+
* copies are left referencing a discarded section:
14+
* - BFD ld fails the link outright ("`.text._Z...' referenced in section
15+
* `.note.stapsdt' ... defined in discarded section");
16+
* - LLD silently resolves the note's location to 0 + addend, producing a
17+
* dangling note (caught by run_test.sh's location validity check).
18+
*
19+
* noinline forces the out-of-line COMDAT copy to exist at any optimization
20+
* level; a fully inlined copy would produce a valid note at every inline
21+
* site instead.
22+
*/
23+
inline __attribute__((noinline)) void cxx_comdat_probe(int x)
24+
{
25+
USDT(test, comdat, x);
26+
}
27+
28+
extern void cxx_comdat_other_func(int x);
29+
30+
#endif /* __CXX_COMDAT_H__ */

‎tests/cxx_comdat1.cpp‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// SPDX-License-Identifier: BSD-2-Clause
2+
#include <cstdio>
3+
#include "common.h"
4+
#include "cxx_comdat.h"
5+
6+
/*
7+
* See cxx_comdat.h: both this file and cxx_comdat2.cpp instantiate
8+
* cxx_comdat_probe()'s out-of-line COMDAT copy; the linker discards one of
9+
* them and its .note.stapsdt entry must go with it (and not dangle or fail
10+
* the link). One or two comdat notes may legitimately survive depending on
11+
* the linker, so USDT_SPECS pins down just the first one; run_test.sh
12+
* rejects dangling leftovers.
13+
*/
14+
15+
int main(int argc, char **argv)
16+
{
17+
if (handle_args(argc, argv))
18+
return 0;
19+
20+
cxx_comdat_probe(1);
21+
cxx_comdat_other_func(2);
22+
23+
return 0;
24+
}
25+
26+
const char *USDT_SPECS =
27+
"test:comdat base=* sema=0 argn=1 args=*.\n"
28+
"...\n"
29+
;

‎tests/cxx_comdat2.cpp‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// SPDX-License-Identifier: BSD-2-Clause
2+
#include "common.h"
3+
#include "cxx_comdat.h"
4+
5+
void __optimize cxx_comdat_other_func(int x)
6+
{
7+
cxx_comdat_probe(x);
8+
}

‎tests/gc_sections.c‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// SPDX-License-Identifier: BSD-2-Clause
2+
#include <stdio.h>
3+
#include "common.h"
4+
#include "../usdt.h"
5+
6+
/*
7+
* Validate USDT note integrity when the linker garbage-collects sections.
8+
* This test is compiled with -ffunction-sections -fdata-sections and linked
9+
* with -Wl,--gc-sections (see Makefile).
10+
*
11+
* gc_collected_func() is never called, so its section is eligible for
12+
* collection. .note.stapsdt is a non-SHF_ALLOC section and linkers differ in
13+
* whether references from it keep sections alive:
14+
* - BFD ld treats them as GC roots, so the unused function (and its
15+
* semaphore) survive and the note stays valid;
16+
* - LLD does not follow them: the function is collected and the note's
17+
* relocation against the collected section silently resolves to
18+
* 0 + addend, leaving a dangling note whose "location" is the nop's
19+
* offset inside the discarded section (tiny bogus value). The implicit
20+
* semaphore in .probes and the .stapsdt.base section are collected the
21+
* same way, zeroing the note's semaphore/base fields.
22+
* Tools iterating notes then trip on the dangling entry; e.g. libbpf fails
23+
* the entire probe, including its valid same-name copies.
24+
*
25+
* run_test.sh fails any test whose binary contains a note whose location
26+
* falls in no executable segment, which is what this test is really
27+
* asserting.
28+
* Which of gc_collected_func()'s notes survive (and with what semaphore
29+
* values) is linker-dependent, so USDT_SPECS only pins down the live
30+
* probes and accepts anything extra. The BASE* and SEMA* stubs (as opposed
31+
* to plain *) assert that the kept probes' .stapsdt.base and semaphore
32+
* references stayed intact (LLD used to garbage-collect those sections and
33+
* zero out the fields even in surviving notes).
34+
*/
35+
36+
void gc_collected_func(int x)
37+
{
38+
USDT(test, collected, x);
39+
USDT_WITH_SEMA(test, collected_sema, x);
40+
}
41+
42+
int main(int argc, char **argv)
43+
{
44+
if (handle_args(argc, argv))
45+
return 0;
46+
47+
USDT(test, kept, 1);
48+
USDT_WITH_SEMA(test, kept_sema, 2);
49+
50+
return 0;
51+
}
52+
53+
const char *USDT_SPECS =
54+
"...\n"
55+
"test:kept base=BASE* sema=0 argn=1 args=*.\n"
56+
"test:kept_sema base=BASE* sema=SEMA* argn=1 args=*.\n"
57+
"...\n"
58+
;

‎tests/run_test.sh‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,20 @@ TEST_BTOUT=$TEST_BIN.act_bt_out.txt
2929
TEST_OUT_SPEC=$TEST_BIN.exp_out.txt
3030
TEST_OUT=$TEST_BIN.act_out.txt
3131

32-
$TEST_BIN -U > $TEST_USDTS_SPEC
32+
TEST_ELFS=$TEST_BIN
3333
if [ "${SHARED:-0}" -eq 1 ] && [ -e "$TEST_LIB" ]; then
34-
# append lib's USDT notes to USDTs from executable
35-
$readelf -n $TEST_BIN $TEST_LIB 2>/dev/null | $awk -f fetch-usdts.awk > $TEST_USDTS_OUT
36-
else
37-
$readelf -n $TEST_BIN 2>/dev/null | $awk -f fetch-usdts.awk > $TEST_USDTS_OUT
34+
# check lib's USDT notes along with executable's
35+
TEST_ELFS="$TEST_ELFS $TEST_LIB"
3836
fi
37+
38+
$TEST_BIN -U > $TEST_USDTS_SPEC
39+
$readelf -n $TEST_ELFS 2>/dev/null | $awk -f fetch-usdts.awk > $TEST_USDTS_OUT
40+
for elf in $TEST_ELFS; do
41+
if ! { $readelf -lW $elf; $readelf -n $elf; } 2>/dev/null | $awk -f check-usdt-locs.awk; then
42+
echo "DANGLING USDT NOTES in $elf (probe code discarded by linker, note left behind)"
43+
exit 1
44+
fi
45+
done
3946
if ! $awk -f check-match.awk $TEST_USDTS_SPEC $TEST_USDTS_OUT; then
4047
echo "USDT SPECS MISMATCH:"
4148
echo "EXPECTED:"

0 commit comments

Comments
 (0)