Skip to content

Commit f8b2244

Browse files
committed
address review comments
1 parent 109699f commit f8b2244

3 files changed

Lines changed: 8 additions & 318 deletions

File tree

libdd-otel-thread-ctx-ffi/README.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,3 @@ attaching, detaching, and updating per-thread OpenTelemetry context records
55
that external readers (e.g. the eBPF profiler) can discover.
66

77
Currently Linux-only (x86-64 and aarch64).
8-
9-
## TLS
10-
11-
The thread-local variable `otel_thread_ctx_v1` and its TLSDESC accessor are
12-
implemented in pure Rust using `global_asm!` and `asm!` in the
13-
`libdd-otel-thread-ctx` crate.

libdd-otel-thread-ctx-ffi/tests/elf_properties.rs

Lines changed: 2 additions & 307 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@
77
//! - `otel_thread_ctx_v1` is exported in the dynamic symbol table as a TLS GLOBAL symbol.
88
//! - `otel_thread_ctx_v1` follows the TLSDESC access model: if there is a relocation for it, it is
99
//! a TLSDESC relocation.
10-
//! - A native executable that statically links libdd-otel-thread-ctx-ffi without exporting
11-
//! `otel_thread_ctx_v1` has libdd's TLSDESC access relaxed to local-exec TLS, leaving no
12-
//! relocation for `otel_thread_ctx_v1`.
10+
//! - The Rust inline-asm TLSDESC access sequence byte-for-byte matches what a C compiler generates
11+
//! (guaranteeing that linker TLS relaxation works identically to a compiler-generated access).
1312
//!
1413
//! Library artifact paths are derived at runtime from the test executable location.
1514
//! The test binary and crate artifacts live in `target/<[triple/]profile>/deps/`.
@@ -51,12 +50,6 @@ struct TlsDescSequence {
5150
relocations: Vec<TlsDescRelocation>,
5251
}
5352

54-
#[derive(Debug)]
55-
struct ArchiveMemberRelocations {
56-
member_name: String,
57-
relocation_types: Vec<RelocationType>,
58-
}
59-
6053
fn deps_dir() -> PathBuf {
6154
// test binary: target/<[triple/]profile>/deps/<name>
6255
let exe = std::env::current_exe().expect("failed to read current executable path");
@@ -135,12 +128,6 @@ fn command_output(command: &mut Command) -> String {
135128
String::from_utf8_lossy(&out.stdout).into_owned()
136129
}
137130

138-
fn objdump(args: &[&str], path: &Path) -> String {
139-
let mut command = Command::new("objdump");
140-
command.args(args).arg(path);
141-
command_output(&mut command)
142-
}
143-
144131
fn assert_command_success(command: &mut Command) {
145132
let out = command
146133
.output()
@@ -210,77 +197,6 @@ fn symbol_indexes_in_table(
210197
.collect()
211198
}
212199

213-
fn relocation_types_for_symbol_in_elf(
214-
data: &[u8],
215-
symbol: &str,
216-
label: &str,
217-
) -> Vec<RelocationType> {
218-
let elf = parse_elf(data, label);
219-
let Some(section_headers) = elf.section_headers() else {
220-
panic!("{label} has no ELF section headers");
221-
};
222-
let mut relocation_types = Vec::new();
223-
224-
for section_header in section_headers
225-
.iter()
226-
.filter(|shdr| matches!(shdr.sh_type, abi::SHT_REL | abi::SHT_RELA))
227-
{
228-
let symbol_indexes =
229-
symbol_indexes_in_table(&elf, section_header.sh_link as usize, symbol, label);
230-
if symbol_indexes.is_empty() {
231-
continue;
232-
}
233-
234-
match section_header.sh_type {
235-
abi::SHT_REL => {
236-
let rels = elf
237-
.section_data_as_rels(&section_header)
238-
.unwrap_or_else(|e| panic!("failed to read REL relocations in {label}: {e}"));
239-
relocation_types.extend(
240-
rels.filter(|rel| symbol_indexes.contains(&rel.r_sym))
241-
.map(|rel| RelocationType(rel.r_type)),
242-
);
243-
}
244-
abi::SHT_RELA => {
245-
let relas = elf
246-
.section_data_as_relas(&section_header)
247-
.unwrap_or_else(|e| panic!("failed to read RELA relocations in {label}: {e}"));
248-
relocation_types.extend(
249-
relas
250-
.filter(|rela| symbol_indexes.contains(&rela.r_sym))
251-
.map(|rela| RelocationType(rela.r_type)),
252-
);
253-
}
254-
_ => unreachable!(),
255-
}
256-
}
257-
258-
relocation_types
259-
}
260-
261-
fn relocation_types_for_symbol_in_file(path: &Path, symbol: &str) -> Vec<RelocationType> {
262-
let data =
263-
std::fs::read(path).unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()));
264-
relocation_types_for_symbol_in_elf(&data, symbol, &path.display().to_string())
265-
}
266-
267-
fn archive_relocation_types_for_symbol(path: &Path, symbol: &str) -> Vec<ArchiveMemberRelocations> {
268-
let mut relocations = Vec::new();
269-
270-
for_each_archive_elf_member(path, |member_name, member_data| {
271-
let label = format!("{}({member_name})", path.display());
272-
let relocation_types = relocation_types_for_symbol_in_elf(member_data, symbol, &label);
273-
if !relocation_types.is_empty() {
274-
relocations.push(ArchiveMemberRelocations {
275-
member_name: member_name.to_owned(),
276-
relocation_types,
277-
});
278-
}
279-
});
280-
281-
relocations
282-
}
283-
284200
fn for_each_archive_elf_member(path: &Path, mut f: impl FnMut(&str, &[u8])) {
285201
let archive_data =
286202
std::fs::read(path).unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()));
@@ -527,88 +443,6 @@ fn compile_tls_shim_object(dir: &Path) -> PathBuf {
527443
object
528444
}
529445

