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 .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,14 @@ jobs:
fi
env:
RUST_BACKTRACE: full
- name: "[${{ steps.rust-version.outputs.version}}] cargo nextest run -p libdd-library-config --no-default-features --features process-context-reader"
shell: bash
run: |
if [[ -z "$PACKAGES" ]] || echo "$PACKAGES" | grep -q "libdd-library-config"; then
cargo nextest run -p libdd-library-config --no-default-features --features process-context-reader --profile ci --verbose --no-tests=pass
fi
env:
RUST_BACKTRACE: full
- name: "[${{ steps.rust-version.outputs.version}}] cargo nextest run --all-features"
shell: bash
run: |
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

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

31 changes: 21 additions & 10 deletions libdd-library-config-ffi/src/tracer_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::os::raw::{c_char, c_int};
/// C-compatible representation of an anonymous file handle
#[repr(C)]
pub struct TracerMemfdHandle {
/// File descriptor (relevant only on Linux)
/// File descriptor on Linux; `-1` on other platforms.
pub fd: c_int,
}

Expand Down Expand Up @@ -102,21 +102,17 @@ pub unsafe extern "C" fn ddog_tracer_metadata_set(
}
}

/// Serializes the `TracerMetadata` into a platform-specific memory handle (e.g., memfd on Linux).
/// This function also attempts to publish the tracer metadata as an OTel process context
/// separately, but will ignore resulting errors.
/// Stores the `TracerMetadata` using the platform's supported mechanisms. Linux serializes the
/// metadata into a memfd and attempts to publish it as an OTel process context. Other platforms
/// publish only the OTel process context.
///
/// # Safety
/// - `ptr` must be a valid, non-null pointer to a `TracerMetadata`.
///
/// # Returns
/// - On Linux: a `TracerMemfdHandle` containing a raw file descriptor to a memory file.
/// - On unsupported platforms: an error.
/// - On other platforms: a `TracerMemfdHandle` with `fd` set to `-1`.
/// - On failure: propagates any internal errors from the metadata storage process.
///
/// # Platform Support
/// This function currently only supports Linux via `memfd`. On other platforms,
/// it will return an error.
#[no_mangle]
pub unsafe extern "C" fn ddog_tracer_metadata_store(
ptr: *mut TracerMetadata,
Expand All @@ -141,8 +137,23 @@ pub unsafe extern "C" fn ddog_tracer_metadata_store(
})
}
#[cfg(not(target_os = "linux"))]
Ok(_) => Err(anyhow::anyhow!("Unsupported platform")),
Ok(_) => Ok(TracerMemfdHandle { fd: -1 }),
Err(err) => Err(err),
};
result.into()
}

#[cfg(all(test, not(target_os = "linux")))]
mod tests {
use super::{ddog_tracer_metadata_store, TracerMetadata};

#[test]
fn store_returns_success_without_a_file_descriptor() {
let mut metadata = TracerMetadata::default();

let handle = unsafe { ddog_tracer_metadata_store(&mut metadata) }.unwrap();
assert_eq!(handle.fd, -1);

libdd_library_config::otel_process_ctx::unpublish().expect("unpublish should succeed");
}
}
3 changes: 3 additions & 0 deletions libdd-library-config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ serial_test = "3.2"
memfd = { version = "0.6" }
libc = "0.2"

[target.'cfg(target_os = "macos")'.dependencies]
portable-atomic = { version = "1.9.0", default-features = false }

[lints.clippy]
std_instead_of_alloc = "warn"
std_instead_of_core = "warn"
4 changes: 2 additions & 2 deletions libdd-library-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
extern crate alloc;

#[cfg(all(
target_os = "linux",
any(feature = "process-context-reader", feature = "process-context-writer")
any(feature = "process-context-reader", feature = "process-context-writer"),
any(target_os = "linux", target_os = "macos", target_os = "windows")
))]
pub mod otel_process_ctx;
pub mod tracer_metadata;
Expand Down
77 changes: 76 additions & 1 deletion libdd-library-config/src/otel_process_ctx.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

