Skip to content

Commit 6cca8ac

Browse files
committed
feat: add setup autotest capability to otel thread ctx
1 parent 2acc549 commit 6cca8ac

7 files changed

Lines changed: 158 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libdd-otel-thread-ctx-ffi/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ crate-type = ["staticlib", "cdylib", "lib"]
1515
bench = false
1616

1717
[dependencies]
18-
libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false }
18+
libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false, optional = true }
1919
libdd-otel-thread-ctx = { path = "../libdd-otel-thread-ctx" }
2020

2121
[features]
2222
default = ["cbindgen"]
2323
cbindgen = ["build_common/cbindgen", "libdd-common-ffi/cbindgen"]
24+
autocheck = ["dep:libdd-common-ffi", "libdd-otel-thread-ctx/autocheck"]
2425

2526
[build-dependencies]
2627
build_common = { path = "../build-common" }

libdd-otel-thread-ctx-ffi/cbindgen.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ renaming_overrides_prefixing = true
2626
# We map it to plain uint8_t in the C header, since it has the same
2727
# representation.
2828
"AtomicU8" = "uint8_t"
29+
"VoidResult" = "ddog_VoidResult"
30+
"Error" = "ddog_Error"
2931

3032
[export.mangle]
3133
rename_types = "PascalCase"

libdd-otel-thread-ctx-ffi/src/lib.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@
99
#[cfg(target_os = "linux")]
1010
pub use linux::*;
1111

