Skip to content

Commit 7122e8d

Browse files
committed
Detect unpublished data on macos after fork
1 parent 07b6ee8 commit 7122e8d

7 files changed

Lines changed: 139 additions & 32 deletions

File tree

Cargo.lock

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

libdd-library-config/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ serial_test = "3.2"
4040
memfd = { version = "0.6" }
4141
libc = "0.2"
4242

43+
[target.'cfg(target_os = "macos")'.dependencies]
44+
portable-atomic = { version = "1.9.0", default-features = false }
45+
4346
[lints.clippy]
4447
std_instead_of_alloc = "warn"
4548
std_instead_of_core = "warn"

libdd-library-config/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
// SPDX-License-Identifier: Apache-2.0
33
extern crate alloc;
44

5-
#[cfg(any(feature = "process-context-reader", feature = "process-context-writer"))]
5+
#[cfg(all(
6+
any(feature = "process-context-reader", feature = "process-context-writer"),
7+
any(target_os = "linux", target_os = "macos", target_os = "windows")
8+
))]
69
pub mod otel_process_ctx;
710
pub mod tracer_metadata;
811

libdd-library-config/src/otel_process_ctx.rs

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ mod writer;
3333
compile_error!("OTel process context requires 64-bit atomics on Linux");
3434
#[cfg(target_os = "linux")]
3535
pub mod linux;
36+
#[cfg(target_os = "macos")]
37+
mod macos;
3638

3739
#[cfg(feature = "process-context-reader")]
3840
pub use reader::ProcessContextSelfReader;
@@ -92,16 +94,50 @@ mod tests {
9294
}
9395
}
9496