//! Implementation of the Linux parts of the [OTEL process
//! Implementation of the [OTEL process
//! context specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/oteps/profiles/4719-process-ctx.md).
//!
//! Note: the Linux implementation follows the discovery method described in the OTEL process
//! specification linked above, that is, uses a memfd or a named mapping with the name OTEL_CTX.
//! This is a strategy only viable on Linux, since MacOS and Windows do not have those exact
//! features. Here, the MacOS and Windows implementations, on the other hand, use a global atomic
//! pointer to the mapping header that is published as a symbol named `otel_process_ctx_v2`.
//! SUCH MECHANISM IS NOT PART OF THE SPECIFICATION, which deals only with Linux.
//!
//! The update/read protocol is seqlock-style: the publisher marks the mapping as unavailable,
//! writes the payload metadata, publishes a non-zero version, and readers accept a copy only if
//! the version they observed before copying still matches afterward. The general algorithm and
Expand All @@ -26,6 +33,8 @@ mod writer;
compile_error!("OTel process context requires 64-bit atomics on Linux");
#[cfg(target_os = "linux")]
pub mod linux;
#[cfg(target_os = "macos")]
mod macos;

#[cfg(feature = "process-context-reader")]
pub use reader::ProcessContextSelfReader;
Expand Down Expand Up @@ -85,8 +94,74 @@ mod tests {
}
}

#[cfg(target_os = "macos")]
mod macos {
use core::{ptr, sync::atomic::Ordering};
use std::io;

use super::super::{
macos::{HEADER_ADDRESS_MASK, PUBLISHER_PID_SHIFT},
writer::macos::otel_process_ctx_v2,
MappingHeaderSnapshot,
};

fn published_header() -> *mut u8 {
let value = otel_process_ctx_v2.load(Ordering::Acquire);
let publisher_pid = (value >> PUBLISHER_PID_SHIFT) as u32;
if publisher_pid != std::process::id() {
return ptr::null_mut();
}

let header_address = (value & HEADER_ADDRESS_MASK) as usize;
ptr::with_exposed_provenance_mut(header_address)
}

pub(super) fn read_process_context() -> io::Result<MappingHeaderSnapshot> {
let header_ptr: *const MappingHeaderSnapshot = published_header().cast();
if header_ptr.is_null() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"no process context is published",
));
}
Ok(unsafe { ptr::read(header_ptr) })
}

pub(super) fn is_published() -> bool {
!published_header().is_null()
}
}

#[cfg(target_os = "windows")]
mod windows {
use core::{ptr, sync::atomic::Ordering};
use std::io;

use super::super::{writer::windows::otel_process_ctx_v2, MappingHeaderSnapshot};

pub(super) fn read_process_context() -> io::Result<MappingHeaderSnapshot> {
let header_ptr: *const MappingHeaderSnapshot =
otel_process_ctx_v2.load(Ordering::Acquire).cast();
if header_ptr.is_null() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"no process context is published",
));
}
Ok(unsafe { ptr::read(header_ptr) })
}

pub(super) fn is_published() -> bool {
!otel_process_ctx_v2.load(Ordering::Acquire).is_null()
}
}

#[cfg(target_os = "linux")]
use linux::{is_published, read_process_context};
#[cfg(target_os = "macos")]
use macos::{is_published, read_process_context};
#[cfg(target_os = "windows")]
use windows::{is_published, read_process_context};

#[test]
#[cfg_attr(miri, ignore)]
Expand Down
26 changes: 26 additions & 0 deletions libdd-library-config/src/otel_process_ctx/macos.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

use portable_atomic::AtomicU128;

#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
compile_error!("OTel process context only supports aarch64 and x86_64 on macOS");
#[cfg(not(target_endian = "little"))]
compile_error!("OTel process context requires a little-endian macOS target");

pub(super) type AtomicPublishedHeader = AtomicU128;

const _: () = {
// Both supported macOS architectures have native 128-bit atomics. Keep this assertion so a
// target change cannot silently select portable-atomic's software-lock fallback.
assert!(AtomicPublishedHeader::is_always_lock_free());
assert!(size_of::<AtomicPublishedHeader>() == size_of::<u128>());
assert!(align_of::<AtomicPublishedHeader>() == 16);
assert!(size_of::<usize>() == size_of::<u64>());
};

// The low 64 bits contain the header address and the next 32 bits contain the publisher PID.
// Keeping them in one value lets readers observe both through a single atomic load.
#[cfg(feature = "process-context-reader")]
pub(super) const HEADER_ADDRESS_MASK: u128 = u64::MAX as u128;
pub(super) const PUBLISHER_PID_SHIFT: u32 = u64::BITS;
16 changes: 14 additions & 2 deletions libdd-library-config/src/otel_process_ctx/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,28 @@ use prost::Message;