12+
/// Verify that this binary was linked with the correct options such that the thread contexts are
13+
/// visible to an external reader (typically the eBPF profiler).
14+
///
15+
/// Returns `VoidResult::Ok` if all checks pass, or a `VoidResult::Err` with a
16+
/// diagnostic message on failure.
17+
#[cfg(all(target_os = "linux", feature = "autocheck"))]
18+
#[no_mangle]
19+
pub extern "C" fn ddog_otel_thread_ctx_autocheck() -> libdd_common_ffi::VoidResult {
20+
match libdd_otel_thread_ctx::autocheck::check_tlsdesc_slot_present() {
21+
Ok(()) => libdd_common_ffi::VoidResult::Ok,
22+
Err(e) => libdd_common_ffi::VoidResult::Err(libdd_common_ffi::Error::from(e)),
23+
}
24+
}
25+
1226
#[cfg(target_os = "linux")]
1327
mod linux {
1428
use libdd_otel_thread_ctx::linux::{ThreadContext, ThreadContextRecord};

libdd-otel-thread-ctx/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,11 @@ publish = false
1616
crate-type = ["lib"]
1717
bench = false
1818

19+
[dependencies]
20+
elf = { version = "0.7", optional = true }
21+
22+
[features]
23+
autocheck = ["dep:elf"]
24+
1925
[build-dependencies]
2026
cc = "1.1.31"
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Runtime ELF self-inspection for shared-library linking correctness.
5+
//!
6+
//! Call [`check_linking`] from within a cdylib context to verify that this
7+
//! shared object was linked with the required TLS properties:
8+
//! - `otel_thread_ctx_v1` is exported as TLS GLOBAL in the dynamic symbol table.
9+
//! - `otel_thread_ctx_v1` is accessed via a TLSDESC relocation in `.rela.dyn`.
10+
//!
11+
//! This module is only available on Linux (the only platform that supports the
12+
//! TLSDESC dialect used by this crate) and only when the `autocheck` feature
13+
//! is enabled.
14+
15+
use elf::{abi, endian::AnyEndian, ElfBytes};
16+
use std::path::PathBuf;
17+
18+
const SYMBOL: &str = "otel_thread_ctx_v1";
19+
20+
/// Verify that this binary was linked with the correct TLS properties for the
21+
/// OTel thread-level context spec.
22+
///
23+
/// Locates the ELF file that contains this function (via `/proc/self/maps`)
24+
/// and asserts that `otel_thread_ctx_v1` is exported as a TLS GLOBAL symbol
25+
/// accessed through a TLSDESC relocation.
26+
///
27+
/// Returns `Ok(())` on success, or an `Err` with a diagnostic message on
28+
/// failure (does not panic).
29+
pub fn check_tlsdesc_slot_present() -> Result<(), String> {
30+
let path = own_so_path()?;
31+
let data =
32+
std::fs::read(&path).map_err(|e| format!("failed to read {}: {e}", path.display()))?;
33+
let elf = ElfBytes::<AnyEndian>::minimal_parse(&data)
34+
.map_err(|e| format!("failed to parse ELF at {}: {e}", path.display()))?;
35+
check_dynsym(&elf)?;
36+
check_tlsdesc_reloc(&elf)?;
37+
Ok(())
38+
}
39+
40+
/// Locate this shared object via `/proc/self/maps` using `check_linking`'s address.
41+
fn own_so_path() -> Result<PathBuf, String> {
42+
let addr = check_tlsdesc_slot_present as *const () as usize;
43+
let maps = std::fs::read_to_string("/proc/self/maps")
44+
.map_err(|e| format!("failed to read /proc/self/maps: {e}"))?;
45+
for line in maps.lines() {
46+
// Format: address perms offset dev inode [pathname]
47+
let fields: Vec<&str> = line.split_whitespace().collect();
48+
if fields.len() < 6 {
49+
continue;
50+
}
51+
let path = fields[5];
52+
if !path.starts_with('/') {
53+
continue;
54+
}
55+
if let Some((start_str, end_str)) = fields[0].split_once('-') {
56+
let start = usize::from_str_radix(start_str, 16).unwrap_or(0);
57+
let end = usize::from_str_radix(end_str, 16).unwrap_or(0);
58+
if addr >= start && addr < end {
59+
return Ok(PathBuf::from(path));
60+
}
61+
}
62+
}
63+
Err("could not find our shared object in /proc/self/maps".into())
64+
}
65+
66+
fn check_dynsym(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> {
67+
let (symtab, strtab) = elf
68+
.dynamic_symbol_table()
69+
.map_err(|e| format!("failed to read .dynsym: {e}"))?
70+
.ok_or_else(|| "no dynamic symbol table found".to_string())?;
71+
let found = symtab.iter().any(|sym| {
72+
strtab
73+
.get(sym.st_name as usize)
74+
.map(|name| {
75+
name == SYMBOL
76+
&& sym.st_symtype() == abi::STT_TLS
77+
&& sym.st_bind() == abi::STB_GLOBAL
78+
})
79+
.unwrap_or(false)
80+
});
81+
if !found {
82+
return Err(format!(
83+
"'{SYMBOL}' not found as TLS GLOBAL in dynamic symbol table"
84+
));
85+
}
86+
Ok(())
87+
}
88+
89+
fn check_tlsdesc_reloc(elf: &ElfBytes<'_, AnyEndian>) -> Result<(), String> {
90+
#[cfg(target_arch = "x86_64")]
91+
const R_TLSDESC: u32 = 36; // R_X86_64_TLSDESC
92+
#[cfg(target_arch = "aarch64")]
93+
const R_TLSDESC: u32 = 1031; // R_AARCH64_TLSDESC
94+
95+
// Find the .dynsym index of the target symbol.
96+
let (symtab, strtab) = elf
97+
.dynamic_symbol_table()
98+
.map_err(|e| format!("failed to read .dynsym: {e}"))?
99+
.ok_or_else(|| "no dynamic symbol table found".to_string())?;
100+
let sym_idx = symtab
101+
.iter()
102+
.enumerate()
103+
.find(|(_, sym)| {
104+
strtab
105+
.get(sym.st_name as usize)
106+
.map(|n| n == SYMBOL)
107+
.unwrap_or(false)
108+
})
109+
.map(|(i, _)| i as u32)
110+
.ok_or_else(|| format!("'{SYMBOL}' not found in .dynsym"))?;
111+
112+
let rela_shdr = elf
113+
.section_header_by_name(".rela.dyn")
114+
.map_err(|e| format!("failed to read section headers: {e}"))?
115+
.ok_or_else(|| ".rela.dyn section not found".to_string())?;
116+
let found = elf
117+
.section_data_as_relas(&rela_shdr)
118+
.map_err(|e| format!("failed to read .rela.dyn: {e}"))?
119+
.any(|r| r.r_type == R_TLSDESC && r.r_sym == sym_idx);
120+
if !found {
121+
return Err(format!("no TLSDESC relocation for '{SYMBOL}' in .rela.dyn"));
122+
}
123+
Ok(())
124+
}

libdd-otel-thread-ctx/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@
6464
//! `atomic_signal_fence`) to keep field writes boxed between the `valid = 0` and `valid = 1`
6565
//! stores during in-place updates.
6666
67+
#[cfg(all(target_os = "linux", feature = "autocheck"))]
68+
pub mod autocheck;
69+
6770
#[cfg(target_os = "linux")]
6871
pub mod linux {
6972
use std::{

0 commit comments

Comments
 (0)