530-
fn format_relocations(relocations: &[ArchiveMemberRelocations]) -> String {
531-
if relocations.is_empty() {
532-
return "<none>".to_owned();
533-
}
534-
535-
relocations
536-
.iter()
537-
.map(|relocations| {
538-
format!(
539-
"{}: {:?}",
540-
relocations.member_name, relocations.relocation_types
541-
)
542-
})
543-
.collect::<Vec<_>>()
544-
.join("\n")
545-
}
546-
547-
fn is_disassembly_header_for(line: &str, name: &str) -> bool {
548-
let Some((_, symbol)) = line.split_once('<') else {
549-
return false;
550-
};
551-
let Some(symbol) = symbol.strip_suffix(">:") else {
552-
return false;
553-
};
554-
symbol == name
555-
|| symbol
556-
.strip_prefix(name)
557-
.is_some_and(|suffix| suffix.starts_with("::"))
558-
}
559-
560-
fn disassembled_functions(output: &str, name: &str) -> Vec<String> {
561-
let mut functions = Vec::new();
562-
let mut current_function = Vec::new();
563-
564-
for line in output.lines() {
565-
if is_disassembly_header_for(line, name) {
566-
if !current_function.is_empty() {
567-
functions.push(current_function.join("\n"));
568-
current_function.clear();
569-
}
570-
current_function.push(line);
571-
continue;
572-
}
573-
574-
if !current_function.is_empty() {
575-
if line.is_empty() {
576-
functions.push(current_function.join("\n"));
577-
current_function.clear();
578-
continue;
579-
}
580-
current_function.push(line);
581-
}
582-
}
583-
584-
if !current_function.is_empty() {
585-
functions.push(current_function.join("\n"));
586-
}
587-
588-
assert!(
589-
!functions.is_empty(),
590-
"could not find disassembly for {name} in:\n{output}"
591-
);
592-
functions
593-
}
594-
595-
#[cfg(target_arch = "aarch64")]
596-
fn disassembly_window_around_line(
597-
function: &str,
598-
needle: &str,
599-
before: usize,
600-
after: usize,
601-
) -> String {
602-
let lines = function.lines().collect::<Vec<_>>();
603-
let line_index = lines
604-
.iter()
605-
.position(|line| line.contains(needle))
606-
.unwrap_or_else(|| panic!("could not find {needle:?} in:\n{function}"));
607-
let start = line_index.saturating_sub(before);
608-
let end = usize::min(line_index + after + 1, lines.len());
609-
lines[start..end].join("\n")
610-
}
611-
612446
#[test]
613447
#[cfg_attr(miri, ignore)]
614448
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
@@ -663,142 +497,3 @@ fn otel_thread_ctx_v1_tls_properties() {
663497
check_readable(&path);
664498
libdd_otel_thread_ctx::sanity_check::check_tls_slot_in(&path).unwrap();
665499
}
666-
667-
#[test]
668-
#[cfg_attr(miri, ignore)]
669-
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
670-
fn statically_linked_executable_relaxes_libdd_tls_slot_to_local_exec() {
671-
if !native_target() {
672-
return;
673-
}
674-
675-
if !required_tools_available(&["cc", "objdump"]) {
676-
return;
677-
}
678-
679-
let staticlib = staticlib_path();
680-
check_readable(&staticlib);
681-
682-
let dir = build_dir("otel-thread-ctx-local-exec");
683-
let source = dir.join("consumer.c");
684-
let object = dir.join("consumer.o");
685-
let executable = dir.join("consumer");
686-
std::fs::write(
687-
&source,
688-
r#"
689-
#include <stdint.h>
690-
691-
void ddog_otel_thread_ctx_update(
692-
const uint8_t (*trace_id)[16],
693-
const uint8_t (*span_id)[8],
694-
const uint8_t (*local_root_span_id)[8]);
695-
void *ddog_otel_thread_ctx_detach(void);
696-
void ddog_otel_thread_ctx_free(void *ctx);
697-
698-
int main(void) {
699-
uint8_t trace_id[16] = {1};
700-
uint8_t span_id[8] = {2};
701-
uint8_t local_root_span_id[8] = {3};
702-
703-
ddog_otel_thread_ctx_update(&trace_id, &span_id, &local_root_span_id);
704-
void *ctx = ddog_otel_thread_ctx_detach();
705-
ddog_otel_thread_ctx_free(ctx);
706-
707-
return ctx == 0 ? 1 : 0;
708-
}
709-
"#,
710-
)
711-
.unwrap_or_else(|e| panic!("failed to write {}: {e}", source.display()));
712-
713-
let mut compile_object = Command::new("cc");
714-
compile_object.args(["-O2", "-ffunction-sections", "-fdata-sections"]);
715-
compile_object.arg("-c").arg(&source).arg("-o").arg(&object);
716-
assert_command_success(&mut compile_object);
717-
718-
let staticlib_relocations = archive_relocation_types_for_symbol(&staticlib, SYMBOL);
719-
assert!(
720-
staticlib_relocations.iter().any(|relocations| relocations
721-
.relocation_types
722-
.iter()
723-
.any(|t| is_tlsdesc_object_relocation(*t))),
724-
"expected an object-file TLSDESC relocation for {SYMBOL} in {}\nfound:\n{}",
725-
staticlib.display(),
726-
format_relocations(&staticlib_relocations)
727-
);
728-
729-
let object_relocations = relocation_types_for_symbol_in_file(&object, SYMBOL);
730-
assert!(
731-
object_relocations.is_empty(),
732-
"expected generated C object to have no relocations for {SYMBOL}; found {object_relocations:?}"
733-
);
734-
735-
let mut link_executable = Command::new("cc");
736-
link_executable
737-
.arg(&object)
738-
.arg(&staticlib)
739-
.args([
740-
"-Wl,--gc-sections",
741-
"-lpthread",
742-
"-ldl",
743-
"-lm",
744-
"-lrt",
745-
"-lutil",
746-
])
747-
.arg("-o")
748-
.arg(&executable);
749-
assert_command_success(&mut link_executable);
750-
751-
// Run the generated executable so the test validates the relaxed TLS access at runtime too.
752-
let mut run_executable = Command::new(&executable);
753-
assert_command_success(&mut run_executable);
754-
755-
let executable_relocations = relocation_types_for_symbol_in_file(&executable, SYMBOL);
756-
assert!(
757-
executable_relocations.is_empty(),
758-
"expected no remaining relocations for {SYMBOL} in {}; found {executable_relocations:?}",
759-
executable.display()
760-
);
761-
762-
let disassembly = objdump(&["-drwC"], &executable);
763-
let tls_slot_functions =
764-
disassembled_functions(&disassembly, "libdd_otel_thread_ctx::linux::with_tls_slot");
765-
766-
#[cfg(target_arch = "x86_64")]
767-
{
768-
assert!(
769-
tls_slot_functions
770-
.iter()
771-
.any(|function| function.contains("%fs:0x0")),
772-
"expected tls_slot() in libdd-otel-thread-ctx to be relaxed to local-exec x86-64 \
773-
TLS access through %fs:0x0\n{}",
774-
tls_slot_functions.join("\n\n")
775-
);
776-
assert!(
777-
tls_slot_functions
778-
.iter()
779-
.all(|function| !function.contains("tlsdesc")),
780-
"expected linker-relaxed local-exec TLS code without TLSDESC operands:\n{}",
781-
tls_slot_functions.join("\n\n")
782-
);
783-
}
784-
785-
#[cfg(target_arch = "aarch64")]
786-
{
787-
let function = tls_slot_functions
788-
.iter()
789-
.find(|function| function.contains("tpidr_el0"))
790-
.unwrap_or_else(|| {
791-
panic!(
792-
"expected tls_slot() in libdd-otel-thread-ctx to use tpidr_el0 after \
793-
relaxation\n{}",
794-
tls_slot_functions.join("\n\n")
795-
)
796-
});
797-
let window = disassembly_window_around_line(function, "tpidr_el0", 4, 3);
798-
assert!(
799-
!window.contains("tlsdesc") && !window.contains("\tblr"),
800-
"expected linker-relaxed local-exec TLS code around tpidr_el0 without a TLSDESC call:\n\
801-
{window}"
802-
);
803-
}
804-
}