use super::{MappingHeaderSnapshot, PROCESS_CTX_VERSION, SIGNATURE, UNPUBLISHED_OR_UPDATING};

#[cfg(target_os = "linux")]
#[cfg(unix)]
mod copy_pipe_unix;
#[cfg(windows)]
mod copy_pipe_windows;
#[cfg(target_os = "linux")]
pub(super) mod linux;
#[cfg(target_os = "macos")]
pub(super) mod macos;
#[cfg(target_os = "windows")]
pub(super) mod windows;

#[cfg(target_os = "linux")]
#[cfg(unix)]
use copy_pipe_unix::CopyPipe as PlatformCopyPipe;
#[cfg(windows)]
use copy_pipe_windows::CopyPipe as PlatformCopyPipe;

#[cfg(target_os = "linux")]
type PlatformHeaderDiscovery = linux::HeaderDiscovery;
#[cfg(target_os = "macos")]
type PlatformHeaderDiscovery = macos::HeaderDiscovery;
#[cfg(target_os = "windows")]
type PlatformHeaderDiscovery = windows::HeaderDiscovery;

pub(super) trait ReaderPlatform {
fn discover_header() -> io::Result<NonNull<u8>>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,64 @@ fn create_pipe() -> io::Result<(OwnedFd, OwnedFd, usize)> {
Ok((read_fd, write_fd, capacity as usize))
}

#[cfg(target_os = "macos")]
fn create_pipe() -> io::Result<(OwnedFd, OwnedFd, usize)> {
let mut fds = [0; 2];
// SAFETY: fds points to space for the two descriptors returned by pipe.
if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
return Err(last_error("failed to create process context copy pipe"));
}

// SAFETY: pipe initialized both descriptors and ownership is transferred exactly once.
let (read_fd, write_fd) =
unsafe { (OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])) };
configure_fd(&read_fd)?;
configure_fd(&write_fd)?;

// POSIX guarantees that an empty pipe accepts at least PIPE_BUF bytes without blocking.
// SAFETY: write_fd is a valid pipe descriptor.
let chunk_size = unsafe { libc::fpathconf(write_fd.as_raw_fd(), libc::_PC_PIPE_BUF) };
if chunk_size <= 0 {
return Err(last_error(
"failed to query process context copy pipe capacity",
));
}
Comment on lines +193 to +198

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making sure I understand here:

  • Linux: use the full capacity of the pipe (F_GETPIPE_SZ) because we have it (Linux extension).
  • macOS: use the atomically writeable size of the pipe _PC_PIPE_BUF, as we do not have the full capacity of the pipe on this platform.

The practical effect is that on Linux, if we have larger contexts, we can copy it all in one go, whereas on macOS we'll have to loop with syscalls. Yes?


Ok((read_fd, write_fd, chunk_size as usize))
}

#[cfg(target_os = "macos")]
fn configure_fd(fd: &OwnedFd) -> io::Result<()> {
Comment on lines +203 to +204

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is okay but I think making a pipe2 polyfill would have come out shorter and kept more code on the same path/design.

// SAFETY: fd is a valid descriptor.
let status = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETFL) };
if status < 0 {
return Err(last_error(
"failed to query process context copy pipe status flags",
));
}
// SAFETY: fd is valid and F_SETFL accepts the status flags returned above.
if unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_SETFL, status | libc::O_NONBLOCK) } < 0 {
return Err(last_error(
"failed to make process context copy pipe non-blocking",
));
}

// SAFETY: fd is a valid descriptor.
let descriptor = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETFD) };
if descriptor < 0 {
return Err(last_error(
"failed to query process context copy pipe descriptor flags",
));
}
// SAFETY: fd is valid and F_SETFD accepts the descriptor flags returned above.
if unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_SETFD, descriptor | libc::FD_CLOEXEC) } < 0 {
return Err(last_error(
"failed to mark process context copy pipe close-on-exec",
));
}
Ok(())
}

fn last_error(context: &'static str) -> io::Error {
let err = io::Error::last_os_error();
io::Error::new(err.kind(), format!("{context}: {err}"))
Expand Down
Loading
Loading