95-
#[cfg(any(target_os = "macos", target_os = "windows"))]
96-
mod non_linux {
97-
use super::super::MappingHeaderSnapshot;
97+
#[cfg(target_os = "macos")]
98+
mod macos {
9899
use core::{ptr, sync::atomic::Ordering};
99100
use std::io;
100101

101-
#[cfg(target_os = "macos")]
102-
use super::super::writer::macos::otel_process_ctx_v2;
103-
#[cfg(target_os = "windows")]
104-
use super::super::writer::windows::otel_process_ctx_v2;
102+
use super::super::{
103+
macos::{HEADER_ADDRESS_MASK, PUBLISHER_PID_SHIFT},
104+
writer::macos::otel_process_ctx_v2,
105+
MappingHeaderSnapshot,
106+
};
107+
108+
fn published_header() -> *mut u8 {
109+
let value = otel_process_ctx_v2.load(Ordering::Acquire);
110+
let publisher_pid = (value >> PUBLISHER_PID_SHIFT) as u32;
111+
if publisher_pid != std::process::id() {
112+
return ptr::null_mut();
113+
}
114+
115+
let header_address = (value & HEADER_ADDRESS_MASK) as usize;
116+
ptr::with_exposed_provenance_mut(header_address)
117+
}
118+
119+
pub(super) fn read_process_context() -> io::Result<MappingHeaderSnapshot> {
120+
let header_ptr: *const MappingHeaderSnapshot = published_header().cast();
121+
if header_ptr.is_null() {
122+
return Err(io::Error::new(
123+
io::ErrorKind::NotFound,
124+
"no process context is published",
125+
));
126+
}
127+
Ok(unsafe { ptr::read(header_ptr) })
128+
}
129+
130+
pub(super) fn is_published() -> bool {
131+
!published_header().is_null()
132+
}
133+
}
134+
135+
#[cfg(target_os = "windows")]
136+
mod windows {
137+
use core::{ptr, sync::atomic::Ordering};
138+
use std::io;
139+
140+
use super::super::{writer::windows::otel_process_ctx_v2, MappingHeaderSnapshot};
105141

106142
pub(super) fn read_process_context() -> io::Result<MappingHeaderSnapshot> {
107143
let header_ptr: *const MappingHeaderSnapshot =
@@ -122,8 +158,10 @@ mod tests {
122158

123159
#[cfg(target_os = "linux")]
124160
use linux::{is_published, read_process_context};
125-
#[cfg(any(target_os = "macos", target_os = "windows"))]
126-
use non_linux::{is_published, read_process_context};
161+
#[cfg(target_os = "macos")]
162+
use macos::{is_published, read_process_context};
163+
#[cfg(target_os = "windows")]
164+
use windows::{is_published, read_process_context};
127165

128166
#[test]
129167
#[cfg_attr(miri, ignore)]
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use portable_atomic::AtomicU128;
5+
6+
#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
7+
compile_error!("OTel process context only supports aarch64 and x86_64 on macOS");
8+
#[cfg(not(target_endian = "little"))]
9+
compile_error!("OTel process context requires a little-endian macOS target");
10+
11+
pub(super) type AtomicPublishedHeader = AtomicU128;
12+
13+
const _: () = {
14+
// Both supported macOS architectures have native 128-bit atomics. Keep this assertion so a
15+
// target change cannot silently select portable-atomic's software-lock fallback.
16+
assert!(AtomicPublishedHeader::is_always_lock_free());
17+
assert!(size_of::<AtomicPublishedHeader>() == size_of::<u128>());
18+
assert!(align_of::<AtomicPublishedHeader>() == 16);
19+
assert!(size_of::<usize>() == size_of::<u64>());
20+
};
21+
22+
// The low 64 bits contain the header address and the next 32 bits contain the publisher PID.
23+
// Keeping them in one value lets readers observe both through a single atomic load.
24+
#[cfg(feature = "process-context-reader")]
25+
pub(super) const HEADER_ADDRESS_MASK: u128 = u64::MAX as u128;
26+
pub(super) const PUBLISHER_PID_SHIFT: u32 = u64::BITS;

libdd-library-config/src/otel_process_ctx/reader/macos.rs

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,52 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
use core::{
5-
ptr::NonNull,
6-
sync::atomic::{AtomicPtr, Ordering},
5+
ptr::{self, NonNull},
6+
sync::atomic::Ordering,
77
};
88
use std::io;
99
#[cfg(not(feature = "process-context-writer"))]
1010
use std::sync::OnceLock;
1111

1212
use super::ReaderPlatform;
13+
use crate::otel_process_ctx::macos::{
14+
AtomicPublishedHeader, HEADER_ADDRESS_MASK, PUBLISHER_PID_SHIFT,
15+
};
1316

1417
pub(super) struct HeaderDiscovery;
1518

1619
impl ReaderPlatform for HeaderDiscovery {
1720
fn discover_header() -> io::Result<NonNull<u8>> {
18-
let header = process_context_global()?.load(Ordering::Acquire);
19-
NonNull::new(header).ok_or_else(|| {
20-
io::Error::new(io::ErrorKind::NotFound, "no process context is published")
21-
})
21+
let global = process_context_global()?;
22+
let current_pid = std::process::id();
23+
24+
let value = global.load(Ordering::Acquire);
25+
let (publisher_pid, header) = unpack_published_header(value);
26+
27+
// After fork, the global retains the parent's publication even if the mapping itself was
28+
// excluded from inheritance. Treat that stale publication exactly like an unpublished one.
29+
if publisher_pid != current_pid {
30+
return Err(not_found());
31+
}
32+
33+
NonNull::new(header).ok_or_else(not_found)
2234
}
2335
}
2436

37+
fn not_found() -> io::Error {
38+
io::Error::new(
39+
io::ErrorKind::NotFound,
40+
"no process context is published by the current process",
41+
)
42+
}
43+
2544
#[cfg(feature = "process-context-writer")]
26-
fn process_context_global() -> io::Result<&'static AtomicPtr<u8>> {
45+
fn process_context_global() -> io::Result<&'static AtomicPublishedHeader> {
2746
Ok(&crate::otel_process_ctx::writer::macos::otel_process_ctx_v2)
2847
}
2948

3049
#[cfg(not(feature = "process-context-writer"))]
31-
fn process_context_global() -> io::Result<&'static AtomicPtr<u8>> {
50+
fn process_context_global() -> io::Result<&'static AtomicPublishedHeader> {
3251
static SYMBOL_ADDRESS: OnceLock<usize> = OnceLock::new();
3352

3453
let address = match SYMBOL_ADDRESS.get() {
@@ -53,27 +72,34 @@ fn process_context_global() -> io::Result<&'static AtomicPtr<u8>> {
5372
}
5473
};
5574

56-
// SAFETY: dlsym returned the address of the exported AtomicPtr<u8>. Successful addresses are
57-
// cached only for the lifetime of the process.
58-
Ok(unsafe { &*(address as *const AtomicPtr<u8>) })
75+
// SAFETY: dlsym returned the address of the exported AtomicPublishedHeader. Successful
76+
// addresses are cached only for the lifetime of the process.
77+
Ok(unsafe { &*(address as *const AtomicPublishedHeader) })
78+
}
79+
80+
fn unpack_published_header(value: u128) -> (u32, *mut u8) {
81+
let publisher_pid = (value >> PUBLISHER_PID_SHIFT) as u32;
82+
let header_address = (value & HEADER_ADDRESS_MASK) as usize;
83+
(
84+
publisher_pid,
85+
ptr::with_exposed_provenance_mut(header_address),
86+
)
5987
}
6088

6189
#[cfg(all(test, not(feature = "process-context-writer")))]
6290
mod tests {
63-
use core::{
64-
ptr,
65-
sync::atomic::{AtomicPtr, Ordering},
66-
};
91+
use core::{ptr, sync::atomic::Ordering};
6792

6893
use libdd_trace_protobuf::opentelemetry::proto::common::v1::{KeyValue, ProcessContext};
6994

7095
use crate::otel_process_ctx::{
96+
macos::{AtomicPublishedHeader, PUBLISHER_PID_SHIFT},
7197
MappingHeaderSnapshot, ProcessContextSelfReader, PROCESS_CTX_VERSION, SIGNATURE,
7298
};
7399

74100
#[no_mangle]
75101
#[allow(non_upper_case_globals)]
76-
pub static otel_process_ctx_v2: AtomicPtr<u8> = AtomicPtr::new(ptr::null_mut());
102+
pub static otel_process_ctx_v2: AtomicPublishedHeader = AtomicPublishedHeader::new(0);
77103

78104
#[test]
79105
fn reads_context_from_exported_global() {
@@ -93,11 +119,13 @@ mod tests {
93119
monotonic_published_at_ns: 1,
94120
payload_ptr: payload.as_ptr(),
95121
};
96-
otel_process_ctx_v2.store(ptr::from_ref(&header).cast_mut().cast(), Ordering::Release);
122+
let published_header = (u128::from(std::process::id()) << PUBLISHER_PID_SHIFT)
123+
| ptr::from_ref(&header).expose_provenance() as u128;
124+
otel_process_ctx_v2.store(published_header, Ordering::Release);
97125

98126
let reader = ProcessContextSelfReader::new().expect("reader creation should succeed");
99127
assert_eq!(reader.read().expect("read should succeed"), expected);
100128

101-
otel_process_ctx_v2.store(ptr::null_mut(), Ordering::Relaxed);
129+
otel_process_ctx_v2.store(0, Ordering::Relaxed);
102130
}
103131
}

libdd-library-config/src/otel_process_ctx/writer/macos.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@ use core::{
88
ffi::c_void,
99
mem::forget,
1010
ptr::{self, NonNull},
11-
sync::atomic::{fence, AtomicPtr, Ordering},
11+
sync::atomic::{fence, Ordering},
1212
};
1313
use std::io;
1414

1515
use super::{HeaderMemoryHolder, MappingHeader, MonotonicTime};
16+
use crate::otel_process_ctx::macos::{AtomicPublishedHeader, PUBLISHER_PID_SHIFT};
1617

18+
// A child inherits this global after fork even when minherit excludes the header mapping. Store
19+
// the publisher PID with the pointer so the child cannot discover its parent's unmapped address.
1720
#[no_mangle]
1821
#[allow(non_upper_case_globals)]
19-
pub static otel_process_ctx_v2: AtomicPtr<u8> = AtomicPtr::new(ptr::null_mut());
22+
pub static otel_process_ctx_v2: AtomicPublishedHeader = AtomicPublishedHeader::new(0);
2023

2124
// From <mach/vm_inherit.h>; the libc crate does not expose this constant.
2225
const VM_INHERIT_NONE: libc::c_int = 2;
@@ -80,11 +83,12 @@ impl HeaderMemoryHolder for VmRegion {
8083
}
8184

8285
fn make_discoverable(&mut self) {
83-
otel_process_ctx_v2.store(self.start_addr.as_ptr().cast(), Ordering::Release);
86+
let value = pack_published_header(std::process::id(), self.start_addr.as_ptr().cast());
87+
otel_process_ctx_v2.store(value, Ordering::Release);
8488
}
8589

8690
fn unpublish_and_release(mut self) -> io::Result<()> {
87-
otel_process_ctx_v2.store(ptr::null_mut(), Ordering::Relaxed);
91+
otel_process_ctx_v2.store(0, Ordering::Relaxed);
8892
// Make it slightly more likely that a reader will observe the unavailability.
8993
fence(Ordering::SeqCst);
9094
self.unmap()?;
@@ -97,6 +101,10 @@ impl HeaderMemoryHolder for VmRegion {
97101
}
98102
}
99103

104+
fn pack_published_header(publisher_pid: u32, header: *mut u8) -> u128 {
105+
(u128::from(publisher_pid) << PUBLISHER_PID_SHIFT) | header.expose_provenance() as u128
106+
}
107+
100108
impl MonotonicTime for MonotonicClock {
101109
fn monotonic_time_ns() -> io::Result<u64> {
102110
// SAFETY: CLOCK_MONOTONIC_RAW is a valid clock ID and this function has no pointer

0 commit comments

Comments
 (0)