-
Notifications
You must be signed in to change notification settings - Fork 21
feat(otel-thread-ctx)!: add self check capability #2095
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
yannham
wants to merge
16
commits into
main
Choose a base branch
from
yannham/thread-ctx-autocheck
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
57203ae
feat: add otel thread ctx setup autotest FFI
yannham 57c96b7
fix: check for absence of GD/LD relocations instead of presence of TL…
yannham 03230c2
refactor: consolidate elf_properties test to reuse autocheck logic
yannham ada9fe5
style: formatting
yannham fc236db
chore: better naming, improve comments, remove so-specific comments
yannham df4e37c
doc: add documentation to a couple functions
yannham 425e208
fix: autocheck -> sanity_check
yannham ca0b169
doc: fix dead link
yannham 8c191a8
doc: fix typo
yannham 60720e8
refactor: use anyhow instead of raw strings for errors
yannham eb60e08
chore: update license file
yannham de690ed
fix: reject all non-TLSDESC TLS relocations in sanity check
yannham edf4aed
doc: avoid double negative in constraint description
yannham 53b9256
fix: cargo doc warnings
yannham d043684
doc: expand on the elf_properties test being for the shared library
yannham ddf086c
fix: handle pathnames with spaces in /proc/self/maps parser
yannham File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,84 +1,34 @@ | ||
| // Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| //! Verify ELF properties of the built cdylib on Linux. | ||
| //! Verify ELF properties of the shared library built on Linux. Running the sanity check in | ||
| //! [libdd_otel_thread_ctx] directly in a Rust test would exercise the static linking case. This | ||
| //! test rather checks that the dynamic library is properly linked, which is why it lives within the | ||
| //! FFI. | ||
| //! | ||
| //! 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(); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| // 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` 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. | ||
|
|
||
| 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] | ||
| // Skip the first 5 whitespace-delimited tokens then take the rest verbatim | ||
| // as the path, so that pathnames containing spaces are preserved intact. | ||
| let mut rest = line; | ||
|
|
||
| for _ in 0..5 { | ||
| rest = rest.trim_start_matches(|c: char| c.is_ascii_whitespace()); | ||
| rest = rest.trim_start_matches(|c: char| !c.is_ascii_whitespace()); | ||
| } | ||
|
|
||
| let path = rest.trim_start_matches(|c: char| c.is_ascii_whitespace()); | ||
|
|
||
| if !path.starts_with('/') { | ||
| continue; | ||
| } | ||
|
|
||
| if let Some((start_str, end_str)) = line | ||
| .split_whitespace() | ||
| .next() | ||
| .and_then(|f| f.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(()) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.