Skip to content

Commit a263aeb

Browse files
committed
add comment and test for relaxation
1 parent 506d690 commit a263aeb

3 files changed

Lines changed: 318 additions & 22 deletions

File tree

libdd-otel-thread-ctx-ffi/build.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ use std::env;
88
fn main() {
99
generate_and_configure_header("otel-thread-ctx.h");
1010

11+
let cross_compiling = env::var("HOST").unwrap() != env::var("TARGET").unwrap();
12+
println!("cargo:rustc-env=LIBDD_OTEL_THREAD_CTX_FFI_CROSS_COMPILING={cross_compiling}");
13+
1114
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
1215
if target_os != "linux" {
1316
return;
Lines changed: 310 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,133 @@
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 cdylib on Linux.
4+
//! Verify ELF properties of the built artifacts on Linux.
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` is accessed via TLSDESC relocations (R_X86_64_TLSDESC or
99
//! R_AARCH64_TLSDESC), as required by the OTel thread-level context sharing spec.
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.
1012
//!
11-
//! The cdylib path is derived at runtime from the test executable location.
12-
//! Both the test binary and the cdylib live in `target/<[triple/]profile>/deps/`.
13+
//! Library artifact paths are derived at runtime from the test executable location.
14+
//! The test binary and crate artifacts live in `target/<[triple/]profile>/deps/`.
1315
1416
#![cfg(target_os = "linux")]
1517

16-
use std::path::PathBuf;
17-
use std::process::Command;
18+
use std::{
19+
io::ErrorKind,
20+
path::{Path, PathBuf},
21+
process::{Command, Stdio},
22+
};
1823

1924
const SYMBOL: &str = "otel_thread_ctx_v1";
2025

21-
fn cdylib_path() -> PathBuf {
26+
fn deps_dir() -> PathBuf {
2227
// test binary: target/<[triple/]profile>/deps/<name>
23-
// cdylib: target/<[triple/]profile>/deps/liblibdd_otel_thread_ctx_ffi.so
2428
let exe = std::env::current_exe().expect("failed to read current executable path");
2529
exe.parent()
2630
.expect("unexpected test executable path structure")
27-
.join("liblibdd_otel_thread_ctx_ffi.so")
31+
.to_owned()
32+
}
33+
34+
fn artifact_path(name: &str) -> PathBuf {
35+
deps_dir().join(name)
36+
}
37+
38+
fn cdylib_path() -> PathBuf {
39+
artifact_path("liblibdd_otel_thread_ctx_ffi.so")
40+
}
41+
42+
fn staticlib_path() -> PathBuf {
43+
artifact_path("liblibdd_otel_thread_ctx_ffi.a")
2844
}
2945

30-
fn check_cdylib_readable(path: &PathBuf) {
46+
fn check_readable(path: &Path) {
3147
assert!(
3248
std::fs::File::open(path).is_ok(),
33-
"cdylib at {} could not be opened for reading",
49+
"{} could not be opened for reading",
3450
path.display()
3551
);
3652
}
3753

38-
fn readelf(args: &[&str], path: &PathBuf) -> String {
39-
let out = Command::new("readelf")
40-
.args(args)
41-
.arg(path)
54+
fn tool_available(tool: &str) -> bool {
55+
match Command::new(tool)
56+
.arg("--version")
57+
.stdout(Stdio::null())
58+
.stderr(Stdio::null())
59+
.status()
60+
{
61+
Ok(_) => true,
62+
Err(e) if e.kind() == ErrorKind::NotFound => {
63+
eprintln!("skipping test: required tool `{tool}` is not available");
64+
false
65+
}
66+
Err(e) => panic!("failed to check whether `{tool}` is available: {e}"),
67+
}
68+
}
69+
70+
fn required_tools_available(tools: &[&str]) -> bool {
71+
tools.iter().all(|tool| tool_available(tool))
72+
}
73+
74+
fn native_target() -> bool {
75+
let cross_compiling = option_env!("LIBDD_OTEL_THREAD_CTX_FFI_CROSS_COMPILING") == Some("true");
76+
if cross_compiling {
77+
eprintln!("skipping test: cross-compiling");
78+
}
79+
!cross_compiling
80+
}
81+
82+
fn command_output(command: &mut Command) -> String {
83+
let out = command
4284
.output()
43-
.expect("failed to run readelf. Is binutils installed?");
85+
.unwrap_or_else(|e| panic!("failed to run {command:?}: {e}"));
86+
assert!(
87+
out.status.success(),
88+
"{command:?} failed with status {}\nstdout:\n{}\nstderr:\n{}",
89+
out.status,
90+
String::from_utf8_lossy(&out.stdout),
91+
String::from_utf8_lossy(&out.stderr)
92+
);
4493
String::from_utf8_lossy(&out.stdout).into_owned()
4594
}
4695

47-
#[test]
48-
#[cfg_attr(miri, ignore)]
49-
fn otel_thread_ctx_v1_in_dynsym() {
50-
let path = cdylib_path();
51-
check_cdylib_readable(&path);
52-
let output = readelf(&["-W", "--dyn-syms"], &path);
96+
fn readelf(args: &[&str], path: &Path) -> String {
97+
let mut command = Command::new("readelf");
98+
command.args(args).arg(path);
99+
command_output(&mut command)
100+
}
101+
102+
fn objdump(args: &[&str], path: &Path) -> String {
103+
let mut command = Command::new("objdump");
104+
command.args(args).arg(path);
105+
command_output(&mut command)
106+
}
107+
108+
fn assert_command_success(command: &mut Command) {
109+
let out = command
110+
.output()
111+
.unwrap_or_else(|e| panic!("failed to run {command:?}: {e}"));
112+
assert!(
113+
out.status.success(),
114+
"{command:?} failed with status {}\nstdout:\n{}\nstderr:\n{}",
115+
out.status,
116+
String::from_utf8_lossy(&out.stdout),
117+
String::from_utf8_lossy(&out.stderr)
118+
);
119+
}
120+
121+
fn build_dir(name: &str) -> PathBuf {
122+
let dir = deps_dir().join(format!("{name}-{}", std::process::id()));
123+
let _ = std::fs::remove_dir_all(&dir);
124+
std::fs::create_dir_all(&dir)
125+
.unwrap_or_else(|e| panic!("failed to create {}: {e}", dir.display()));
126+
dir
127+
}
128+
129+
fn assert_symbol_is_tls_global_in_dynsym(path: &Path) {
130+
let output = readelf(&["-W", "--dyn-syms"], path);
53131
let line = output
54132
.lines()
55133
.find(|l| l.contains(SYMBOL))
@@ -60,11 +138,88 @@ fn otel_thread_ctx_v1_in_dynsym() {
60138
);
61139
}
62140

141+
fn disassembled_functions(output: &str, name: &str) -> Vec<String> {
142+
let marker = format!("<{name}>:");
143+
let mut functions = Vec::new();
144+
let mut current_function = Vec::new();
145+
146+
for line in output.lines() {
147+
if line.contains(&marker) {
148+
if !current_function.is_empty() {
149+
functions.push(current_function.join("\n"));
150+
current_function.clear();
151+
}
152+
current_function.push(line);
153+
continue;
154+
}
155+
156+
if !current_function.is_empty() {
157+
if line.is_empty() {
158+
functions.push(current_function.join("\n"));
159+
current_function.clear();
160+
continue;
161+
}
162+
current_function.push(line);
163+
}
164+
}
165+
166+
if !current_function.is_empty() {
167+
functions.push(current_function.join("\n"));
168+
}
169+
170+
assert!(
171+
!functions.is_empty(),
172+
"could not find disassembly for {name} in:\n{output}"
173+
);
174+
functions
175+
}
176+
177+
#[cfg(target_arch = "aarch64")]
178+
fn disassembly_window_around_line(
179+
function: &str,
180+
needle: &str,
181+
before: usize,
182+
after: usize,
183+
) -> String {
184+
let lines = function.lines().collect::<Vec<_>>();
185+
let line_index = lines
186+
.iter()
187+
.position(|line| line.contains(needle))
188+
.unwrap_or_else(|| panic!("could not find {needle:?} in:\n{function}"));
189+
let start = line_index.saturating_sub(before);
190+
let end = usize::min(line_index + after + 1, lines.len());
191+
lines[start..end].join("\n")
192+
}
193+
194+
#[test]
195+
#[cfg_attr(miri, ignore)]
196+
fn otel_thread_ctx_v1_in_dynsym() {
197+
if !native_target() {
198+
return;
199+
}
200+
201+
if !required_tools_available(&["readelf"]) {
202+
return;
203+
}
204+
205+
let path = cdylib_path();
206+
check_readable(&path);
207+
assert_symbol_is_tls_global_in_dynsym(&path);
208+
}
209+
63210
#[test]
64211
#[cfg_attr(miri, ignore)]
65212
fn otel_thread_ctx_v1_tlsdesc_reloc() {
213+
if !native_target() {
214+
return;
215+
}
216+
217+
if !required_tools_available(&["readelf"]) {
218+
return;
219+
}
220+
66221
let path = cdylib_path();
67-
check_cdylib_readable(&path);
222+
check_readable(&path);
68223
let output = readelf(&["-W", "--relocs"], &path);
69224
let found = output.lines().any(|l| {
70225
l.contains(SYMBOL) && (l.contains("R_X86_64_TLSDESC") || l.contains("R_AARCH64_TLSDESC"))
@@ -82,3 +237,136 @@ fn otel_thread_ctx_v1_tlsdesc_reloc() {
82237
.join("\n")
83238
);
84239
}
240+
241+
#[test]
242+
#[cfg_attr(miri, ignore)]
243+
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
244+
fn statically_linked_executable_relaxes_libdd_tls_slot_to_local_exec() {
245+
if !native_target() {
246+
return;
247+
}
248+
249+
if !required_tools_available(&["cc", "readelf", "objdump"]) {
250+
return;
251+
}
252+
253+
let staticlib = staticlib_path();
254+
check_readable(&staticlib);
255+
256+
let dir = build_dir("otel-thread-ctx-local-exec");
257+
let source = dir.join("consumer.c");
258+
let object = dir.join("consumer.o");
259+
let executable = dir.join("consumer");
260+
std::fs::write(
261+
&source,
262+
r#"
263+
#include <stdint.h>
264+
265+
void ddog_otel_thread_ctx_update(
266+
const uint8_t (*trace_id)[16],
267+
const uint8_t (*span_id)[8],
268+
const uint8_t (*local_root_span_id)[8]);
269+
void *ddog_otel_thread_ctx_detach(void);
270+
void ddog_otel_thread_ctx_free(void *ctx);
271+
272+
int main(void) {
273+
uint8_t trace_id[16] = {1};
274+
uint8_t span_id[8] = {2};
275+
uint8_t local_root_span_id[8] = {3};
276+
277+
ddog_otel_thread_ctx_update(&trace_id, &span_id, &local_root_span_id);
278+
void *ctx = ddog_otel_thread_ctx_detach();
279+
ddog_otel_thread_ctx_free(ctx);
280+
281+
return ctx == 0 ? 1 : 0;
282+
}
283+
"#,
284+
)
285+
.unwrap_or_else(|e| panic!("failed to write {}: {e}", source.display()));
286+
287+
let mut compile_object = Command::new("cc");
288+
compile_object.args(["-O2", "-ffunction-sections", "-fdata-sections"]);
289+
compile_object.arg("-c").arg(&source).arg("-o").arg(&object);
290+
assert_command_success(&mut compile_object);
291+
292+
// the static library should have a TLSDESC relocation for the symbol
293+
let staticlib_relocs = readelf(&["-W", "--relocs"], &staticlib);
294+
assert!(staticlib_relocs
295+
.lines()
296+
.any(|l| l.contains(SYMBOL) && l.contains("TLSDESC")));
297+
298+
// the object file only imports ddog_otel_*
299+
let object_relocs = readelf(&["-W", "--relocs"], &object);
300+
assert!(!object_relocs.lines().any(|l| l.contains(SYMBOL)));
301+
302+
let mut link_executable = Command::new("cc");
303+
link_executable
304+
.arg(&object)
305+
.arg(&staticlib)
306+
.args([
307+
"-Wl,--gc-sections",
308+
"-lpthread",
309+
"-ldl",
310+
"-lm",
311+
"-lrt",
312+
"-lutil",
313+
])
314+
.arg("-o")
315+
.arg(&executable);
316+
assert_command_success(&mut link_executable);
317+
318+
// Run the generated executable so the test validates the relaxed TLS access at runtime too.
319+
let mut run_executable = Command::new(&executable);
320+
assert_command_success(&mut run_executable);
321+
322+
let executable_relocs = readelf(&["-W", "--relocs"], &executable);
323+
assert!(
324+
!executable_relocs
325+
.lines()
326+
.any(|l| l.contains(SYMBOL) && l.contains("TLSDESC")),
327+
"expected TLSDESC relocations for {SYMBOL} to be relaxed in the executable"
328+
);
329+
330+
let disassembly = objdump(&["-drwC"], &executable);
331+
let tls_slot_functions =
332+
disassembled_functions(&disassembly, "libdd_otel_thread_ctx::linux::with_tls_slot");
333+
334+
#[cfg(target_arch = "x86_64")]
335+
{
336+
assert!(
337+
tls_slot_functions
338+
.iter()
339+
.any(|function| function.contains("%fs:0x0")),
340+
"expected tls_slot() in libdd-otel-thread-ctx to be relaxed to local-exec x86-64 \
341+
TLS access through %fs:0x0\n{}",
342+
tls_slot_functions.join("\n\n")
343+
);
344+
assert!(
345+
tls_slot_functions
346+
.iter()
347+
.all(|function| !function.contains("tlsdesc")),
348+
"expected linker-relaxed local-exec TLS code without TLSDESC operands:\n{}",
349+
tls_slot_functions.join("\n\n")
350+
);
351+
}
352+
353+
#[cfg(target_arch = "aarch64")]
354+
{
355+
let function = tls_slot_functions
356+
.iter()
357+
.find(|function| function.contains("tpidr_el0"))
358+
.unwrap_or_else(|| {
359+
panic!(
360+
"expected tls_slot() in libdd-otel-thread-ctx to use tpidr_el0 after \
361+
relaxation\n{}",
362+
tls_slot_functions.join("\n\n")
363+
)
364+
});
365+
let window = disassembly_window_around_line(function, "tpidr_el0", 4, 3);
366+
assert!(
367+
!window.contains("tlsdesc") && !window.contains("\tblr"),
368+
"expected linker-relaxed local-exec TLS code around tpidr_el0 without a TLSDESC call:\n\
369+
{window}"
370+
);
371+
}
372+
}

0 commit comments

Comments
 (0)