Skip to content

Commit 80c14f4

Browse files
committed
address review comments
1 parent 109699f commit 80c14f4

3 files changed

Lines changed: 13 additions & 334 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: 7 additions & 323 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
22
// SPDX-License-Identifier: Apache-2.0
33

4-
//! Verify ELF properties of the built artifacts on Linux.
4+
//! Verify ELF properties of the built artifacts on Linux x86_64/aarch64.
55
//!
66
//! These tests check that:
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/`.
1615
17-
#![cfg(target_os = "linux")]
16+
#![cfg(all(
17+
target_os = "linux",
18+
any(target_arch = "x86_64", target_arch = "aarch64")
19+
))]
1820

1921
use std::{
2022
fmt,
@@ -51,12 +53,6 @@ struct TlsDescSequence {
5153
relocations: Vec<TlsDescRelocation>,
5254
}
5355

54-
#[derive(Debug)]
55-
struct ArchiveMemberRelocations {
56-
member_name: String,
57-
relocation_types: Vec<RelocationType>,
58-
}
59-
6056
fn deps_dir() -> PathBuf {
6157
// test binary: target/<[triple/]profile>/deps/<name>
6258
let exe = std::env::current_exe().expect("failed to read current executable path");
@@ -121,26 +117,6 @@ fn skip_tls_shim_asm_test() -> bool {
121117
skip
122118
}
123119

124-
fn command_output(command: &mut Command) -> String {
125-
let out = command
126-
.output()
127-
.unwrap_or_else(|e| panic!("failed to run {command:?}: {e}"));
128-
assert!(
129-
out.status.success(),
130-
"{command:?} failed with status {}\nstdout:\n{}\nstderr:\n{}",
131-
out.status,
132-
String::from_utf8_lossy(&out.stdout),
133-
String::from_utf8_lossy(&out.stderr)
134-
);
135-
String::from_utf8_lossy(&out.stdout).into_owned()
136-
}
137-
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-
144120
fn assert_command_success(command: &mut Command) {
145121
let out = command
146122
.output()
@@ -210,77 +186,6 @@ fn symbol_indexes_in_table(
210186
.collect()
211187
}
212188

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-
284189
fn for_each_archive_elf_member(path: &Path, mut f: impl FnMut(&str, &[u8])) {
285190
let archive_data =
286191
std::fs::read(path).unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()));
@@ -527,88 +432,6 @@ fn compile_tls_shim_object(dir: &Path) -> PathBuf {
527432
object
528433
}
529434

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-
612435
#[test]
613436
#[cfg_attr(miri, ignore)]
614437
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
@@ -663,142 +486,3 @@ fn otel_thread_ctx_v1_tls_properties() {
663486
check_readable(&path);
664487
libdd_otel_thread_ctx::sanity_check::check_tls_slot_in(&path).unwrap();
665488
}
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-
}

0 commit comments

Comments
 (0)