From c5b0efe5c215586b1a86af1a0a4141792b6e1fa6 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Fri, 24 Apr 2026 15:36:15 +0200 Subject: [PATCH 01/14] feat: add otel thread ctx setup autotest FFI --- Cargo.lock | 7 ++ libdd-otel-thread-ctx-ffi/Cargo.toml | 3 +- libdd-otel-thread-ctx-ffi/cbindgen.toml | 4 + libdd-otel-thread-ctx-ffi/src/lib.rs | 14 +++ libdd-otel-thread-ctx/Cargo.toml | 6 ++ libdd-otel-thread-ctx/src/autocheck.rs | 124 ++++++++++++++++++++++++ libdd-otel-thread-ctx/src/lib.rs | 3 + 7 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 libdd-otel-thread-ctx/src/autocheck.rs diff --git a/Cargo.lock b/Cargo.lock index c20979e098..2bf6008d59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1738,6 +1738,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "elf" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -3179,6 +3185,7 @@ version = "1.0.0" dependencies = [ "build_common", "cc", + "elf", ] [[package]] diff --git a/libdd-otel-thread-ctx-ffi/Cargo.toml b/libdd-otel-thread-ctx-ffi/Cargo.toml index 8001933cd6..b6cfbe8b2b 100644 --- a/libdd-otel-thread-ctx-ffi/Cargo.toml +++ b/libdd-otel-thread-ctx-ffi/Cargo.toml @@ -15,12 +15,13 @@ crate-type = ["staticlib", "cdylib", "lib"] bench = false [dependencies] -libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false } +libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false, optional = true } libdd-otel-thread-ctx = { path = "../libdd-otel-thread-ctx" } [features] default = ["cbindgen"] cbindgen = ["build_common/cbindgen", "libdd-common-ffi/cbindgen"] +autocheck = ["dep:libdd-common-ffi", "libdd-otel-thread-ctx/autocheck"] [build-dependencies] build_common = { path = "../build-common" } diff --git a/libdd-otel-thread-ctx-ffi/cbindgen.toml b/libdd-otel-thread-ctx-ffi/cbindgen.toml index b4f06019b5..526631d58c 100644 --- a/libdd-otel-thread-ctx-ffi/cbindgen.toml +++ b/libdd-otel-thread-ctx-ffi/cbindgen.toml @@ -21,6 +21,10 @@ include = ["libdd-common-ffi", "libdd-otel-thread-ctx"] prefix = "ddog_" renaming_overrides_prefixing = true +[export.rename] +"VoidResult" = "ddog_VoidResult" +"Error" = "ddog_Error" + [export.mangle] rename_types = "PascalCase" diff --git a/libdd-otel-thread-ctx-ffi/src/lib.rs b/libdd-otel-thread-ctx-ffi/src/lib.rs index bc96e5b020..73e2d602a4 100644 --- a/libdd-otel-thread-ctx-ffi/src/lib.rs +++ b/libdd-otel-thread-ctx-ffi/src/lib.rs @@ -8,6 +8,20 @@ #[cfg(target_os = "linux")] pub use linux::*; +/// Verify that this binary was linked with the correct options such that the thread contexts are +/// visible to an external reader (typically the eBPF profiler). +/// +/// Returns `VoidResult::Ok` if all checks pass, or a `VoidResult::Err` with a +/// diagnostic message on failure. +#[cfg(all(target_os = "linux", feature = "autocheck"))] +#[no_mangle] +pub extern "C" fn ddog_otel_thread_ctx_autocheck() -> libdd_common_ffi::VoidResult { + match libdd_otel_thread_ctx::autocheck::check_tlsdesc_slot_present() { + Ok(()) => libdd_common_ffi::VoidResult::Ok, + Err(e) => libdd_common_ffi::VoidResult::Err(libdd_common_ffi::Error::from(e)), + } +} + #[cfg(target_os = "linux")] mod linux { use libdd_otel_thread_ctx::linux::{ThreadContext, ThreadContextHandle}; diff --git a/libdd-otel-thread-ctx/Cargo.toml b/libdd-otel-thread-ctx/Cargo.toml index bd8ce4c372..0ef6baf27b 100644 --- a/libdd-otel-thread-ctx/Cargo.toml +++ b/libdd-otel-thread-ctx/Cargo.toml @@ -16,6 +16,12 @@ publish = false crate-type = ["lib"] bench = false +[dependencies] +elf = { version = "0.7", optional = true } + +[features] +autocheck = ["dep:elf"] + [build-dependencies] build_common = { path = "../build-common" } cc = "1.1.31" diff --git a/libdd-otel-thread-ctx/src/autocheck.rs b/libdd-otel-thread-ctx/src/autocheck.rs new file mode 100644 index 0000000000..dbece260fe --- /dev/null +++ b/libdd-otel-thread-ctx/src/autocheck.rs @@ -0,0 +1,124 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Runtime ELF self-inspection for shared-library linking correctness. +//! +//! Call [`check_linking`] from within a cdylib context to verify that this +//! shared object was linked with the required TLS properties: +//! - `otel_thread_ctx_v1` is exported as TLS GLOBAL in the dynamic symbol table. +//! - `otel_thread_ctx_v1` is accessed via a TLSDESC relocation in `.rela.dyn`. +//! +//! This module is only available on Linux (the only platform that supports the +//! TLSDESC dialect used by this crate) and only when the `autocheck` feature +//! is enabled. + +use elf::{abi, endian::AnyEndian, ElfBytes}; +use std::path::PathBuf; + +const SYMBOL: &str = "otel_thread_ctx_v1"; + +/// Verify that this binary was linked with the correct TLS properties for the +/// OTel thread-level context spec. +/// +/// Locates the ELF file that contains this function (via `/proc/self/maps`) +/// and asserts that `otel_thread_ctx_v1` is exported as a TLS GLOBAL symbol +/// accessed through a TLSDESC relocation. +/// +/// Returns `Ok(())` on success, or an `Err` with a diagnostic message on +/// failure (does not panic). +pub fn check_tlsdesc_slot_present() -> Result<(), String> { + let path = own_so_path()?; + let data = + std::fs::read(&path).map_err(|e| format!("failed to read {}: {e}", path.display()))?; + let elf = ElfBytes::::minimal_parse(&data) + .map_err(|e| format!("failed to parse ELF at {}: {e}", path.display()))?; + check_dynsym(&elf)?; + check_tlsdesc_reloc(&elf)?; + Ok(()) +} + +/// Locate this shared object via `/proc/self/maps` using `check_linking`'s address. +fn own_so_path() -> Result { + let addr = check_tlsdesc_slot_present as *const () as usize; + let maps = std::fs::read_to_string("/proc/self/maps") + .map_err(|e| format!("failed to read /proc/self/maps: {e}"))?; + for line in maps.lines() { + // Format: address perms offset dev inode [pathname] + let fields: Vec<&str> = line.split_whitespace().collect(); + if fields.len() < 6 { + continue; + } + let path = fields[5]; + if !path.starts_with('/') { + continue; + } + if let Some((start_str, end_str)) = fields[0].split_once('-') { + let start = usize::from_str_radix(start_str, 16).unwrap_or(0); + let end = usize::from_str_radix(end_str, 16).unwrap_or(0); + if addr >= start && addr < end { + return Ok(PathBuf::from(path)); + } + } + } + Err("could not find our shared object in /proc/self/maps".into()) +} + +fn check_dynsym(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> { + let (symtab, strtab) = elf + .dynamic_symbol_table() + .map_err(|e| format!("failed to read .dynsym: {e}"))? + .ok_or_else(|| "no dynamic symbol table found".to_string())?; + let found = symtab.iter().any(|sym| { + strtab + .get(sym.st_name as usize) + .map(|name| { + name == SYMBOL + && sym.st_symtype() == abi::STT_TLS + && sym.st_bind() == abi::STB_GLOBAL + }) + .unwrap_or(false) + }); + if !found { + return Err(format!( + "'{SYMBOL}' not found as TLS GLOBAL in dynamic symbol table" + )); + } + Ok(()) +} + +fn check_tlsdesc_reloc(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> { + #[cfg(target_arch = "x86_64")] + const R_TLSDESC: u32 = 36; // R_X86_64_TLSDESC + #[cfg(target_arch = "aarch64")] + const R_TLSDESC: u32 = 1031; // R_AARCH64_TLSDESC + + // Find the .dynsym index of the target symbol. + let (symtab, strtab) = elf + .dynamic_symbol_table() + .map_err(|e| format!("failed to read .dynsym: {e}"))? + .ok_or_else(|| "no dynamic symbol table found".to_string())?; + let sym_idx = symtab + .iter() + .enumerate() + .find(|(_, sym)| { + strtab + .get(sym.st_name as usize) + .map(|n| n == SYMBOL) + .unwrap_or(false) + }) + .map(|(i, _)| i as u32) + .ok_or_else(|| format!("'{SYMBOL}' not found in .dynsym"))?; + + let rela_shdr = elf + .section_header_by_name(".rela.dyn") + .map_err(|e| format!("failed to read section headers: {e}"))? + .ok_or_else(|| ".rela.dyn section not found".to_string())?; + let found = elf + .section_data_as_relas(&rela_shdr) + .map_err(|e| format!("failed to read .rela.dyn: {e}"))? + .any(|r| r.r_type == R_TLSDESC && r.r_sym == sym_idx); + if !found { + return Err(format!("no TLSDESC relocation for '{SYMBOL}' in .rela.dyn")); + } + Ok(()) +} diff --git a/libdd-otel-thread-ctx/src/lib.rs b/libdd-otel-thread-ctx/src/lib.rs index 649a5fc18c..0741fdaf01 100644 --- a/libdd-otel-thread-ctx/src/lib.rs +++ b/libdd-otel-thread-ctx/src/lib.rs @@ -64,6 +64,9 @@ //! `atomic_signal_fence`) to keep field writes boxed between the `valid = 0` and `valid = 1` //! stores during in-place updates. +#[cfg(all(target_os = "linux", feature = "autocheck"))] +pub mod autocheck; + #[cfg(target_os = "linux")] pub mod linux { use std::{ From e51a08f4258e5daf1957a7bec68eff69d42118ab Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 8 Jun 2026 12:01:57 +0200 Subject: [PATCH 02/14] fix: check for absence of GD/LD relocations instead of presence of TLSDESC The linker can optimize TLSDESC to Local Exec, so a positive TLSDESC check is too strict. Instead, assert that no General Dynamic or Local Dynamic relocations (DTPMOD/DTPOFF) are present for otel_thread_ctx_v1. Co-Authored-By: Claude Opus 4.6 --- libdd-otel-thread-ctx-ffi/src/lib.rs | 2 +- .../tests/elf_properties.rs | 32 ++++++---- libdd-otel-thread-ctx/src/autocheck.rs | 59 +++++++++++++------ 3 files changed, 61 insertions(+), 32 deletions(-) diff --git a/libdd-otel-thread-ctx-ffi/src/lib.rs b/libdd-otel-thread-ctx-ffi/src/lib.rs index 73e2d602a4..e4c68a6964 100644 --- a/libdd-otel-thread-ctx-ffi/src/lib.rs +++ b/libdd-otel-thread-ctx-ffi/src/lib.rs @@ -16,7 +16,7 @@ pub use linux::*; #[cfg(all(target_os = "linux", feature = "autocheck"))] #[no_mangle] pub extern "C" fn ddog_otel_thread_ctx_autocheck() -> libdd_common_ffi::VoidResult { - match libdd_otel_thread_ctx::autocheck::check_tlsdesc_slot_present() { + match libdd_otel_thread_ctx::autocheck::check_tls_slot_present() { Ok(()) => libdd_common_ffi::VoidResult::Ok, Err(e) => libdd_common_ffi::VoidResult::Err(libdd_common_ffi::Error::from(e)), } diff --git a/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs b/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs index a429a29a00..2369d426fd 100644 --- a/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs +++ b/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs @@ -5,8 +5,8 @@ //! //! These tests check that: //! - `otel_thread_ctx_v1` is exported in the dynamic symbol table as a TLS GLOBAL symbol. -//! - `otel_thread_ctx_v1` is accessed via TLSDESC relocations (R_X86_64_TLSDESC or -//! R_AARCH64_TLSDESC), as required by the OTel thread-level context sharing spec. +//! - `otel_thread_ctx_v1` does NOT use General Dynamic or Local Dynamic TLS relocations +//! (DTPMOD/DTPOFF). The linker may resolve to TLSDESC or Local Exec; both are acceptable. //! //! The cdylib path is derived at runtime from the test executable location. //! Both the test binary and the cdylib live in `target/<[triple/]profile>/deps/`. @@ -62,21 +62,29 @@ fn otel_thread_ctx_v1_in_dynsym() { #[test] #[cfg_attr(miri, ignore)] -fn otel_thread_ctx_v1_tlsdesc_reloc() { +fn otel_thread_ctx_v1_no_gd_ld_reloc() { let path = cdylib_path(); check_cdylib_readable(&path); let output = readelf(&["-W", "--relocs"], &path); - let found = output.lines().any(|l| { - l.contains(SYMBOL) && (l.contains("R_X86_64_TLSDESC") || l.contains("R_AARCH64_TLSDESC")) - }); + + const FORBIDDEN: &[&str] = &[ + "R_X86_64_DTPMOD64", + "R_X86_64_DTPOFF64", + "R_AARCH64_TLS_DTPMOD", + "R_AARCH64_TLS_DTPREL", + ]; + + let bad_lines: Vec<&str> = output + .lines() + .filter(|l| l.contains(SYMBOL) && FORBIDDEN.iter().any(|f| l.contains(f))) + .collect(); assert!( - found, - "No TLSDESC relocation found for '{SYMBOL}' in {}\n\ - All relocations mentioning the symbol:\n{}", + bad_lines.is_empty(), + "'{SYMBOL}' has General Dynamic / Local Dynamic relocations in {}:\n{}\n\ + Expected TLSDESC or Local Exec instead.", path.display(), - output - .lines() - .filter(|l| l.contains(SYMBOL)) + bad_lines + .iter() .map(|l| format!(" {l}")) .collect::>() .join("\n") diff --git a/libdd-otel-thread-ctx/src/autocheck.rs b/libdd-otel-thread-ctx/src/autocheck.rs index dbece260fe..343271e0be 100644 --- a/libdd-otel-thread-ctx/src/autocheck.rs +++ b/libdd-otel-thread-ctx/src/autocheck.rs @@ -3,10 +3,12 @@ //! Runtime ELF self-inspection for shared-library linking correctness. //! -//! Call [`check_linking`] from within a cdylib context to verify that this -//! shared object was linked with the required TLS properties: +//! Call [`check_tls_slot_present`] from within a cdylib context to verify that +//! this shared object was linked with the required TLS properties: //! - `otel_thread_ctx_v1` is exported as TLS GLOBAL in the dynamic symbol table. -//! - `otel_thread_ctx_v1` is accessed via a TLSDESC relocation in `.rela.dyn`. +//! - `otel_thread_ctx_v1` is NOT accessed via General Dynamic or Local Dynamic +//! TLS relocations (DTPMOD/DTPOFF) in `.rela.dyn`. The linker may resolve to +//! TLSDESC or Local Exec depending on optimization; both are acceptable. //! //! This module is only available on Linux (the only platform that supports the //! TLSDESC dialect used by this crate) and only when the `autocheck` feature @@ -22,24 +24,24 @@ const SYMBOL: &str = "otel_thread_ctx_v1"; /// /// Locates the ELF file that contains this function (via `/proc/self/maps`) /// and asserts that `otel_thread_ctx_v1` is exported as a TLS GLOBAL symbol -/// accessed through a TLSDESC relocation. +/// with no General Dynamic or Local Dynamic TLS relocations. /// /// Returns `Ok(())` on success, or an `Err` with a diagnostic message on /// failure (does not panic). -pub fn check_tlsdesc_slot_present() -> Result<(), String> { +pub fn check_tls_slot_present() -> Result<(), String> { let path = own_so_path()?; let data = std::fs::read(&path).map_err(|e| format!("failed to read {}: {e}", path.display()))?; let elf = ElfBytes::::minimal_parse(&data) .map_err(|e| format!("failed to parse ELF at {}: {e}", path.display()))?; check_dynsym(&elf)?; - check_tlsdesc_reloc(&elf)?; + check_no_gd_ld_reloc(&elf)?; Ok(()) } /// Locate this shared object via `/proc/self/maps` using `check_linking`'s address. fn own_so_path() -> Result { - let addr = check_tlsdesc_slot_present as *const () as usize; + let addr = check_tls_slot_present as *const () as usize; let maps = std::fs::read_to_string("/proc/self/maps") .map_err(|e| format!("failed to read /proc/self/maps: {e}"))?; for line in maps.lines() { @@ -86,13 +88,18 @@ fn check_dynsym(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> { Ok(()) } -fn check_tlsdesc_reloc(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> { +fn check_no_gd_ld_reloc(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> { #[cfg(target_arch = "x86_64")] - const R_TLSDESC: u32 = 36; // R_X86_64_TLSDESC + const FORBIDDEN_RELOCS: &[(u32, &str)] = &[ + (16, "R_X86_64_DTPMOD64"), + (17, "R_X86_64_DTPOFF64"), + ]; #[cfg(target_arch = "aarch64")] - const R_TLSDESC: u32 = 1031; // R_AARCH64_TLSDESC + const FORBIDDEN_RELOCS: &[(u32, &str)] = &[ + (1028, "R_AARCH64_TLS_DTPMOD"), + (1029, "R_AARCH64_TLS_DTPREL"), + ]; - // Find the .dynsym index of the target symbol. let (symtab, strtab) = elf .dynamic_symbol_table() .map_err(|e| format!("failed to read .dynsym: {e}"))? @@ -111,14 +118,28 @@ fn check_tlsdesc_reloc(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> { let rela_shdr = elf .section_header_by_name(".rela.dyn") - .map_err(|e| format!("failed to read section headers: {e}"))? - .ok_or_else(|| ".rela.dyn section not found".to_string())?; - let found = elf - .section_data_as_relas(&rela_shdr) - .map_err(|e| format!("failed to read .rela.dyn: {e}"))? - .any(|r| r.r_type == R_TLSDESC && r.r_sym == sym_idx); - if !found { - return Err(format!("no TLSDESC relocation for '{SYMBOL}' in .rela.dyn")); + .map_err(|e| format!("failed to read section headers: {e}"))?; + + if let Some(rela_shdr) = rela_shdr { + let bad: Vec<&str> = elf + .section_data_as_relas(&rela_shdr) + .map_err(|e| format!("failed to read .rela.dyn: {e}"))? + .filter(|r| r.r_sym == sym_idx) + .filter_map(|r| { + FORBIDDEN_RELOCS + .iter() + .find(|(typ, _)| *typ == r.r_type) + .map(|(_, name)| *name) + }) + .collect(); + if !bad.is_empty() { + return Err(format!( + "'{SYMBOL}' has General Dynamic / Local Dynamic relocations in .rela.dyn: {}. \ + Expected TLSDESC or Local Exec instead.", + bad.join(", ") + )); + } } + Ok(()) } From bb2a3184c441e5851e0110737506f80b64d29c02 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 8 Jun 2026 12:04:43 +0200 Subject: [PATCH 03/14] refactor: consolidate elf_properties test to reuse autocheck logic Extract check_tls_slot_in(path) from check_tls_slot_present() so the integration test can call the same programmatic checks instead of shelling out to readelf. Co-Authored-By: Claude Opus 4.6 --- libdd-otel-thread-ctx-ffi/Cargo.toml | 3 + .../tests/elf_properties.rs | 72 ++----------------- libdd-otel-thread-ctx/src/autocheck.rs | 25 ++++--- 3 files changed, 23 insertions(+), 77 deletions(-) diff --git a/libdd-otel-thread-ctx-ffi/Cargo.toml b/libdd-otel-thread-ctx-ffi/Cargo.toml index b6cfbe8b2b..b4a2c9545d 100644 --- a/libdd-otel-thread-ctx-ffi/Cargo.toml +++ b/libdd-otel-thread-ctx-ffi/Cargo.toml @@ -23,5 +23,8 @@ default = ["cbindgen"] cbindgen = ["build_common/cbindgen", "libdd-common-ffi/cbindgen"] autocheck = ["dep:libdd-common-ffi", "libdd-otel-thread-ctx/autocheck"] +[dev-dependencies] +libdd-otel-thread-ctx = { path = "../libdd-otel-thread-ctx", features = ["autocheck"] } + [build-dependencies] build_common = { path = "../build-common" } diff --git a/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs b/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs index 2369d426fd..8e4d03bef9 100644 --- a/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs +++ b/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs @@ -3,10 +3,10 @@ //! Verify ELF properties of the built cdylib on Linux. //! -//! These tests check that: +//! Delegates to [`libdd_otel_thread_ctx::autocheck::check_tls_slot_in`] which +//! checks that: //! - `otel_thread_ctx_v1` is exported in the dynamic symbol table as a TLS GLOBAL symbol. -//! - `otel_thread_ctx_v1` does NOT use General Dynamic or Local Dynamic TLS relocations -//! (DTPMOD/DTPOFF). The linker may resolve to TLSDESC or Local Exec; both are acceptable. +//! - `otel_thread_ctx_v1` does NOT use General Dynamic or Local Dynamic TLS relocations. //! //! The cdylib path is derived at runtime from the test executable location. //! Both the test binary and the cdylib live in `target/<[triple/]profile>/deps/`. @@ -14,79 +14,17 @@ #![cfg(target_os = "linux")] use std::path::PathBuf; -use std::process::Command; - -const SYMBOL: &str = "otel_thread_ctx_v1"; fn cdylib_path() -> PathBuf { - // test binary: target/<[triple/]profile>/deps/ - // cdylib: target/<[triple/]profile>/deps/liblibdd_otel_thread_ctx_ffi.so let exe = std::env::current_exe().expect("failed to read current executable path"); exe.parent() .expect("unexpected test executable path structure") .join("liblibdd_otel_thread_ctx_ffi.so") } -fn check_cdylib_readable(path: &PathBuf) { - assert!( - std::fs::File::open(path).is_ok(), - "cdylib at {} could not be opened for reading", - path.display() - ); -} - -fn readelf(args: &[&str], path: &PathBuf) -> String { - let out = Command::new("readelf") - .args(args) - .arg(path) - .output() - .expect("failed to run readelf. Is binutils installed?"); - String::from_utf8_lossy(&out.stdout).into_owned() -} - -#[test] -#[cfg_attr(miri, ignore)] -fn otel_thread_ctx_v1_in_dynsym() { - let path = cdylib_path(); - check_cdylib_readable(&path); - let output = readelf(&["-W", "--dyn-syms"], &path); - let line = output - .lines() - .find(|l| l.contains(SYMBOL)) - .unwrap_or_else(|| panic!("'{SYMBOL}' not found in dynsym of {}", path.display())); - assert!( - line.contains("TLS") && line.contains("GLOBAL"), - "'{SYMBOL}' is in dynsym but not as TLS GLOBAL — got:\n {line}" - ); -} - #[test] #[cfg_attr(miri, ignore)] -fn otel_thread_ctx_v1_no_gd_ld_reloc() { +fn otel_thread_ctx_v1_tls_properties() { let path = cdylib_path(); - check_cdylib_readable(&path); - let output = readelf(&["-W", "--relocs"], &path); - - const FORBIDDEN: &[&str] = &[ - "R_X86_64_DTPMOD64", - "R_X86_64_DTPOFF64", - "R_AARCH64_TLS_DTPMOD", - "R_AARCH64_TLS_DTPREL", - ]; - - let bad_lines: Vec<&str> = output - .lines() - .filter(|l| l.contains(SYMBOL) && FORBIDDEN.iter().any(|f| l.contains(f))) - .collect(); - assert!( - bad_lines.is_empty(), - "'{SYMBOL}' has General Dynamic / Local Dynamic relocations in {}:\n{}\n\ - Expected TLSDESC or Local Exec instead.", - path.display(), - bad_lines - .iter() - .map(|l| format!(" {l}")) - .collect::>() - .join("\n") - ); + libdd_otel_thread_ctx::autocheck::check_tls_slot_in(&path).unwrap(); } diff --git a/libdd-otel-thread-ctx/src/autocheck.rs b/libdd-otel-thread-ctx/src/autocheck.rs index 343271e0be..6c681e66c9 100644 --- a/libdd-otel-thread-ctx/src/autocheck.rs +++ b/libdd-otel-thread-ctx/src/autocheck.rs @@ -1,7 +1,9 @@ // Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 -//! Runtime ELF self-inspection for shared-library linking correctness. +//! Runtime ELF self-inspection for shared library. Verifies that the OTel thread context symbol is +//! discoverable by an out-of-process reader as required by the OTel thread-level context sharing +//! specification. //! //! Call [`check_tls_slot_present`] from within a cdylib context to verify that //! this shared object was linked with the required TLS properties: @@ -15,23 +17,20 @@ //! is enabled. use elf::{abi, endian::AnyEndian, ElfBytes}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; const SYMBOL: &str = "otel_thread_ctx_v1"; -/// Verify that this binary was linked with the correct TLS properties for the -/// OTel thread-level context spec. +/// Verify TLS properties of an ELF file at the given path. /// -/// Locates the ELF file that contains this function (via `/proc/self/maps`) -/// and asserts that `otel_thread_ctx_v1` is exported as a TLS GLOBAL symbol -/// with no General Dynamic or Local Dynamic TLS relocations. +/// Checks that `otel_thread_ctx_v1` is exported as a TLS GLOBAL symbol with no +/// General Dynamic or Local Dynamic TLS relocations. /// /// Returns `Ok(())` on success, or an `Err` with a diagnostic message on /// failure (does not panic). -pub fn check_tls_slot_present() -> Result<(), String> { - let path = own_so_path()?; +pub fn check_tls_slot_in(path: &Path) -> Result<(), String> { let data = - std::fs::read(&path).map_err(|e| format!("failed to read {}: {e}", path.display()))?; + std::fs::read(path).map_err(|e| format!("failed to read {}: {e}", path.display()))?; let elf = ElfBytes::::minimal_parse(&data) .map_err(|e| format!("failed to parse ELF at {}: {e}", path.display()))?; check_dynsym(&elf)?; @@ -39,6 +38,12 @@ pub fn check_tls_slot_present() -> Result<(), String> { Ok(()) } +/// Same as [`check_tls_slot_in`], but automatically locates this shared object +/// via `/proc/self/maps`. Intended for use from within the cdylib at runtime. +pub fn check_tls_slot_present() -> Result<(), String> { + check_tls_slot_in(&own_so_path()?) +} + /// Locate this shared object via `/proc/self/maps` using `check_linking`'s address. fn own_so_path() -> Result { let addr = check_tls_slot_present as *const () as usize; From f76b04141d0a432ef45b3a68bb5ea292be6acd16 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 8 Jun 2026 12:10:23 +0200 Subject: [PATCH 04/14] style: formatting --- libdd-otel-thread-ctx/src/autocheck.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/libdd-otel-thread-ctx/src/autocheck.rs b/libdd-otel-thread-ctx/src/autocheck.rs index 6c681e66c9..d8fac8b2aa 100644 --- a/libdd-otel-thread-ctx/src/autocheck.rs +++ b/libdd-otel-thread-ctx/src/autocheck.rs @@ -8,9 +8,9 @@ //! Call [`check_tls_slot_present`] from within a cdylib context to verify that //! this shared object was linked with the required TLS properties: //! - `otel_thread_ctx_v1` is exported as TLS GLOBAL in the dynamic symbol table. -//! - `otel_thread_ctx_v1` is NOT accessed via General Dynamic or Local Dynamic -//! TLS relocations (DTPMOD/DTPOFF) in `.rela.dyn`. The linker may resolve to -//! TLSDESC or Local Exec depending on optimization; both are acceptable. +//! - `otel_thread_ctx_v1` is NOT accessed via General Dynamic or Local Dynamic TLS relocations +//! (DTPMOD/DTPOFF) in `.rela.dyn`. The linker may resolve to TLSDESC or Local Exec depending on +//! optimization; both are acceptable. //! //! This module is only available on Linux (the only platform that supports the //! TLSDESC dialect used by this crate) and only when the `autocheck` feature @@ -95,10 +95,8 @@ fn check_dynsym(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> { fn check_no_gd_ld_reloc(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> { #[cfg(target_arch = "x86_64")] - const FORBIDDEN_RELOCS: &[(u32, &str)] = &[ - (16, "R_X86_64_DTPMOD64"), - (17, "R_X86_64_DTPOFF64"), - ]; + const FORBIDDEN_RELOCS: &[(u32, &str)] = + &[(16, "R_X86_64_DTPMOD64"), (17, "R_X86_64_DTPOFF64")]; #[cfg(target_arch = "aarch64")] const FORBIDDEN_RELOCS: &[(u32, &str)] = &[ (1028, "R_AARCH64_TLS_DTPMOD"), From aae27566021e21aba6b3b1138f920adb9774badf Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 8 Jun 2026 15:25:07 +0200 Subject: [PATCH 05/14] chore: better naming, improve comments, remove so-specific comments --- libdd-otel-thread-ctx-ffi/Cargo.toml | 4 +- libdd-otel-thread-ctx-ffi/src/lib.rs | 2 +- .../tests/elf_properties.rs | 2 +- libdd-otel-thread-ctx/Cargo.toml | 2 +- libdd-otel-thread-ctx/src/lib.rs | 4 +- .../src/{autocheck.rs => sanity_check.rs} | 41 +++++++++---------- 6 files changed, 27 insertions(+), 28 deletions(-) rename libdd-otel-thread-ctx/src/{autocheck.rs => sanity_check.rs} (77%) diff --git a/libdd-otel-thread-ctx-ffi/Cargo.toml b/libdd-otel-thread-ctx-ffi/Cargo.toml index b4a2c9545d..1c0f288628 100644 --- a/libdd-otel-thread-ctx-ffi/Cargo.toml +++ b/libdd-otel-thread-ctx-ffi/Cargo.toml @@ -21,10 +21,10 @@ libdd-otel-thread-ctx = { path = "../libdd-otel-thread-ctx" } [features] default = ["cbindgen"] cbindgen = ["build_common/cbindgen", "libdd-common-ffi/cbindgen"] -autocheck = ["dep:libdd-common-ffi", "libdd-otel-thread-ctx/autocheck"] +sanity-check = ["dep:libdd-common-ffi", "libdd-otel-thread-ctx/sanity-check"] [dev-dependencies] -libdd-otel-thread-ctx = { path = "../libdd-otel-thread-ctx", features = ["autocheck"] } +libdd-otel-thread-ctx = { path = "../libdd-otel-thread-ctx", features = ["sanity-check"] } [build-dependencies] build_common = { path = "../build-common" } diff --git a/libdd-otel-thread-ctx-ffi/src/lib.rs b/libdd-otel-thread-ctx-ffi/src/lib.rs index e4c68a6964..3012a062e1 100644 --- a/libdd-otel-thread-ctx-ffi/src/lib.rs +++ b/libdd-otel-thread-ctx-ffi/src/lib.rs @@ -13,7 +13,7 @@ pub use linux::*; /// /// Returns `VoidResult::Ok` if all checks pass, or a `VoidResult::Err` with a /// diagnostic message on failure. -#[cfg(all(target_os = "linux", feature = "autocheck"))] +#[cfg(all(target_os = "linux", feature = "sanity-check"))] #[no_mangle] pub extern "C" fn ddog_otel_thread_ctx_autocheck() -> libdd_common_ffi::VoidResult { match libdd_otel_thread_ctx::autocheck::check_tls_slot_present() { diff --git a/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs b/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs index 8e4d03bef9..93cd1b37c7 100644 --- a/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs +++ b/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs @@ -26,5 +26,5 @@ fn cdylib_path() -> PathBuf { #[cfg_attr(miri, ignore)] fn otel_thread_ctx_v1_tls_properties() { let path = cdylib_path(); - libdd_otel_thread_ctx::autocheck::check_tls_slot_in(&path).unwrap(); + libdd_otel_thread_ctx::sanity_check::check_tls_slot_in(&path).unwrap(); } diff --git a/libdd-otel-thread-ctx/Cargo.toml b/libdd-otel-thread-ctx/Cargo.toml index 0ef6baf27b..095a723f4e 100644 --- a/libdd-otel-thread-ctx/Cargo.toml +++ b/libdd-otel-thread-ctx/Cargo.toml @@ -20,7 +20,7 @@ bench = false elf = { version = "0.7", optional = true } [features] -autocheck = ["dep:elf"] +sanity-check = ["dep:elf"] [build-dependencies] build_common = { path = "../build-common" } diff --git a/libdd-otel-thread-ctx/src/lib.rs b/libdd-otel-thread-ctx/src/lib.rs index 0741fdaf01..10142a4c12 100644 --- a/libdd-otel-thread-ctx/src/lib.rs +++ b/libdd-otel-thread-ctx/src/lib.rs @@ -64,8 +64,8 @@ //! `atomic_signal_fence`) to keep field writes boxed between the `valid = 0` and `valid = 1` //! stores during in-place updates. -#[cfg(all(target_os = "linux", feature = "autocheck"))] -pub mod autocheck; +#[cfg(all(target_os = "linux", feature = "sanity-check"))] +pub mod sanity_check; #[cfg(target_os = "linux")] pub mod linux { diff --git a/libdd-otel-thread-ctx/src/autocheck.rs b/libdd-otel-thread-ctx/src/sanity_check.rs similarity index 77% rename from libdd-otel-thread-ctx/src/autocheck.rs rename to libdd-otel-thread-ctx/src/sanity_check.rs index d8fac8b2aa..3595f9ee72 100644 --- a/libdd-otel-thread-ctx/src/autocheck.rs +++ b/libdd-otel-thread-ctx/src/sanity_check.rs @@ -5,29 +5,23 @@ //! discoverable by an out-of-process reader as required by the OTel thread-level context sharing //! specification. //! -//! Call [`check_tls_slot_present`] from within a cdylib context to verify that -//! this shared object was linked with the required TLS properties: +//! Call [`check_tls_slot_present`] from within a shared object or a statically linked executables +//! to verify that the binary was linked with the correct option: //! - `otel_thread_ctx_v1` is exported as TLS GLOBAL in the dynamic symbol table. //! - `otel_thread_ctx_v1` is NOT accessed via General Dynamic or Local Dynamic TLS relocations -//! (DTPMOD/DTPOFF) in `.rela.dyn`. The linker may resolve to TLSDESC or Local Exec depending on +//! (DTPMOD/DTPOFF) in `.rela.dyn`. The linker may pick TLSDESC or Local Exec depending on //! optimization; both are acceptable. //! -//! This module is only available on Linux (the only platform that supports the -//! TLSDESC dialect used by this crate) and only when the `autocheck` feature -//! is enabled. +//! This module is only available on Linux (the only platform that supports the TLSDESC dialect used +//! by this crate) and only when the `sanity-check` feature is enabled. use elf::{abi, endian::AnyEndian, ElfBytes}; use std::path::{Path, PathBuf}; const SYMBOL: &str = "otel_thread_ctx_v1"; -/// Verify TLS properties of an ELF file at the given path. -/// -/// Checks that `otel_thread_ctx_v1` is exported as a TLS GLOBAL symbol with no -/// General Dynamic or Local Dynamic TLS relocations. -/// -/// Returns `Ok(())` on success, or an `Err` with a diagnostic message on -/// failure (does not panic). +/// Safe as [sanity_check], but takes the object file as an argument. Useful for a test setting +/// where the test code is separate from the artifact to validate. pub fn check_tls_slot_in(path: &Path) -> Result<(), String> { let data = std::fs::read(path).map_err(|e| format!("failed to read {}: {e}", path.display()))?; @@ -38,15 +32,20 @@ pub fn check_tls_slot_in(path: &Path) -> Result<(), String> { Ok(()) } -/// Same as [`check_tls_slot_in`], but automatically locates this shared object -/// via `/proc/self/maps`. Intended for use from within the cdylib at runtime. -pub fn check_tls_slot_present() -> Result<(), String> { - check_tls_slot_in(&own_so_path()?) +/// Check that the current running module has been linked appropriately to make the OTel shared +/// thread context discoverable. +/// +/// Checks that `otel_thread_ctx_v1` is exported as a TLS GLOBAL symbol with no General Dynamic or +/// Local Dynamic TLS relocations. It's an indirect check for TLSDESC, which implies either no +/// relocations (Local Exec/static binary case), or a TLSDESC relocation (dynamic library case). +pub fn sanity_check() -> Result<(), String> { + check_tls_slot_in(&own_elf_path()?) } -/// Locate this shared object via `/proc/self/maps` using `check_linking`'s address. -fn own_so_path() -> Result { - let addr = check_tls_slot_present as *const () as usize; +/// Locate the current running module (shared or not) via `/proc/self/maps`. +fn own_elf_path() -> Result { + // We use the address of an arbitrary function of this module. + let addr = sanity_check as *const () as usize; let maps = std::fs::read_to_string("/proc/self/maps") .map_err(|e| format!("failed to read /proc/self/maps: {e}"))?; for line in maps.lines() { @@ -67,7 +66,7 @@ fn own_so_path() -> Result { } } } - Err("could not find our shared object in /proc/self/maps".into()) + Err("could not find our own object file in /proc/self/maps".into()) } fn check_dynsym(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> { From 06bb468de7eeeb760e53a413f0056750e47aabcb Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 8 Jun 2026 15:35:26 +0200 Subject: [PATCH 06/14] doc: add documentation to a couple functions --- libdd-otel-thread-ctx/src/sanity_check.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libdd-otel-thread-ctx/src/sanity_check.rs b/libdd-otel-thread-ctx/src/sanity_check.rs index 3595f9ee72..105f6c8036 100644 --- a/libdd-otel-thread-ctx/src/sanity_check.rs +++ b/libdd-otel-thread-ctx/src/sanity_check.rs @@ -69,6 +69,7 @@ fn own_elf_path() -> Result { Err("could not find our own object file in /proc/self/maps".into()) } +/// Check that [SYMBOL] is present in the `.dynsym` table of the ELF data. fn check_dynsym(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> { let (symtab, strtab) = elf .dynamic_symbol_table() @@ -92,6 +93,9 @@ fn check_dynsym(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> { Ok(()) } +/// Check that there's either no TLS relocation for [SYMBOL] in the given ELF file, or if there is, +/// it's a TLSDESC one. In practice, the check is negative: we check for the absence of relocations +/// associated with the General Dynamic or Local Dynamic TLS access model. fn check_no_gd_ld_reloc(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> { #[cfg(target_arch = "x86_64")] const FORBIDDEN_RELOCS: &[(u32, &str)] = From 53823b2aa16dddf2e4a52adf4157ef5cd33c417d Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 8 Jun 2026 15:39:26 +0200 Subject: [PATCH 07/14] fix: autocheck -> sanity_check --- libdd-otel-thread-ctx-ffi/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libdd-otel-thread-ctx-ffi/src/lib.rs b/libdd-otel-thread-ctx-ffi/src/lib.rs index 3012a062e1..ec77d043d7 100644 --- a/libdd-otel-thread-ctx-ffi/src/lib.rs +++ b/libdd-otel-thread-ctx-ffi/src/lib.rs @@ -15,8 +15,8 @@ pub use linux::*; /// diagnostic message on failure. #[cfg(all(target_os = "linux", feature = "sanity-check"))] #[no_mangle] -pub extern "C" fn ddog_otel_thread_ctx_autocheck() -> libdd_common_ffi::VoidResult { - match libdd_otel_thread_ctx::autocheck::check_tls_slot_present() { +pub extern "C" fn ddog_otel_thread_ctx_sanity_check() -> libdd_common_ffi::VoidResult { + match libdd_otel_thread_ctx::sanity_check::sanity_check() { Ok(()) => libdd_common_ffi::VoidResult::Ok, Err(e) => libdd_common_ffi::VoidResult::Err(libdd_common_ffi::Error::from(e)), } From cf189d36f0ee9ae8f5646d9e5465e4bc3efe6bcb Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 8 Jun 2026 15:41:26 +0200 Subject: [PATCH 08/14] doc: fix dead link --- libdd-otel-thread-ctx/src/sanity_check.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libdd-otel-thread-ctx/src/sanity_check.rs b/libdd-otel-thread-ctx/src/sanity_check.rs index 105f6c8036..75e2e39261 100644 --- a/libdd-otel-thread-ctx/src/sanity_check.rs +++ b/libdd-otel-thread-ctx/src/sanity_check.rs @@ -5,8 +5,9 @@ //! discoverable by an out-of-process reader as required by the OTel thread-level context sharing //! specification. //! -//! Call [`check_tls_slot_present`] from within a shared object or a statically linked executables -//! to verify that the binary was linked with the correct option: +//! Call [`sanity_check`] from within a shared object or a statically linked executables to verify +//! that the binary was linked with the correct option: +//! //! - `otel_thread_ctx_v1` is exported as TLS GLOBAL in the dynamic symbol table. //! - `otel_thread_ctx_v1` is NOT accessed via General Dynamic or Local Dynamic TLS relocations //! (DTPMOD/DTPOFF) in `.rela.dyn`. The linker may pick TLSDESC or Local Exec depending on From 9fa260d6ae14ce7bc67995fb84dda0a970e20bf6 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 8 Jun 2026 15:42:38 +0200 Subject: [PATCH 09/14] doc: fix typo --- libdd-otel-thread-ctx/src/sanity_check.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libdd-otel-thread-ctx/src/sanity_check.rs b/libdd-otel-thread-ctx/src/sanity_check.rs index 75e2e39261..b602c1f2a4 100644 --- a/libdd-otel-thread-ctx/src/sanity_check.rs +++ b/libdd-otel-thread-ctx/src/sanity_check.rs @@ -38,7 +38,7 @@ pub fn check_tls_slot_in(path: &Path) -> Result<(), String> { /// /// Checks that `otel_thread_ctx_v1` is exported as a TLS GLOBAL symbol with no General Dynamic or /// Local Dynamic TLS relocations. It's an indirect check for TLSDESC, which implies either no -/// relocations (Local Exec/static binary case), or a TLSDESC relocation (dynamic library case). +/// relocation (Local Exec/static binary case) or a TLSDESC relocation (dynamic library case). pub fn sanity_check() -> Result<(), String> { check_tls_slot_in(&own_elf_path()?) } From e548fe9da231fd053b4349334fa6d56a4e557ced Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 8 Jun 2026 15:51:18 +0200 Subject: [PATCH 10/14] refactor: use anyhow instead of raw strings for errors --- Cargo.lock | 1 + libdd-otel-thread-ctx/Cargo.toml | 3 +- libdd-otel-thread-ctx/src/sanity_check.rs | 44 +++++++++++------------ 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2bf6008d59..9322bd0bf1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3183,6 +3183,7 @@ dependencies = [ name = "libdd-otel-thread-ctx" version = "1.0.0" dependencies = [ + "anyhow", "build_common", "cc", "elf", diff --git a/libdd-otel-thread-ctx/Cargo.toml b/libdd-otel-thread-ctx/Cargo.toml index 095a723f4e..0580970a96 100644 --- a/libdd-otel-thread-ctx/Cargo.toml +++ b/libdd-otel-thread-ctx/Cargo.toml @@ -17,10 +17,11 @@ crate-type = ["lib"] bench = false [dependencies] +anyhow = { version = "1.0", optional = true } elf = { version = "0.7", optional = true } [features] -sanity-check = ["dep:elf"] +sanity-check = ["dep:elf", "dep:anyhow"] [build-dependencies] build_common = { path = "../build-common" } diff --git a/libdd-otel-thread-ctx/src/sanity_check.rs b/libdd-otel-thread-ctx/src/sanity_check.rs index b602c1f2a4..eecbc5bb46 100644 --- a/libdd-otel-thread-ctx/src/sanity_check.rs +++ b/libdd-otel-thread-ctx/src/sanity_check.rs @@ -16,6 +16,7 @@ //! This module is only available on Linux (the only platform that supports the TLSDESC dialect used //! by this crate) and only when the `sanity-check` feature is enabled. +use anyhow::{bail, Context}; use elf::{abi, endian::AnyEndian, ElfBytes}; use std::path::{Path, PathBuf}; @@ -23,11 +24,10 @@ const SYMBOL: &str = "otel_thread_ctx_v1"; /// Safe as [sanity_check], but takes the object file as an argument. Useful for a test setting /// where the test code is separate from the artifact to validate. -pub fn check_tls_slot_in(path: &Path) -> Result<(), String> { - let data = - std::fs::read(path).map_err(|e| format!("failed to read {}: {e}", path.display()))?; +pub fn check_tls_slot_in(path: &Path) -> anyhow::Result<()> { + let data = std::fs::read(path).with_context(|| format!("failed to read {}", path.display()))?; let elf = ElfBytes::::minimal_parse(&data) - .map_err(|e| format!("failed to parse ELF at {}: {e}", path.display()))?; + .with_context(|| format!("failed to parse ELF at {}", path.display()))?; check_dynsym(&elf)?; check_no_gd_ld_reloc(&elf)?; Ok(()) @@ -39,16 +39,16 @@ pub fn check_tls_slot_in(path: &Path) -> Result<(), String> { /// Checks that `otel_thread_ctx_v1` is exported as a TLS GLOBAL symbol with no General Dynamic or /// Local Dynamic TLS relocations. It's an indirect check for TLSDESC, which implies either no /// relocation (Local Exec/static binary case) or a TLSDESC relocation (dynamic library case). -pub fn sanity_check() -> Result<(), String> { +pub fn sanity_check() -> anyhow::Result<()> { check_tls_slot_in(&own_elf_path()?) } /// Locate the current running module (shared or not) via `/proc/self/maps`. -fn own_elf_path() -> Result { +fn own_elf_path() -> anyhow::Result { // We use the address of an arbitrary function of this module. let addr = sanity_check as *const () as usize; - let maps = std::fs::read_to_string("/proc/self/maps") - .map_err(|e| format!("failed to read /proc/self/maps: {e}"))?; + let maps = + std::fs::read_to_string("/proc/self/maps").context("failed to read /proc/self/maps")?; for line in maps.lines() { // Format: address perms offset dev inode [pathname] let fields: Vec<&str> = line.split_whitespace().collect(); @@ -67,15 +67,15 @@ fn own_elf_path() -> Result { } } } - Err("could not find our own object file in /proc/self/maps".into()) + bail!("could not find our own object file in /proc/self/maps") } /// Check that [SYMBOL] is present in the `.dynsym` table of the ELF data. -fn check_dynsym(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> { +fn check_dynsym(elf: &ElfBytes<'_, AnyEndian>) -> anyhow::Result<()> { let (symtab, strtab) = elf .dynamic_symbol_table() - .map_err(|e| format!("failed to read .dynsym: {e}"))? - .ok_or_else(|| "no dynamic symbol table found".to_string())?; + .context("failed to read .dynsym")? + .context("no dynamic symbol table found")?; let found = symtab.iter().any(|sym| { strtab .get(sym.st_name as usize) @@ -87,9 +87,7 @@ fn check_dynsym(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> { .unwrap_or(false) }); if !found { - return Err(format!( - "'{SYMBOL}' not found as TLS GLOBAL in dynamic symbol table" - )); + bail!("'{SYMBOL}' not found as TLS GLOBAL in dynamic symbol table"); } Ok(()) } @@ -97,7 +95,7 @@ fn check_dynsym(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> { /// Check that there's either no TLS relocation for [SYMBOL] in the given ELF file, or if there is, /// it's a TLSDESC one. In practice, the check is negative: we check for the absence of relocations /// associated with the General Dynamic or Local Dynamic TLS access model. -fn check_no_gd_ld_reloc(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> { +fn check_no_gd_ld_reloc(elf: &ElfBytes<'_, AnyEndian>) -> anyhow::Result<()> { #[cfg(target_arch = "x86_64")] const FORBIDDEN_RELOCS: &[(u32, &str)] = &[(16, "R_X86_64_DTPMOD64"), (17, "R_X86_64_DTPOFF64")]; @@ -109,8 +107,8 @@ fn check_no_gd_ld_reloc(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> { let (symtab, strtab) = elf .dynamic_symbol_table() - .map_err(|e| format!("failed to read .dynsym: {e}"))? - .ok_or_else(|| "no dynamic symbol table found".to_string())?; + .context("failed to read .dynsym")? + .context("no dynamic symbol table found")?; let sym_idx = symtab .iter() .enumerate() @@ -121,16 +119,16 @@ fn check_no_gd_ld_reloc(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> { .unwrap_or(false) }) .map(|(i, _)| i as u32) - .ok_or_else(|| format!("'{SYMBOL}' not found in .dynsym"))?; + .with_context(|| format!("'{SYMBOL}' not found in .dynsym"))?; let rela_shdr = elf .section_header_by_name(".rela.dyn") - .map_err(|e| format!("failed to read section headers: {e}"))?; + .context("failed to read section headers")?; if let Some(rela_shdr) = rela_shdr { let bad: Vec<&str> = elf .section_data_as_relas(&rela_shdr) - .map_err(|e| format!("failed to read .rela.dyn: {e}"))? + .context("failed to read .rela.dyn")? .filter(|r| r.r_sym == sym_idx) .filter_map(|r| { FORBIDDEN_RELOCS @@ -140,11 +138,11 @@ fn check_no_gd_ld_reloc(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> { }) .collect(); if !bad.is_empty() { - return Err(format!( + bail!( "'{SYMBOL}' has General Dynamic / Local Dynamic relocations in .rela.dyn: {}. \ Expected TLSDESC or Local Exec instead.", bad.join(", ") - )); + ); } } From 55ae539e98ed9ff17349e0a0a96740a7856fcc00 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 8 Jun 2026 16:28:25 +0200 Subject: [PATCH 11/14] chore: update license file --- LICENSE-3rdparty.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 16d8a34b9e..332e65ee3a 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -125,6 +125,7 @@ dispatch2,https://github.com/madsmtm/objc2,Zlib OR Apache-2.0 OR MIT,"Mads Marqu displaydoc,https://github.com/yaahc/displaydoc,MIT OR Apache-2.0,Jane Lusby dyn-clone,https://github.com/dtolnay/dyn-clone,MIT OR Apache-2.0,David Tolnay either,https://github.com/rayon-rs/either,MIT OR Apache-2.0,bluss +elf,https://github.com/cole14/rust-elf,MIT OR Apache-2.0,Christopher Cole encoding_rs,https://github.com/hsivonen/encoding_rs,(Apache-2.0 OR MIT) AND BSD-3-Clause,Henri Sivonen enum-as-inner,https://github.com/bluejekyll/enum-as-inner,MIT OR Apache-2.0,Benjamin Fry equivalent,https://github.com/cuviper/equivalent,Apache-2.0 OR MIT,The equivalent Authors From 3252e7acb9289dc6f9c2ec0426e1b2604476c3c4 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 8 Jun 2026 17:48:05 +0200 Subject: [PATCH 12/14] fix: reject all non-TLSDESC TLS relocations in sanity check The previous check only forbade GD/LD relocations (DTPMOD/DTPOFF), missing other TLS relocation types like TPOFF64 or GOTTPOFF. Flip from a blocklist to an allowlist: only TLSDESC is accepted. Rename check_no_gd_ld_reloc -> check_tlsdesc_reloc_only accordingly. Co-Authored-By: Claude Opus 4.6 --- .../tests/elf_properties.rs | 2 +- libdd-otel-thread-ctx/src/sanity_check.rs | 48 ++++++++----------- 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs b/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs index 93cd1b37c7..4063fd06b3 100644 --- a/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs +++ b/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs @@ -6,7 +6,7 @@ //! Delegates to [`libdd_otel_thread_ctx::autocheck::check_tls_slot_in`] which //! checks that: //! - `otel_thread_ctx_v1` is exported in the dynamic symbol table as a TLS GLOBAL symbol. -//! - `otel_thread_ctx_v1` does NOT use General Dynamic or Local Dynamic TLS relocations. +//! - `otel_thread_ctx_v1` has no non-TLSDESC TLS relocations. //! //! The cdylib path is derived at runtime from the test executable location. //! Both the test binary and the cdylib live in `target/<[triple/]profile>/deps/`. diff --git a/libdd-otel-thread-ctx/src/sanity_check.rs b/libdd-otel-thread-ctx/src/sanity_check.rs index eecbc5bb46..c7456ee969 100644 --- a/libdd-otel-thread-ctx/src/sanity_check.rs +++ b/libdd-otel-thread-ctx/src/sanity_check.rs @@ -9,9 +9,9 @@ //! that the binary was linked with the correct option: //! //! - `otel_thread_ctx_v1` is exported as TLS GLOBAL in the dynamic symbol table. -//! - `otel_thread_ctx_v1` is NOT accessed via General Dynamic or Local Dynamic TLS relocations -//! (DTPMOD/DTPOFF) in `.rela.dyn`. The linker may pick TLSDESC or Local Exec depending on -//! optimization; both are acceptable. +//! - `otel_thread_ctx_v1` has no non-TLSDESC TLS relocations in `.rela.dyn`. The linker may pick +//! TLSDESC or Local Exec depending on optimization; both are acceptable. All other TLS relocation +//! types (DTPMOD, DTPOFF, TPOFF, GOTTPOFF, etc.) are rejected. //! //! This module is only available on Linux (the only platform that supports the TLSDESC dialect used //! by this crate) and only when the `sanity-check` feature is enabled. @@ -29,16 +29,16 @@ pub fn check_tls_slot_in(path: &Path) -> anyhow::Result<()> { let elf = ElfBytes::::minimal_parse(&data) .with_context(|| format!("failed to parse ELF at {}", path.display()))?; check_dynsym(&elf)?; - check_no_gd_ld_reloc(&elf)?; + check_tlsdesc_reloc_only(&elf)?; Ok(()) } /// Check that the current running module has been linked appropriately to make the OTel shared /// thread context discoverable. /// -/// Checks that `otel_thread_ctx_v1` is exported as a TLS GLOBAL symbol with no General Dynamic or -/// Local Dynamic TLS relocations. It's an indirect check for TLSDESC, which implies either no -/// relocation (Local Exec/static binary case) or a TLSDESC relocation (dynamic library case). +/// Checks that `otel_thread_ctx_v1` is exported as a TLS GLOBAL symbol and that any TLS +/// relocations targeting it are TLSDESC. No relocation (Local Exec/static binary) is also +/// acceptable. pub fn sanity_check() -> anyhow::Result<()> { check_tls_slot_in(&own_elf_path()?) } @@ -92,18 +92,14 @@ fn check_dynsym(elf: &ElfBytes<'_, AnyEndian>) -> anyhow::Result<()> { Ok(()) } -/// Check that there's either no TLS relocation for [SYMBOL] in the given ELF file, or if there is, -/// it's a TLSDESC one. In practice, the check is negative: we check for the absence of relocations -/// associated with the General Dynamic or Local Dynamic TLS access model. -fn check_no_gd_ld_reloc(elf: &ElfBytes<'_, AnyEndian>) -> anyhow::Result<()> { +/// Check that any relocation for [SYMBOL] in `.rela.dyn` is a TLSDESC relocation. No relocation at +/// all (Local Exec / static binary) is also acceptable. All other TLS relocation types (DTPMOD, +/// DTPOFF, TPOFF, GOTTPOFF, etc.) are rejected. +fn check_tlsdesc_reloc_only(elf: &ElfBytes<'_, AnyEndian>) -> anyhow::Result<()> { #[cfg(target_arch = "x86_64")] - const FORBIDDEN_RELOCS: &[(u32, &str)] = - &[(16, "R_X86_64_DTPMOD64"), (17, "R_X86_64_DTPOFF64")]; + const TLSDESC_RELOC: u32 = 36; // R_X86_64_TLSDESC #[cfg(target_arch = "aarch64")] - const FORBIDDEN_RELOCS: &[(u32, &str)] = &[ - (1028, "R_AARCH64_TLS_DTPMOD"), - (1029, "R_AARCH64_TLS_DTPREL"), - ]; + const TLSDESC_RELOC: u32 = 1031; // R_AARCH64_TLSDESC let (symtab, strtab) = elf .dynamic_symbol_table() @@ -126,22 +122,18 @@ fn check_no_gd_ld_reloc(elf: &ElfBytes<'_, AnyEndian>) -> anyhow::Result<()> { .context("failed to read section headers")?; if let Some(rela_shdr) = rela_shdr { - let bad: Vec<&str> = elf + let bad: Vec = elf .section_data_as_relas(&rela_shdr) .context("failed to read .rela.dyn")? - .filter(|r| r.r_sym == sym_idx) - .filter_map(|r| { - FORBIDDEN_RELOCS - .iter() - .find(|(typ, _)| *typ == r.r_type) - .map(|(_, name)| *name) - }) + .filter(|r| r.r_sym == sym_idx && r.r_type != TLSDESC_RELOC) + .map(|r| r.r_type) .collect(); if !bad.is_empty() { + let types: Vec = bad.iter().map(|t| format!("type {t}")).collect(); bail!( - "'{SYMBOL}' has General Dynamic / Local Dynamic relocations in .rela.dyn: {}. \ - Expected TLSDESC or Local Exec instead.", - bad.join(", ") + "'{SYMBOL}' has non-TLSDESC relocations in .rela.dyn: {}. \ + Only TLSDESC or no relocation (Local Exec) is accepted.", + types.join(", ") ); } } From c0ec04781f1dbf2a5444388d5ca8b9fc490f8bec Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 9 Jun 2026 10:36:31 +0200 Subject: [PATCH 13/14] doc: avoid double negative in constraint description --- libdd-otel-thread-ctx-ffi/tests/elf_properties.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs b/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs index 4063fd06b3..fc24f8d387 100644 --- a/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs +++ b/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs @@ -6,7 +6,8 @@ //! Delegates to [`libdd_otel_thread_ctx::autocheck::check_tls_slot_in`] which //! checks that: //! - `otel_thread_ctx_v1` is exported in the dynamic symbol table as a TLS GLOBAL symbol. -//! - `otel_thread_ctx_v1` has no non-TLSDESC TLS relocations. +//! - `otel_thread_ctx_v1` follows the TLSDESC access model (if there's a relocation, it's a TLSDESC +//! one). //! //! The cdylib path is derived at runtime from the test executable location. //! Both the test binary and the cdylib live in `target/<[triple/]profile>/deps/`. From 804db1da9a0a033c702ac92e5fe2d906ead94be9 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 9 Jun 2026 11:25:29 +0200 Subject: [PATCH 14/14] fix: cargo doc warnings --- libdd-otel-thread-ctx-ffi/tests/elf_properties.rs | 2 +- libdd-otel-thread-ctx/src/sanity_check.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs b/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs index fc24f8d387..3281ca1d2d 100644 --- a/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs +++ b/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs @@ -7,7 +7,7 @@ //! checks that: //! - `otel_thread_ctx_v1` is exported in the dynamic symbol table as a TLS GLOBAL symbol. //! - `otel_thread_ctx_v1` follows the TLSDESC access model (if there's a relocation, it's a TLSDESC -//! one). +//! one). //! //! The cdylib path is derived at runtime from the test executable location. //! Both the test binary and the cdylib live in `target/<[triple/]profile>/deps/`. diff --git a/libdd-otel-thread-ctx/src/sanity_check.rs b/libdd-otel-thread-ctx/src/sanity_check.rs index c7456ee969..f71b5c0e2f 100644 --- a/libdd-otel-thread-ctx/src/sanity_check.rs +++ b/libdd-otel-thread-ctx/src/sanity_check.rs @@ -9,9 +9,9 @@ //! that the binary was linked with the correct option: //! //! - `otel_thread_ctx_v1` is exported as TLS GLOBAL in the dynamic symbol table. -//! - `otel_thread_ctx_v1` has no non-TLSDESC TLS relocations in `.rela.dyn`. The linker may pick -//! TLSDESC or Local Exec depending on optimization; both are acceptable. All other TLS relocation -//! types (DTPMOD, DTPOFF, TPOFF, GOTTPOFF, etc.) are rejected. +//! - `otel_thread_ctx_v1` follows the TLSDESC model: there's either no relocation in `.rela.dyn` +//! (Local Exec), or a TLSDESC one. All other TLS relocation types (DTPMOD, DTPOFF, TPOFF, +//! GOTTPOFF, etc.) are rejected. //! //! This module is only available on Linux (the only platform that supports the TLSDESC dialect used //! by this crate) and only when the `sanity-check` feature is enabled.