Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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 <jlusby@yaah.dev>
dyn-clone,https://github.com/dtolnay/dyn-clone,MIT OR Apache-2.0,David Tolnay <dtolnay@gmail.com>
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 <chris.cole.09@gmail.com>
encoding_rs,https://github.com/hsivonen/encoding_rs,(Apache-2.0 OR MIT) AND BSD-3-Clause,Henri Sivonen <hsivonen@hsivonen.fi>
enum-as-inner,https://github.com/bluejekyll/enum-as-inner,MIT OR Apache-2.0,Benjamin Fry <benjaminfry@me.com>
equivalent,https://github.com/cuviper/equivalent,Apache-2.0 OR MIT,The equivalent Authors
Expand Down
6 changes: 5 additions & 1 deletion libdd-otel-thread-ctx-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ 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"]
sanity-check = ["dep:libdd-common-ffi", "libdd-otel-thread-ctx/sanity-check"]

[dev-dependencies]
libdd-otel-thread-ctx = { path = "../libdd-otel-thread-ctx", features = ["sanity-check"] }

[build-dependencies]
build_common = { path = "../build-common" }
4 changes: 4 additions & 0 deletions libdd-otel-thread-ctx-ffi/cbindgen.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
14 changes: 14 additions & 0 deletions libdd-otel-thread-ctx-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "sanity-check"))]
#[no_mangle]
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)),
}
}

#[cfg(target_os = "linux")]
mod linux {
use libdd_otel_thread_ctx::linux::{ThreadContext, ThreadContextHandle};
Expand Down
65 changes: 6 additions & 59 deletions libdd-otel-thread-ctx-ffi/tests/elf_properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,82 +3,29 @@

//! 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` 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` 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/`.

#![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/<name>
// 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_tlsdesc_reloc() {
fn otel_thread_ctx_v1_tls_properties() {
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"))
});
assert!(
found,
"No TLSDESC relocation found for '{SYMBOL}' in {}\n\
All relocations mentioning the symbol:\n{}",
path.display(),
output
.lines()
.filter(|l| l.contains(SYMBOL))
.map(|l| format!(" {l}"))
.collect::<Vec<_>>()
.join("\n")
);
libdd_otel_thread_ctx::sanity_check::check_tls_slot_in(&path).unwrap();
}
7 changes: 7 additions & 0 deletions libdd-otel-thread-ctx/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ publish = false
crate-type = ["lib"]
bench = false

[dependencies]
anyhow = { version = "1.0", optional = true }
elf = { version = "0.7", optional = true }

[features]
sanity-check = ["dep:elf", "dep:anyhow"]

[build-dependencies]
build_common = { path = "../build-common" }
cc = "1.1.31"
3 changes: 3 additions & 0 deletions libdd-otel-thread-ctx/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "sanity-check"))]
pub mod sanity_check;

#[cfg(target_os = "linux")]
pub mod linux {
use std::{
Expand Down
142 changes: 142 additions & 0 deletions libdd-otel-thread-ctx/src/sanity_check.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

//! 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 [`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` 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.

use anyhow::{bail, Context};
use elf::{abi, endian::AnyEndian, ElfBytes};
use std::path::{Path, PathBuf};

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) -> anyhow::Result<()> {
let data = std::fs::read(path).with_context(|| format!("failed to read {}", path.display()))?;
let elf = ElfBytes::<AnyEndian>::minimal_parse(&data)
.with_context(|| format!("failed to parse ELF at {}", path.display()))?;
check_dynsym(&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 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()?)
}

/// Locate the current running module (shared or not) via `/proc/self/maps`.
fn own_elf_path() -> anyhow::Result<PathBuf> {
// 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").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();
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));
}
}
}
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>) -> anyhow::Result<()> {
let (symtab, strtab) = elf
.dynamic_symbol_table()
.context("failed to read .dynsym")?
.context("no dynamic symbol table found")?;
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 {
bail!("'{SYMBOL}' not found as TLS GLOBAL in dynamic symbol table");
}
Ok(())
}

/// 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 TLSDESC_RELOC: u32 = 36; // R_X86_64_TLSDESC
#[cfg(target_arch = "aarch64")]
const TLSDESC_RELOC: u32 = 1031; // R_AARCH64_TLSDESC

let (symtab, strtab) = elf
.dynamic_symbol_table()
.context("failed to read .dynsym")?
.context("no dynamic symbol table found")?;
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)
.with_context(|| format!("'{SYMBOL}' not found in .dynsym"))?;

let rela_shdr = elf
.section_header_by_name(".rela.dyn")
.context("failed to read section headers")?;

if let Some(rela_shdr) = rela_shdr {
let bad: Vec<u32> = elf
.section_data_as_relas(&rela_shdr)
.context("failed to read .rela.dyn")?
.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<String> = bad.iter().map(|t| format!("type {t}")).collect();
bail!(
"'{SYMBOL}' has non-TLSDESC relocations in .rela.dyn: {}. \
Only TLSDESC or no relocation (Local Exec) is accepted.",
types.join(", ")
);
}
}

Ok(())
}
Loading