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
1924const 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 {}\n stdout:\n {}\n stderr:\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 {}\n stdout:\n {}\n stderr:\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) ]
65212fn 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( "\t blr" ) ,
368+ "expected linker-relaxed local-exec TLS code around tpidr_el0 without a TLSDESC call:\n \
369+ {window}"
370+ ) ;
371+ }
372+ }
0 commit comments