diff --git a/Cargo.lock b/Cargo.lock index 7b6e9fbe57..d83ffcbab3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3164,9 +3164,9 @@ name = "libdd-otel-thread-ctx" version = "1.0.0" dependencies = [ "anyhow", - "build_common", - "cc", "elf", + "object 0.36.5", + "sha2", ] [[package]] diff --git a/libdd-otel-thread-ctx-ffi/Cargo.toml b/libdd-otel-thread-ctx-ffi/Cargo.toml index 1c0f288628..234c325915 100644 --- a/libdd-otel-thread-ctx-ffi/Cargo.toml +++ b/libdd-otel-thread-ctx-ffi/Cargo.toml @@ -22,9 +22,22 @@ libdd-otel-thread-ctx = { path = "../libdd-otel-thread-ctx" } default = ["cbindgen"] cbindgen = ["build_common/cbindgen", "libdd-common-ffi/cbindgen"] sanity-check = ["dep:libdd-common-ffi", "libdd-otel-thread-ctx/sanity-check"] +# Gate for the gen_tls_shim_hash binary. Do not build it by default; it's very +# rarely needed (and maybe won't ever be): only if the corresponding test is +# failing one day, and we need to update the TLSDESC inline assembly to match +# what newer versions of clang generate. +gen-tls-shim-hash = ["libdd-otel-thread-ctx/test-utils"] + +# Dev tool to regenerate the TLSDESC golden hash for tests. +[[bin]] +name = "gen_tls_shim_hash" +required-features = ["gen-tls-shim-hash"] [dev-dependencies] -libdd-otel-thread-ctx = { path = "../libdd-otel-thread-ctx", features = ["sanity-check"] } +libdd-otel-thread-ctx = { path = "../libdd-otel-thread-ctx", features = [ + "sanity-check", + "test-utils", +] } [build-dependencies] build_common = { path = "../build-common" } diff --git a/libdd-otel-thread-ctx-ffi/README.md b/libdd-otel-thread-ctx-ffi/README.md index ae268d5770..9957554043 100644 --- a/libdd-otel-thread-ctx-ffi/README.md +++ b/libdd-otel-thread-ctx-ffi/README.md @@ -5,57 +5,3 @@ attaching, detaching, and updating per-thread OpenTelemetry context records that external readers (e.g. the eBPF profiler) can discover. Currently Linux-only (x86-64 and aarch64). - -## Optimized build (cross-language inlining) - -The OTel thread-level context sharing specification requires the use of the -TLSDESC dialect for the thread-local variable that holds the current context. -Because (stable) `rustc` doesn't currently provide a way to control the TLS -dialect, we need to use a small C shim that defines the variable and expose a -one-line getter. This unfortunately adds one level of indirection (a function -call) when attaching or detaching a context. - -With the right toolchain, it's possible to use Link-Time Optimization (LTO) to -inline the C wrapper at link time. The requirements are: - -- `clang` is available to compile the C shim to LLVM IR (version requirements - aren't clear -- tested with clang18 and clang20, but ideally the version - should be the same or close to the LLVM version shipped with `rustc`) -- Either the Rust toolchain ships `lld` or there's a system-wide `lld` install - (Rust has been shipping `rust-lld` for a long time now, something like since - 1.53+, however some musl-based distro like Alpine might have the Rust - toolchain without `rust-lld`) -- `lld` version is at least 18.1 (TLSDESC support) - -**If those requirements are met, setting the environment variables -`CARGO_TARGET__RUSTFLAGS=-Clinker-plugin-lto -Clinker=clang` and -`LIBDD_OTEL_THREAD_CTX_INLINE=1` when calling to `cargo` will trigger the -optimized build where the C shim is inlined.** Here, `` is the target -triple in screaming snake case. - -External environment variables are needed because cross-language LTO requires -two `rustc` codegen flags (`-Clinker-plugin-lto` and `-Clinker=clang`) that -cannot be set from a Cargo build script: they must come from `RUSTFLAGS` or -`.cargo/config.toml`, which can't be entirely automated from Rust only. We -advise to set those flags via the target-scoped -`CARGO_TARGET__RUSTFLAGS` env var so they don't leak to build scripts -or proc-macros if cross-compiling. - -### Build script - -The `build-optimized.sh` wrapper script is provided as a convenience and as an -example. - -#### Usage - -```bash -./build-optimized.sh -``` - -The script auto-detects the host triple. To cross-compile: - -```bash -./build-optimized.sh --target aarch64-unknown-linux-gnu -``` - -Extra arguments are forwarded to `cargo build`. diff --git a/libdd-otel-thread-ctx-ffi/build-optimized.sh b/libdd-otel-thread-ctx-ffi/build-optimized.sh deleted file mode 100755 index b52fd657a1..0000000000 --- a/libdd-otel-thread-ctx-ffi/build-optimized.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ -# SPDX-License-Identifier: Apache-2.0 -# -# Build libdd-otel-thread-ctx-ffi with cross-language LTO so the C TLS shim is -# inlined into the Rust FFI functions, eliminating a function-call indirection -# on every TLS access. -# -# Requirements: clang, lld (rust-lld from the toolchain is used automatically). -# The requirements are checked by the build.rs script. -# -# Usage: -# # auto-detect host triple -# ./build-optimized.sh -# # explicit target -# ./build-optimized.sh --target aarch64-unknown-linux-gnu -# -# Any extra arguments are forwarded to `cargo build`. -set -euo pipefail - -# Parse --target from args, or auto-detect the host triple. -TARGET="" -EXTRA_ARGS=() -while [[ $# -gt 0 ]]; do - case "$1" in - --target) - TARGET="$2"; shift 2 ;; - --target=*) - TARGET="${1#--target=}"; shift ;; - *) - EXTRA_ARGS+=("$1"); shift ;; - esac -done - -if [[ -z "$TARGET" ]]; then - TARGET=$(rustc -vV | sed -n 's/host: //p') -fi - -# CARGO_TARGET__RUSTFLAGS scopes the flags to the target only, keeping -# build scripts and proc-macros unaffected. -TARGET_ENV=$(echo "$TARGET" | tr 'a-z-' 'A-Z_') -FLAGS_VAR="CARGO_TARGET_${TARGET_ENV}_RUSTFLAGS" -EXISTING_FLAGS="${!FLAGS_VAR:-}" -export "$FLAGS_VAR=${EXISTING_FLAGS:+$EXISTING_FLAGS }-Clinker-plugin-lto -Clinker=clang" -export LIBDD_OTEL_THREAD_CTX_INLINE=1 - -cargo build --release \ - --target "$TARGET" \ - -p libdd-otel-thread-ctx-ffi \ - "${EXTRA_ARGS[@]}" - -# Sanity-check that the C shim was actually inlined, if `nm` is available. -if ! command -v nm &>/dev/null; then - echo >&2 "WARNING: skipping sanity check that the C TLS shim was inlined (\`nm\` not found)" -else - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - SO="$REPO_ROOT/target/$TARGET/release/liblibdd_otel_thread_ctx_ffi.so" - - if [[ -f "$SO" ]]; then - if ! NM_OUTPUT=$(nm "$SO" 2>&1); then - echo >&2 "WARNING: command \`nm\` failed on $SO. Skipping sanity check that the C TLS shim was inlined." - elif echo "$NM_OUTPUT" | grep -q 'libdd_get_otel_thread_ctx'; then - echo >&2 "ERROR: build succeeded but the C TLS shim (libdd_get_otel_thread_ctx_v1) was NOT inlined." - echo >&2 "Cross-language LTO may not be working. Check that clang and lld versions are recent enough and compatible with the Rust toolchain's LLVM." - exit 1 - fi - fi -fi diff --git a/libdd-otel-thread-ctx-ffi/build.rs b/libdd-otel-thread-ctx-ffi/build.rs index 61fa63764b..930272f09a 100644 --- a/libdd-otel-thread-ctx-ffi/build.rs +++ b/libdd-otel-thread-ctx-ffi/build.rs @@ -3,84 +3,20 @@ extern crate build_common; use build_common::{find_rust_lld_dir, generate_and_configure_header}; -use std::{env, fmt::Display, path::PathBuf, process::Command}; - -#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] -struct LldVersion { - major: u32, - minor: u32, -} - -impl Display for LldVersion { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}.{}", self.major, self.minor) - } -} - -/// Parse the major and minor version from `ld.lld --version` output. -/// -/// Typical formats: -/// "LLD 18.1.3 (compatible with GNU linkers)" -/// "LLD 19.1.0" -fn system_lld_version() -> Option { - let output = Command::new("ld.lld").arg("--version").output().ok()?; - if !output.status.success() { - return None; - } - String::from_utf8_lossy(&output.stdout) - .split_whitespace() - .find_map(|tok| { - let mut splitted = tok.split('.'); - let major = splitted.next()?.parse::().ok()?; - let minor = splitted.next()?.parse::().ok()?; - - Some(LldVersion { major, minor }) - }) -} - -/// TLSDESC is supported in LLD from version 18.1. -const MIN_LLD_VERSION_FOR_TLSDESC: LldVersion = LldVersion { - major: 18, - minor: 1, -}; - -/// Validate that a suitable LLD is available for cross-language LTO. -/// -/// Returns the rust-lld `gcc-ld/` directory if found; `None` means the system -/// `ld.lld` will be used instead. Panics with a clear message when the -/// requirements are not met. -fn resolve_lld_for_inline(target_arch: &str) -> Option { - if let Some(dir) = find_rust_lld_dir() { - return Some(dir); - } - - match system_lld_version() { - Some(v) if target_arch != "x86_64" || v >= MIN_LLD_VERSION_FOR_TLSDESC => None, - Some(v) => panic!( - "LIBDD_OTEL_THREAD_CTX_INLINE requires LLD >= {MIN_LLD_VERSION_FOR_TLSDESC} on \ - x86-64 (for -mllvm -enable-tlsdesc), but system ld.lld is version {v}. \ - Install a newer LLD or use a Rust toolchain that bundles rust-lld." - ), - None => panic!( - "LIBDD_OTEL_THREAD_CTX_INLINE requires LLD for cross-language LTO, but neither \ - rust-lld nor a system ld.lld was found." - ), - } -} +use std::env; fn main() { generate_and_configure_header("otel-thread-ctx.h"); + let cross_compiling = env::var("HOST").unwrap() != env::var("TARGET").unwrap(); + println!("cargo:rustc-env=LIBDD_OTEL_THREAD_CTX_FFI_CROSS_COMPILING={cross_compiling}"); + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); if target_os != "linux" { return; } - println!("cargo:rerun-if-env-changed=LIBDD_OTEL_THREAD_CTX_INLINE"); - - let inline_mode = env::var("LIBDD_OTEL_THREAD_CTX_INLINE").is_ok_and(|v| v == "1"); let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); - let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); // Export the TLSDESC thread-local variable to the dynamic symbol table so external readers // (e.g. the eBPF profiler) can discover it. Rust's cdylib linker applies a version script with @@ -93,32 +29,10 @@ fn main() { // Merging multiple version scripts is not supported by GNU ld, so we need lld. We prefer the // toolchain's bundled rust-lld (LLD 19+ since Rust 1.84) over the system lld (if it even // exists). If rust-lld is not found we fall back to whatever `lld` the system provides. - - // If `LIBDD_OTEL_THREAD_CTX_INLINE` is set to `1`, we try to inline the C shim. See the README - // for more details. - if inline_mode { - let rust_lld_dir = resolve_lld_for_inline(&target_arch); - - // Emit link args for ALL link types (not just cdylib) so that test binaries also link - // correctly when RUSTFLAGS sets clang as the linker (in practice we should only build/care - // about the shared object file in inline mode). - if let Some(dir) = rust_lld_dir { - println!("cargo:rustc-link-arg=-B{}", dir.display()); - } - println!("cargo:rustc-link-arg=-fuse-ld=lld"); - - // On x86-64, tell the LLVM backend to use TLSDESC during LTO codegen. - // On aarch64 TLSDESC is the default and the only model. - if target_arch == "x86_64" { - println!("cargo:rustc-link-arg=-Wl,-mllvm,-enable-tlsdesc"); - } - } else { - // Default mode: only the cdylib needs lld (for the version script). - if let Some(gcc_ld_dir) = find_rust_lld_dir() { - println!("cargo:rustc-cdylib-link-arg=-B{}", gcc_ld_dir.display()); - } - println!("cargo:rustc-cdylib-link-arg=-fuse-ld=lld"); + if let Some(gcc_ld_dir) = find_rust_lld_dir() { + println!("cargo:rustc-cdylib-link-arg=-B{}", gcc_ld_dir.display()); } + println!("cargo:rustc-cdylib-link-arg=-fuse-ld=lld"); println!( "cargo:rustc-cdylib-link-arg=-Wl,--version-script={manifest_dir}/tls-dynamic-list.txt" diff --git a/libdd-otel-thread-ctx-ffi/src/bin/gen_tls_shim_hash.rs b/libdd-otel-thread-ctx-ffi/src/bin/gen_tls_shim_hash.rs new file mode 100644 index 0000000000..29c8aaa44a --- /dev/null +++ b/libdd-otel-thread-ctx-ffi/src/bin/gen_tls_shim_hash.rs @@ -0,0 +1,129 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Generate the "golden" hash of the TLSDESC access sequence for `otel_thread_ctx_v1` from a +//! clang's output for a small C shim (inlined as [`TLS_SHIM_C`], written to a temp file and +//! compiled at runtime). +//! +//! This is the reference side of the `tlsdesc_inline_sequence` integration test: the test hashes +//! the sequence our inline assembly produces and asserts it equals the hash printed here. When the +//! inline assembly (or the pinned toolchain) legitimately changes, re-run this to obtain the new +//! hash and paste it into the test. +//! +//! This is a dev tool, not part of a normal build: it lives behind the `gen-tls-shim-hash` feature +//! (via the `[[bin]]` `required-features`), so it is only compiled when that feature is enabled. +//! +//! # Reference compilers +//! +//! - **aarch64** uses `clang` (cross-compiling with `--target=`): TLSDESC is the default dialect +//! for a `global-dynamic` access, so no extra flag is needed. +//! - **x86-64** uses `gcc` with `-mtls-dialect=gnu2`. The pinned CI image below ships Clang 18, +//! which predates x86-64 TLSDESC support (`-mtls-dialect=gnu2` is Clang 19+), so we fall back to +//! gcc there, which is just a work-around. The clang and gcc sequences are currently byte-by-byte +//! identical for x86_64 anyway. Feel free to update to using clang once Clang19+ lands in CI +//! images. +//! +//! The compilers can be overridden with `$CC` (x86-64) and `$CLANG` (aarch64). +//! +//! # Reproducibility +//! +//! For a *reproducible* hash, run this inside the latest [Ubuntu Datadog CI build +//! image](https://github.com/DataDog/libddprof-build/blob/main/ubuntu.Dockerfile). The current hash +//! was built from rev `758a114281f3545b40598272ea1c41404b43f2f6`. +//! +//! ```text +//! docker run --rm -v "$PWD":/repo:ro -e CARGO_TARGET_DIR=/tmp/target -e CARGO_HOME=/tmp/cargo \ +//! -w /repo registry.ddbuild.io/ci/libddprof-build:dependencies_ubuntu_30 \ +//! bash -c 'for a in x86_64 aarch64; do \ +//! cargo run --quiet --no-default-features --features gen-tls-shim-hash \ +//! --bin gen_tls_shim_hash -p libdd-otel-thread-ctx-ffi -- "$a"; done' +//! ``` + +use std::{path::Path, process::Command}; + +use libdd_otel_thread_ctx::test_utils::tls_shim_window::{windows_in_object_file, Arch}; + +/// The reference C translation unit: a public `otel_thread_ctx_v1` TLS symbol compiled as a shared +/// object plus an `accessor, so the compiler emits exactly one TLSDESC access sequence. It is +/// written to a temp file and compiled at runtime. +const TLS_SHIM_C: &str = r#"extern __thread void *otel_thread_ctx_v1 __attribute__((tls_model("global-dynamic"))); + +__attribute__((noinline)) void **tls_slot_from_c(void) { + return &otel_thread_ctx_v1; +} +"#; + +/// Compile `tls_shim.c` for `arch` with the flags that produce a `global-dynamic` TLSDESC access, +/// matching the access model our inline assembly implements. See the module docs for why x86-64 +/// uses gcc and aarch64 uses Clang. +fn compile_reference(arch: Arch, source: &Path, object: &Path) { + let mut cmd = match arch { + Arch::X86_64 => { + // gcc: the psABI-canonical x86-64 TLSDESC sequence. `gnu2` selects the TLSDESC dialect. + let cc = std::env::var("CC").unwrap_or_else(|_| "gcc".to_owned()); + let mut cmd = Command::new(cc); + cmd.arg("-mtls-dialect=gnu2"); + cmd + } + Arch::Aarch64 => { + // clang, cross-compiling: TLSDESC is the default dialect for a `global-dynamic` access. + let clang = std::env::var("CLANG").unwrap_or_else(|_| "clang".to_owned()); + let mut cmd = Command::new(clang); + cmd.arg(format!("--target={}", arch.clang_target_triple())); + cmd + } + }; + cmd.args(["-O2", "-fPIC", "-fomit-frame-pointer", "-c"]); + cmd.arg(source).arg("-o").arg(object); + + eprintln!("running: {cmd:?}"); + let status = cmd + .status() + .unwrap_or_else(|e| panic!("failed to run reference compiler: {e}")); + assert!( + status.success(), + "reference compiler failed with status {status}" + ); +} + +fn main() { + let arch = match std::env::args().nth(1) { + Some(arg) => arg.parse().unwrap_or_else(|e| { + eprintln!("error: {e}"); + std::process::exit(2); + }), + None => Arch::host(), + }; + + let out_dir = std::env::temp_dir().join(format!("gen_tls_shim_hash-{}", std::process::id())); + std::fs::create_dir_all(&out_dir) + .unwrap_or_else(|e| panic!("failed to create {}: {e}", out_dir.display())); + let source = out_dir.join("tls_shim.c"); + std::fs::write(&source, TLS_SHIM_C) + .unwrap_or_else(|e| panic!("failed to write {}: {e}", source.display())); + let object = out_dir.join(format!("tls_shim_{arch:?}.o")); + + compile_reference(arch, &source, &object); + + let windows = windows_in_object_file(&object); + assert_eq!( + windows.len(), + 1, + "expected exactly one TLSDESC access in the reference object {}; found {}", + object.display(), + windows.len() + ); + + let window = &windows[0]; + assert_eq!( + window.arch, arch, + "reference object architecture ({:?}) does not match requested architecture ({arch:?})", + window.arch + ); + + let _ = std::fs::remove_dir_all(&out_dir); + + println!("arch: {arch:?}"); + println!("bytes: {}", window.hex_dump()); + println!("hash: {}", window.hash_hex()); +} diff --git a/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs b/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs index 7d0e51a446..d96c75e5ce 100644 --- a/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs +++ b/libdd-otel-thread-ctx-ffi/tests/elf_properties.rs @@ -6,29 +6,27 @@ //! test rather checks that the dynamic library is properly linked, which is why it lives within the //! FFI. //! -//! 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` follows the TLSDESC access model (if there's a relocation, it's a TLSDESC -//! one). +//! This checks that, in the linked `cdylib`: +//! - `otel_thread_ctx_v1` is exported in the dynamic symbol table as a TLS GLOBAL symbol; +//! - it follows the TLSDESC access model: if there is a relocation for it, it is a TLSDESC +//! relocation. //! -//! 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")] +//! The complementary check that our inline assembly emits the exact TLSDESC instruction sequence a +//! compiler would (so the linker relaxes it correctly) lives in `tlsdesc_inline_sequence.rs`. +//! +//! Library artifact paths are derived at runtime from the test executable location. -use std::path::PathBuf; +#![cfg(all( + target_os = "linux", + any(target_arch = "x86_64", target_arch = "aarch64") +))] -fn cdylib_path() -> PathBuf { - 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") -} +use libdd_otel_thread_ctx::test_utils::artifacts::{artifact_path, check_readable}; #[test] #[cfg_attr(miri, ignore)] fn otel_thread_ctx_v1_tls_properties() { - let path = cdylib_path(); + let path = artifact_path("liblibdd_otel_thread_ctx_ffi.so"); + check_readable(&path); libdd_otel_thread_ctx::sanity_check::check_tls_slot_in(&path).unwrap(); } diff --git a/libdd-otel-thread-ctx-ffi/tests/tlsdesc_inline_sequence.rs b/libdd-otel-thread-ctx-ffi/tests/tlsdesc_inline_sequence.rs new file mode 100644 index 0000000000..f3bd1a55f9 --- /dev/null +++ b/libdd-otel-thread-ctx-ffi/tests/tlsdesc_inline_sequence.rs @@ -0,0 +1,94 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Verify that the TLSDESC access to `otel_thread_ctx_v1` emitted by our inline assembly +//! ([`libdd_otel_thread_ctx::linux::tls_slot`]) is byte-for-byte identical to what a reference C +//! compiler emits for a `global-dynamic` TLSDESC access. +//! +//! Rather than compiling a C reference at test time, which requires to control precisely the +//! toolchain for reproducibility, and doing a instruction per instruction comparison, we compare a +//! SHA-256 hash of our sequence against a +//! hardcoded "golden" hash generated once instead. +//! +//! The golden hash is generated once outside of tests from a pinned toolchain and a C shim (see +//! [`EXPECTED_TLSDESC_HASH_X86_64`] / [`EXPECTED_TLSDESC_HASH_AARCH64`] via the `gen_tls_shim_hash` +//! binary). This ensures we have the exact same assembly than the one generated by the pinned +//! toolchain. We can update this hash if in the future the sequence generated by clang/gcc change +//! and we want to match the newer versions. +//! +//! We inspect the `staticlib` artifact (unlinked, so the relocations are still present) rather than +//! the relaxed `cdylib`. Library artifact paths are derived at runtime from the test executable +//! location. + +#![cfg(all( + target_os = "linux", + any(target_arch = "x86_64", target_arch = "aarch64") +))] + +use libdd_otel_thread_ctx::test_utils::artifacts::{artifact_path, check_readable}; +use libdd_otel_thread_ctx::test_utils::tls_shim_window::{windows_in_archive, Arch, SYMBOL}; + +/// Golden SHA-256 of the full x86-64 TLSDESC sequence (`lea …@tlsdesc` / `call …@tlscall` / +/// `add %fs:0x0, %rax`), generated by the `gen_tls_shim_hash` binary. See [`regenerate_hint`]. +const EXPECTED_TLSDESC_HASH_X86_64: &str = + "5b38b6b2a151997785463a5d9d2b1f1cb9027660ea37f2aafe94867508cedc66"; + +/// Golden SHA-256 of the full aarch64 TLSDESC sequence (`adrp`/`ldr`/`add`/`blr` followed by the +/// thread-pointer computation `mrs x8, tpidr_el0` / `add x0, x8, x0`), generated by the +/// `gen_tls_shim_hash` binary. See [`regenerate_hint`]. +const EXPECTED_TLSDESC_HASH_AARCH64: &str = + "f8adc6415409a726b51332a2c36931de9d3537dc33705c4a4aa160268e40ca97"; + +fn expected_hash(arch: Arch) -> &'static str { + match arch { + Arch::X86_64 => EXPECTED_TLSDESC_HASH_X86_64, + Arch::Aarch64 => EXPECTED_TLSDESC_HASH_AARCH64, + } +} + +/// Explains, for a failure message, how the golden hash is produced and how to refresh it. +fn regenerate_hint() -> String { + "The golden hash is the SHA-256 of the TLSDESC access sequence a reference C compiler emits for \ + a small TLS shim generated by the `gen_tls_shim_hash` binary. \ + If you changed the inline assembly in `libdd_otel_thread_ctx::linux::tls_slot` (or the pinned \ + toolchain) on purpose, see `gen_tls_shim_hash.rs` for instructions to re-run the \ + generator and update `EXPECTED_TLSDESC_HASH_*` in this file. Otherwise, the sequence our \ + assembly produces has drifted from what some compiler emits (and from what the linker expects \ + for TLS relaxation): run the generator and diff its `bytes:` line against the `actual bytes` \ + above to see which instruction differs." + .to_owned() +} + +#[test] +#[cfg_attr(miri, ignore)] +fn tlsdesc_inline_assembly_matches_reference_hash() { + let staticlib = artifact_path("liblibdd_otel_thread_ctx_ffi.a"); + check_readable(&staticlib); + + let windows = windows_in_archive(&staticlib); + assert!( + !windows.is_empty(), + "expected at least one Rust inline-asm TLSDESC access for {SYMBOL} in {}", + staticlib.display() + ); + + for window in windows { + let expected = expected_hash(window.arch); + let actual = window.hash_hex(); + assert_eq!( + actual, + expected, + "\nTLSDESC sequence hash mismatch for {SYMBOL} in {}\n \ + arch: {:?}\n \ + actual bytes: {}\n \ + actual hash: {}\n \ + expected hash: {}\n\n{}", + window.label, + window.arch, + window.hex_dump(), + actual, + expected, + regenerate_hint(), + ); + } +} diff --git a/libdd-otel-thread-ctx/Cargo.toml b/libdd-otel-thread-ctx/Cargo.toml index 0580970a96..e9b58b7e28 100644 --- a/libdd-otel-thread-ctx/Cargo.toml +++ b/libdd-otel-thread-ctx/Cargo.toml @@ -19,10 +19,10 @@ bench = false [dependencies] anyhow = { version = "1.0", optional = true } elf = { version = "0.7", optional = true } +object = { version = "0.36", default-features = false, features = ["archive", "read_core"], optional = true } +sha2 = { version = "0.10", optional = true } [features] sanity-check = ["dep:elf", "dep:anyhow"] - -[build-dependencies] -build_common = { path = "../build-common" } -cc = "1.1.31" +# Test helpers shared with `libdd-otel-thread-ctx-ffi`'s integration tests and examples. +test-utils = ["dep:elf", "dep:object", "dep:sha2"] diff --git a/libdd-otel-thread-ctx/README.md b/libdd-otel-thread-ctx/README.md index c55675f7d2..731edb9676 100644 --- a/libdd-otel-thread-ctx/README.md +++ b/libdd-otel-thread-ctx/README.md @@ -15,10 +15,10 @@ Linux only for now. ## TLS -The C shim (`src/tls_shim.c`) is required because `rustc` does not yet support -the TLSDESC TLS dialect required by the spec to export `otel_thread_ctx_v1`. -Since the reader and the writer must agree on the TLS dialect/model, we rely on -the C compiler to emit the right access pattern. +The TLS symbol `otel_thread_ctx_v1` and its TLSDESC accessor are defined +directly in Rust using `global_asm!` and `asm!` (both stable since Rust 1.65 / +1.59). This avoids a C build dependency while guaranteeing the TLSDESC dialect +on both x86-64 and aarch64 as required by the spec. ## Usage diff --git a/libdd-otel-thread-ctx/build.rs b/libdd-otel-thread-ctx/build.rs deleted file mode 100644 index adfda34153..0000000000 --- a/libdd-otel-thread-ctx/build.rs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ -// SPDX-License-Identifier: Apache-2.0 -extern crate build_common; - -use std::env; -use std::process::Command; - -fn clang_is_available() -> bool { - Command::new("clang") - .arg("--version") - .output() - .map(|o| o.status.success()) - .unwrap_or(false) -} - -fn main() { - let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); - let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); - - if target_os != "linux" { - return; - } - - if !matches!(target_arch.as_str(), "x86_64" | "aarch64") { - panic!( - "Unsupported architecture `{}` for otel-thread-ctx on Linux. Only x86_64 and aarch64 are currently supported.", - target_arch - ) - } - - println!("cargo:rerun-if-env-changed=LIBDD_OTEL_THREAD_CTX_INLINE"); - println!("cargo:rerun-if-changed=src/tls_shim.c"); - - // The otel-thread-ctx FFI crate has a special flag to inline the C shim inside the final - // library. This setup has additional requirements for the build of this crate, which are - // enforced below when the flag is set. - let inline_mode = env::var_os("LIBDD_OTEL_THREAD_CTX_INLINE").is_some_and(|v| v == "1"); - - let mut build = cc::Build::new(); - - if inline_mode { - assert!( - clang_is_available(), - "LIBDD_OTEL_THREAD_CTX_INLINE is set but `clang` was not found. \ - Cross-language LTO requires clang as the C compiler." - ); - build.compiler("clang"); - build.flag("-flto=thin"); - - // Any binary linking this crate in inline mode (including test - // binaries) needs lld, because -Clinker-plugin-lto passes LTO plugin - // options that only lld understands. - if let Some(dir) = build_common::find_rust_lld_dir() { - println!("cargo:rustc-link-arg=-B{}", dir.display()); - } - println!("cargo:rustc-link-arg=-fuse-ld=lld"); - - // Note: in the inline setup, TLS dialect selection is handled by the linker and is taken - // care of by the build script of otel-thread-ctx-ffi - } else if target_arch == "x86_64" { - // - On aarch64, TLSDESC is already the only dynamic TLS model so no flag is needed. - // - On x86-64, we use `-mtls-dialect=gnu2` (supported since GCC 4.4 and Clang 19+) to force - // the use of TLSDESC as mandated by the spec. If it's not supported, this build will - // fail. - build.flag("-mtls-dialect=gnu2"); - } - - build.file("src/tls_shim.c").compile("tls_shim"); -} diff --git a/libdd-otel-thread-ctx/src/lib.rs b/libdd-otel-thread-ctx/src/lib.rs index 10142a4c12..868e3a17a2 100644 --- a/libdd-otel-thread-ctx/src/lib.rs +++ b/libdd-otel-thread-ctx/src/lib.rs @@ -5,8 +5,8 @@ //! //! This crate implements the publisher side of the Thread Context OTEP (PR #4947). //! -//! Since `rustc` doesn't currently support the TLSDESC dialect, we use a C shim to set and get -//! the thread-local storage used for the context. +//! Since `rustc` doesn't currently support the TLSDESC dialect, we define the thread-local +//! storage symbol and its accessor using inline assembly (`global_asm!` / `asm!`). //! //! ## Usage //! @@ -17,15 +17,16 @@ //! The simplest pattern, when applicable, is to attach one record and then mutate it in place. //! This avoids allocation in the hot path. //! -//! ```ignore +//! ```rust //! use libdd_otel_thread_ctx::linux::ThreadContext; //! //! let trace_id = [0u8; 16]; //! let span_id = [1u8; 8]; +//! let local_root_span_id = [2u8; 8]; //! //! // First call allocates a record and attaches it. -//! ThreadContext::new(trace_id, span_id, &[(0, "first")]).attach(); -//! ThreadContext::update(trace_id, span_id, &[(0, "second")]); +//! ThreadContext::new(trace_id, span_id, local_root_span_id, &[(0, "first")]).attach(); +//! ThreadContext::update(trace_id, span_id, local_root_span_id, &[(0, "second")]); //! ThreadContext::detach(); //! ``` //! @@ -35,15 +36,16 @@ //! to be saved and restored repeatedly. Could be the case with async-runtimes where several tasks //! might run on the same thread, or even move from one thread to another, for example. //! -//! ```ignore +//! ```rust //! use libdd_otel_thread_ctx::linux::ThreadContext; //! //! let trace_id = [0u8; 16]; //! let span_id = [1u8; 8]; +//! let local_root_span_id = [2u8; 8]; //! let attrs: &[(u8, &str)] = &[(0, "GET"), (1, "/api/v1")]; //! //! // Publish a new context and save the previously attached one (if any). -//! let ctx = ThreadContext::new(trace_id, span_id, attrs); +//! let ctx = ThreadContext::new(trace_id, span_id, local_root_span_id, attrs); //! let previous = ctx.attach(); //! //! // ... do work inside the span ... @@ -64,23 +66,113 @@ //! `atomic_signal_fence`) to keep field writes boxed between the `valid = 0` and `valid = 1` //! stores during in-place updates. +// The `linux` module below resolves the TLS slot with TLSDESC inline assembly that is only written +// for x86_64 and aarch64. Reject any other architecture on Linux at compile time. On non-Linux +// targets the `linux` module is not compiled, so there's no such constraint. +#[cfg(all( + target_os = "linux", + not(any(target_arch = "x86_64", target_arch = "aarch64")) +))] +compile_error!( + "Unsupported architecture for otel-thread-ctx on Linux. Only x86_64 and aarch64 are currently \ + supported." +); + #[cfg(all(target_os = "linux", feature = "sanity-check"))] pub mod sanity_check; -#[cfg(target_os = "linux")] +#[cfg(feature = "test-utils")] +pub mod test_utils; + +#[cfg(all( + target_os = "linux", + any(target_arch = "x86_64", target_arch = "aarch64") +))] pub mod linux { use std::{ - ffi::c_void, mem, ptr::{self, NonNull}, sync::atomic::{compiler_fence, AtomicPtr, AtomicU8, Ordering}, }; + // Define the thread-local pointer that external readers (e.g. the eBPF profiler) discover via + // the dynamic symbol table. It must be an exported ELF `STT_TLS` object accessed via the + // TLSDESC dialect, as mandated by the OTel thread-level context sharing spec. + // + // Stable `rustc` cannot select the TLS dialect for a `#[thread_local]` static, so we declare + // the symbol directly in assembly (an 8-byte, zero-initialised slot in `.tbss`) and resolve + // its per-thread address through TLSDESC in [`tls_slot`]. + core::arch::global_asm!( + ".section .tbss,\"awT\",@nobits", + ".globl otel_thread_ctx_v1", + ".type otel_thread_ctx_v1, @tls_object", + ".size otel_thread_ctx_v1, 8", + ".balign 8", + "otel_thread_ctx_v1:", + ".zero 8", + ".previous", + ); + + /// Return the address of the current thread's `otel_thread_ctx_v1` TLS slot, resolved through + /// the TLSDESC dialect. + #[cfg(target_arch = "x86_64")] + #[inline(always)] + unsafe fn tls_slot() -> *mut *mut ThreadContextRecord { + let ptr: usize; + // + // WARNING: keep the assembly below in the canonical compiler-emitted TLSDESC form. Linkers + // rely on these exact relocation-bearing instruction patterns for TLS relaxation, + // especially when this crate is linked statically. Harmless-looking rewrites can hide part + // of the sequence from the linker and produce a partially relaxed access that computes an + // invalid TLS address. + core::arch::asm!( + "leaq otel_thread_ctx_v1@tlsdesc(%rip), %rax", + "call *otel_thread_ctx_v1@TLSCALL(%rax)", + "addq %fs:0, %rax", + // There is a call instruction, but the whole point of TLSDESC is to use a fast calling + // convention. GCC's x86-64 port assumes that FLAGS_REG and RAX are changed while all + // other registers are preserved[^1]. LLVM similarly only clobbers RAX[^2] (and flags). + // So we don't need to clobber additional registers or to use `clobber_abi` here (which + // would negate most of the advantage of TLSDESC). + // + // [^1]: https://maskray.me/blog/2021-02-14-all-about-thread-local-storage + // [^2]: https://raw.githubusercontent.com/llvm/llvm-project/main/llvm/lib/Target/X86/X86InstrCompiler.td + out("rax") ptr, + options(att_syntax), + ); + ptr as *mut *mut ThreadContextRecord + } + + /// Return the address of the current thread's `otel_thread_ctx_v1` TLS slot, resolved through + /// the TLSDESC dialect. + #[cfg(target_arch = "aarch64")] + #[inline(always)] + unsafe fn tls_slot() -> *mut *mut ThreadContextRecord { + let ptr: usize; + // WARNING: do not change the assembly below. See the warning above for amd64, and + // https://github.com/ARM-software/abi-aa/blob/main/sysvabi64/sysvabi64.rst#general-dynamic. + // This code match byte-per-byte what clang generates, and this is verified during tests. + core::arch::asm!( + "adrp x0, :tlsdesc:otel_thread_ctx_v1", + "ldr x1, [x0, :tlsdesc_lo12:otel_thread_ctx_v1]", + "add x0, x0, :tlsdesc_lo12:otel_thread_ctx_v1", + ".tlsdesccall otel_thread_ctx_v1", + "blr x1", + "mrs x8, tpidr_el0", + "add x0, x8, x0", + out("x0") ptr, + out("x1") _, + out("x8") _, + out("x30") _, + ); + ptr as *mut *mut ThreadContextRecord + } + /// Run `f` with an atomic view of the current thread's TLS slot. /// - /// The address calculation requires a call to a C shim in order to use the TLSDESC dialect - /// from Rust. The returned address is stable (per thread), so callers should try to do as - /// much work as possible inside a single call to reduce the number of C-shim round-trips. + /// The address calculation goes through the TLSDESC dialect via [`tls_slot`]. The returned + /// address is stable (per thread), so callers should try to do as much work as possible + /// inside a single call. /// /// The slot is read by an async signal handler. Atomic operations should in general use /// [Ordering::Relaxed], but modifications to the record might need additional compiler-only @@ -89,11 +181,6 @@ pub mod linux { where F: FnOnce(&AtomicPtr) -> R, { - extern "C" { - /// Return the address of the current thread's `otel_thread_ctx_v1` local. - fn libdd_get_otel_thread_ctx_v1() -> *mut *mut c_void; - } - const { assert!( mem::align_of::>() @@ -102,11 +189,9 @@ pub mod linux { } // Safety: the const assertion above ensures the alignment is correct. The TLS slot is - // valid for the lifetime of the current thread. The `extern "C"` declaration is scoped - // to this function, guaranteeing that all accesses go through the `AtomicPtr` wrapper. - let slot = unsafe { - AtomicPtr::from_ptr(libdd_get_otel_thread_ctx_v1().cast::<*mut ThreadContextRecord>()) - }; + // valid for the lifetime of the current thread, and all accesses go through the + // `AtomicPtr` wrapper. + let slot = unsafe { AtomicPtr::from_ptr(tls_slot()) }; f(slot) } @@ -462,7 +547,7 @@ pub mod linux { } #[cfg(test)] - // The tests are set to be ignored by Miri, since accessing the TLS through C isn't supported. + // The tests are set to be ignored by Miri, since the inline-asm TLSDESC access isn't supported. mod tests { use super::{ThreadContext, ThreadContextRecord}; use std::sync::atomic::Ordering; @@ -672,7 +757,7 @@ pub mod linux { let _ = ThreadContext::detach(); } - // Make sure the C shim is indeed providing a thread-local address. + // Make sure the TLSDESC accessor is indeed providing a thread-local address. #[test] #[cfg_attr(miri, ignore)] fn tls_slots_are_per_thread() { diff --git a/libdd-otel-thread-ctx/src/test_utils/artifacts.rs b/libdd-otel-thread-ctx/src/test_utils/artifacts.rs new file mode 100644 index 0000000000..c2521b3223 --- /dev/null +++ b/libdd-otel-thread-ctx/src/test_utils/artifacts.rs @@ -0,0 +1,32 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Helpers to locate a crate's build artifacts from within an integration test binary. +//! +//! A test binary and the crate artifacts it exercises (`cdylib`, `staticlib`) live side by side in +//! `target/<[triple/]profile>/deps/`, so the paths are derived from the running test executable. + +use std::path::{Path, PathBuf}; + +/// Directory that holds the running test binary and the crate artifacts. +pub fn deps_dir() -> PathBuf { + // test binary: target/<[triple/]profile>/deps/ + let exe = std::env::current_exe().expect("failed to read current executable path"); + exe.parent() + .expect("unexpected test executable path structure") + .to_owned() +} + +/// Path to the artifact named `name` sitting next to the running test binary. +pub fn artifact_path(name: &str) -> PathBuf { + deps_dir().join(name) +} + +/// Assert that `path` can be opened for reading. +pub fn check_readable(path: &Path) { + assert!( + std::fs::File::open(path).is_ok(), + "{} could not be opened for reading", + path.display() + ); +} diff --git a/libdd-otel-thread-ctx/src/test_utils/mod.rs b/libdd-otel-thread-ctx/src/test_utils/mod.rs new file mode 100644 index 0000000000..1ff7af72be --- /dev/null +++ b/libdd-otel-thread-ctx/src/test_utils/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Test utilities shared between this crate's tests, the `libdd-otel-thread-ctx-ffi` integration +//! tests and the `gen_tls_shim_hash` dev tool. + +pub mod artifacts; +pub mod tls_shim_window; diff --git a/libdd-otel-thread-ctx/src/test_utils/tls_shim_window.rs b/libdd-otel-thread-ctx/src/test_utils/tls_shim_window.rs new file mode 100644 index 0000000000..a06f92e248 --- /dev/null +++ b/libdd-otel-thread-ctx/src/test_utils/tls_shim_window.rs @@ -0,0 +1,358 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Shared logic for locating and hashing the TLSDESC access sequence of `otel_thread_ctx_v1` in an +//! ELF object. +//! +//! This module has two consumers in `libdd-otel-thread-ctx-ffi`: +//! - the `tlsdesc_inline_sequence` integration test, which extracts the sequence from our own +//! statically linked artifact (produced by the inline assembly in `libdd_otel_thread_ctx`) and +//! hashes it +//! - the `gen_tls_shim_hash` binary, which extracts the sequence from a Clang-compiled reference +//! object and hashes it to produce the "golden" hash +//! +//! Both consumers must agree byte-for-byte, so the extraction lives here once and is shared. +//! +//! The target architecture is read from the ELF header at runtime, so the generator can process a +//! cross-compiled reference object for either architecture from any host. +//! +//! The "window" is the full TLSDESC access sequence, in program order: +//! - **x86-64**: `lea …@tlsdesc(%rip), %rax` / `call *…@tlscall(%rax)` / `add %fs:0x0, %rax`. +//! - **aarch64**: `adrp`/`ldr`/`add`/`blr` (the relocation-bearing core) followed by the +//! thread-pointer computation `mrs x8, tpidr_el0` / `add x0, x8, x0`. +//! +//! The window is located from the object's TLSDESC relocations and then sliced out with a fixed, +//! architecture-specific offset formula that matches how both Clang and our inline assembly lay the +//! sequence out (contiguously, thread-pointer read immediately after the call). The +//! relocation-bearing immediates are zero in the object file (the linker fills them in), so the +//! sliced bytes are stable. +//! +//! This window location and size might have to be patched if future versions of clang generate a +//! different access sequence in the future. + +use std::{path::Path, str::FromStr}; + +use elf::{abi, endian::AnyEndian, symbol::SymbolTable, ElfBytes}; +use object::read::archive::ArchiveFile; +use sha2::{Digest, Sha256}; + +/// The exported TLS symbol whose access sequence we hash. +pub const SYMBOL: &str = "otel_thread_ctx_v1"; + +/// The architectures for which we know how to locate and slice a TLSDESC sequence. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Arch { + X86_64, + Aarch64, +} + +impl FromStr for Arch { + type Err = String; + + fn from_str(name: &str) -> Result { + match name { + "x86_64" | "amd64" | "x86-64" => Ok(Arch::X86_64), + "aarch64" | "arm64" => Ok(Arch::Aarch64), + other => Err(format!( + "unknown architecture `{other}` (expected x86_64 or aarch64)" + )), + } + } +} + +impl Arch { + /// The architecture of the host this code was compiled for. + pub fn host() -> Arch { + if cfg!(target_arch = "aarch64") { + Arch::Aarch64 + } else { + Arch::X86_64 + } + } + + /// The `--target=` triple to hand a cross-compiling Clang for this architecture. + pub fn clang_target_triple(self) -> &'static str { + match self { + Arch::X86_64 => "x86_64-unknown-linux-gnu", + Arch::Aarch64 => "aarch64-unknown-linux-gnu", + } + } + + fn from_e_machine(e_machine: u16, label: &str) -> Arch { + match e_machine { + abi::EM_X86_64 => Arch::X86_64, + abi::EM_AARCH64 => Arch::Aarch64, + other => panic!("{label} has unsupported ELF machine type {other}"), + } + } + + /// Number of object-file TLSDESC relocations a single access emits. + fn relocations_per_access(self) -> usize { + match self { + // `lea` (GOTPC32_TLSDESC) + `call` (TLSDESC_CALL). + Arch::X86_64 => 2, + // `adrp` (ADR_PAGE21) + `ldr` (LD64_LO12) + `add` (ADD_LO12) + `blr` (CALL). + Arch::Aarch64 => 4, + } + } + + fn is_tlsdesc_object_relocation(self, relocation_type: u32) -> bool { + // `R_*_TLSDESC` (x86-64) / `R_AARCH64_TLSDESC` are the dynamic-linker relocations emitted + // after linking, so they are intentionally excluded here. + match self { + Arch::X86_64 => matches!( + relocation_type, + abi::R_X86_64_GOTPC32_TLSDESC | abi::R_X86_64_TLSDESC_CALL + ), + Arch::Aarch64 => matches!( + relocation_type, + abi::R_AARCH64_TLSDESC_LD_PREL19 + | abi::R_AARCH64_TLSDESC_ADR_PREL21 + | abi::R_AARCH64_TLSDESC_ADR_PAGE21 + | abi::R_AARCH64_TLSDESC_LD64_LO12 + | abi::R_AARCH64_TLSDESC_ADD_LO12 + | abi::R_AARCH64_TLSDESC_OFF_G1 + | abi::R_AARCH64_TLSDESC_OFF_G0_NC + | abi::R_AARCH64_TLSDESC_LDR + | abi::R_AARCH64_TLSDESC_ADD + | abi::R_AARCH64_TLSDESC_CALL + ), + } + } + + /// Compute the `[start, end)` byte bounds of the full TLSDESC sequence within its section, from + /// the (offset-sorted) relocation offsets of a single access. + fn sequence_bounds(self, offsets: &[u64], section_len: usize) -> (usize, usize) { + let (start, end) = match self { + // x86-64: the first relocation sits on the `lea` displacement (3 bytes into the + // instruction) and the second on the `call`. The trailing `add %fs:0x0, %rax` ends 11 + // bytes past the `call` relocation. + Arch::X86_64 => { + let first = to_usize(offsets[0], "first relocation offset"); + let call = to_usize(offsets[1], "call relocation offset"); + let start = first + .checked_sub(3) + .expect("x86-64 relocation offset is before the LEA displacement"); + (start, call + 11) + } + // aarch64: the four relocations sit on `adrp`/`ldr`/`add`/`blr`. The window starts at + // the `adrp` and extends 12 bytes past the last relocation: `blr` (4) followed by the + // two non-relocated instructions `mrs x8, tpidr_el0` (4) and `add x0, x8, x0` (4). + Arch::Aarch64 => { + let start = to_usize(offsets[0], "first relocation offset"); + let last = to_usize(offsets[offsets.len() - 1], "last relocation offset"); + (start, last + 12) + } + }; + assert!( + end <= section_len, + "{self:?} TLSDESC sequence extends beyond section data" + ); + (start, end) + } +} + +fn to_usize(value: u64, what: &str) -> usize { + usize::try_from(value).unwrap_or_else(|_| panic!("{what} does not fit in usize")) +} + +/// A single TLSDESC access sequence extracted from an object, ready to be hashed. +pub struct TlsDescWindow { + /// Human-readable origin (file, archive member, …), used in messages. + pub label: String, + /// The detected architecture of the object the sequence came from. + pub arch: Arch, + /// The full sequence bytes, sliced out with the architecture-specific bounds. + pub bytes: Vec, +} + +impl TlsDescWindow { + /// Lowercase hex SHA-256 of the sequence bytes. + pub fn hash_hex(&self) -> String { + let digest = Sha256::digest(&self.bytes); + let mut out = String::with_capacity(digest.len() * 2); + for byte in digest { + out.push_str(&format!("{byte:02x}")); + } + out + } + + /// Render the sequence bytes as hex, one 4-byte little-endian word per group on aarch64 + /// (matching the fixed-width instruction encoding) or a single hex string on x86-64. Used in + /// failure messages so a human can eyeball which bytes differ. + pub fn hex_dump(&self) -> String { + match self.arch { + Arch::Aarch64 => self + .bytes + .chunks(4) + .map(|word| word.iter().map(|b| format!("{b:02x}")).collect::()) + .collect::>() + .join(" "), + Arch::X86_64 => self.bytes.iter().map(|b| format!("{b:02x}")).collect(), + } + } +} + +fn parse_elf<'data>(data: &'data [u8], label: &str) -> ElfBytes<'data, AnyEndian> { + ElfBytes::::minimal_parse(data) + .unwrap_or_else(|e| panic!("failed to parse ELF data from {label}: {e}")) +} + +fn symbol_indexes_in_table( + elf: &ElfBytes<'_, AnyEndian>, + symtab_index: usize, + label: &str, +) -> Vec { + let Some(section_headers) = elf.section_headers() else { + panic!("{label} has no ELF section headers"); + }; + let symtab_header = section_headers + .get(symtab_index) + .unwrap_or_else(|e| panic!("failed to read symbol table header {symtab_index}: {e}")); + + // Relocation sections link to the symbol table they use; archive members usually use + // `.symtab`, while linked dynamic artifacts may use `.dynsym`. + if !matches!(symtab_header.sh_type, abi::SHT_SYMTAB | abi::SHT_DYNSYM) { + return Vec::new(); + } + + let strtab_header = section_headers + .get(symtab_header.sh_link as usize) + .unwrap_or_else(|e| panic!("failed to read linked string table header: {e}")); + let strtab = elf + .section_data_as_strtab(&strtab_header) + .unwrap_or_else(|e| panic!("failed to read linked string table in {label}: {e}")); + let (symtab_data, _) = elf + .section_data(&symtab_header) + .unwrap_or_else(|e| panic!("failed to read symbol table data in {label}: {e}")); + let symtab = SymbolTable::new(elf.ehdr.endianness, elf.ehdr.class, symtab_data); + + symtab + .iter() + .enumerate() + .filter_map(|(index, sym)| { + strtab + .get(sym.st_name as usize) + .ok() + .filter(|name| *name == SYMBOL) + .map(|_| index as u32) + }) + .collect() +} + +/// Extract every TLSDESC access sequence for [`SYMBOL`] from a single ELF object blob. +pub fn windows_in_elf(data: &[u8], label: &str) -> Vec { + let elf = parse_elf(data, label); + let arch = Arch::from_e_machine(elf.ehdr.e_machine, label); + let Some(section_headers) = elf.section_headers() else { + panic!("{label} has no ELF section headers"); + }; + let mut windows = Vec::new(); + + for section_header in section_headers + .iter() + .filter(|shdr| matches!(shdr.sh_type, abi::SHT_REL | abi::SHT_RELA)) + { + let symbol_indexes = symbol_indexes_in_table(&elf, section_header.sh_link as usize, label); + if symbol_indexes.is_empty() { + continue; + } + + let target_header = section_headers + .get(section_header.sh_info as usize) + .unwrap_or_else(|e| panic!("failed to read relocation target section header: {e}")); + let (target_data, _) = elf + .section_data(&target_header) + .unwrap_or_else(|e| panic!("failed to read relocation target section in {label}: {e}")); + + let mut offsets = Vec::new(); + match section_header.sh_type { + abi::SHT_REL => { + let rels = elf + .section_data_as_rels(§ion_header) + .unwrap_or_else(|e| panic!("failed to read REL relocations in {label}: {e}")); + offsets.extend( + rels.filter(|rel| { + symbol_indexes.contains(&rel.r_sym) + && arch.is_tlsdesc_object_relocation(rel.r_type) + }) + .map(|rel| rel.r_offset), + ); + } + abi::SHT_RELA => { + let relas = elf + .section_data_as_relas(§ion_header) + .unwrap_or_else(|e| panic!("failed to read RELA relocations in {label}: {e}")); + offsets.extend( + relas + .filter(|rela| { + symbol_indexes.contains(&rela.r_sym) + && arch.is_tlsdesc_object_relocation(rela.r_type) + }) + .map(|rela| rela.r_offset), + ); + } + _ => unreachable!(), + } + + if offsets.is_empty() { + continue; + } + + offsets.sort_unstable(); + let per_access = arch.relocations_per_access(); + assert!( + offsets.len() % per_access == 0, + "expected TLSDESC relocations for {SYMBOL} in {label} to come in groups of \ + {per_access}; found {} offsets", + offsets.len() + ); + + for chunk in offsets.chunks_exact(per_access) { + let (start, end) = arch.sequence_bounds(chunk, target_data.len()); + windows.push(TlsDescWindow { + label: label.to_owned(), + arch, + bytes: target_data[start..end].to_vec(), + }); + } + } + + windows +} + +/// Extract every TLSDESC access sequence for [`SYMBOL`] from a single ELF object file on disk. +pub fn windows_in_object_file(path: &Path) -> Vec { + let data = + std::fs::read(path).unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display())); + windows_in_elf(&data, &path.display().to_string()) +} + +/// Extract every TLSDESC access sequence for [`SYMBOL`] from every ELF member of a static archive. +pub fn windows_in_archive(path: &Path) -> Vec { + let archive_data = + std::fs::read(path).unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display())); + let archive = ArchiveFile::parse(&*archive_data) + .unwrap_or_else(|e| panic!("failed to parse archive {}: {e}", path.display())); + + let mut windows = Vec::new(); + for member in archive.members() { + let member = + member.unwrap_or_else(|e| panic!("failed to read member in {}: {e}", path.display())); + let member_data = member.data(&*archive_data).unwrap_or_else(|e| { + panic!( + "failed to read member data for {} in {}: {e}", + String::from_utf8_lossy(member.name()), + path.display() + ) + }); + + if member_data.starts_with(&abi::ELFMAGIC) { + let member_name = String::from_utf8_lossy(member.name()).into_owned(); + let label = format!("{}({member_name})", path.display()); + windows.extend(windows_in_elf(member_data, &label)); + } + } + + windows +} diff --git a/libdd-otel-thread-ctx/src/tls_shim.c b/libdd-otel-thread-ctx/src/tls_shim.c deleted file mode 100644 index 0322967725..0000000000 --- a/libdd-otel-thread-ctx/src/tls_shim.c +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ -// SPDX-License-Identifier: Apache-2.0 - -// Declares the thread-local pointer that external readers (e.g. the eBPF -// profiler) discover via the dynsym table. The Rust layer accesses this -// pointer in lib.rs. -// -// The variable is declared in C in order to use the TLSDESC dialect for -// thread-local storage, which is required by the OTel thread-level context -// sharing spec. Unfortunately, it's not possible to have Rust use this dialect -// as of today. -#include - -__attribute__((visibility("default"))) -__thread void *otel_thread_ctx_v1 = NULL; - -// Return the resolved address of the thread-local variable. -void **libdd_get_otel_thread_ctx_v1(void) { - return &otel_thread_ctx_v1; -}