libdd-otel-thread-ctx/src/lib.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,6 @@ pub mod linux {
8282
// Stable `rustc` cannot select the TLS dialect for a `#[thread_local]` static, so we declare
8383
// the symbol directly in assembly (an 8-byte, zero-initialised slot in `.tbss`) and resolve
8484
// its per-thread address through TLSDESC in [`tls_slot`].
85-
//
86-
// WARNING: keep the assembly below in the canonical compiler-emitted TLSDESC form. Linkers
87-
// rely on these exact relocation-bearing instruction patterns for TLS relaxation, especially
88-
// when this crate is linked statically. Harmless-looking rewrites can hide part of the sequence
89-
// from the linker and produce a partially relaxed access that computes an invalid TLS address.
9085
#[cfg(all(
9186
target_os = "linux",
9287
any(target_arch = "x86_64", target_arch = "aarch64")
@@ -108,6 +103,11 @@ pub mod linux {
108103
#[inline(always)]
109104
unsafe fn tls_slot() -> *mut *mut ThreadContextRecord {
110105
let ptr: usize;
106+
// WARNING: keep the assembly below in the canonical compiler-emitted TLSDESC form. Linkers
107+
// rely on these exact relocation-bearing instruction patterns for TLS relaxation,
108+
// especially when this crate is linked statically. Harmless-looking rewrites can hide part
109+
// of the sequence from the linker and produce a partially relaxed access that computes an
110+
// invalid TLS address.
111111
core::arch::asm!(
112112
"leaq otel_thread_ctx_v1@tlsdesc(%rip), %rax",
113113
"call *otel_thread_ctx_v1@TLSCALL(%rax)",
@@ -124,6 +124,7 @@ pub mod linux {
124124
#[inline(always)]
125125
unsafe fn tls_slot() -> *mut *mut ThreadContextRecord {
126126
let ptr: usize;
127+
// WARNING: do not change the assembly below. See the warning above for amd64.
127128
core::arch::asm!(
128129
"mrs x1, tpidr_el0",
129130
"adrp x0, :tlsdesc:otel_thread_ctx_v1",

0 commit comments

Comments
 (0)