|
1 | 1 | // Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ |
2 | 2 | // SPDX-License-Identifier: Apache-2.0 |
3 | 3 |
|
4 | | -//! Verify ELF properties of the built artifacts on Linux. |
| 4 | +//! Verify ELF properties of the built artifacts on Linux x86_64/aarch64. |
5 | 5 | //! |
6 | 6 | //! These tests check that: |
7 | 7 | //! - `otel_thread_ctx_v1` is exported in the dynamic symbol table as a TLS GLOBAL symbol. |
8 | 8 | //! - `otel_thread_ctx_v1` follows the TLSDESC access model: if there is a relocation for it, it is |
9 | 9 | //! 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). |
13 | 12 | //! |
14 | 13 | //! Library artifact paths are derived at runtime from the test executable location. |
15 | 14 | //! The test binary and crate artifacts live in `target/<[triple/]profile>/deps/`. |
16 | 15 |
|
17 | | -#![cfg(target_os = "linux")] |
| 16 | +#![cfg(all( |
| 17 | + target_os = "linux", |
| 18 | + any(target_arch = "x86_64", target_arch = "aarch64") |
| 19 | +))] |
18 | 20 |
|
19 | 21 | use std::{ |
20 | 22 | fmt, |
@@ -51,12 +53,6 @@ struct TlsDescSequence { |
51 | 53 | relocations: Vec<TlsDescRelocation>, |
52 | 54 | } |
53 | 55 |
|
54 | | -#[derive(Debug)] |
55 | | -struct ArchiveMemberRelocations { |
56 | | - member_name: String, |
57 | | - relocation_types: Vec<RelocationType>, |
58 | | -} |
59 | | - |
60 | 56 | fn deps_dir() -> PathBuf { |
61 | 57 | // test binary: target/<[triple/]profile>/deps/<name> |
62 | 58 | let exe = std::env::current_exe().expect("failed to read current executable path"); |
@@ -121,26 +117,6 @@ fn skip_tls_shim_asm_test() -> bool { |
121 | 117 | skip |
122 | 118 | } |
123 | 119 |
|
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 | | - |
144 | 120 | fn assert_command_success(command: &mut Command) { |
145 | 121 | let out = command |
146 | 122 | .output() |
@@ -210,77 +186,6 @@ fn symbol_indexes_in_table( |
210 | 186 | .collect() |
211 | 187 | } |
212 | 188 |
|
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(§ion_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(§ion_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 | | - |
284 | 189 | fn for_each_archive_elf_member(path: &Path, mut f: impl FnMut(&str, &[u8])) { |
285 | 190 | let archive_data = |
286 | 191 | 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 { |
527 | 432 | object |
528 | 433 | } |
529 | 434 |
|
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 | | - |
612 | 435 | #[test] |
613 | 436 | #[cfg_attr(miri, ignore)] |
614 | 437 | #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] |
@@ -663,142 +486,3 @@ fn otel_thread_ctx_v1_tls_properties() { |
663 | 486 | check_readable(&path); |
664 | 487 | libdd_otel_thread_ctx::sanity_check::check_tls_slot_in(&path).unwrap(); |
665 | 488 | } |
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