Skip to content

Commit 97138bc

Browse files
committed
fix(library-config): update Linux process context
1 parent 89a636d commit 97138bc

5 files changed

Lines changed: 99 additions & 59 deletions

File tree

libdd-library-config/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ crate-type = ["lib"]
1515
bench = false
1616

1717
[features]
18-
default = ["process-context-reader", "process-context-writer"]
18+
default = ["process-context-writer"]
1919
otel-thread-ctx = ["process-context-writer"]
2020
process-context-reader = []
2121
process-context-writer = []

libdd-library-config/src/otel_process_ctx.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
//! Implementation of the Linux parts of the [OTEL process
5-
//! context specification](https://github.com/open-telemetry/opentelemetry-specification/pull/4719).
5+
//! context specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/oteps/profiles/4719-process-ctx.md).
66
//!
77
//! The update/read protocol is seqlock-style: the publisher marks the mapping as unavailable,
88
//! writes the payload metadata, publishes a non-zero version, and readers accept a copy only if

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ impl ProcessMemoryCopy for CopyPipe {
7777
io::ErrorKind::WouldBlock,
7878
"process context memory was unmapped during read",
7979
),
80-
pipe_dirty: false,
80+
// actually false on Linux: if EFAULT is returned, nothing was written;
81+
// should anything have been written already we would get a short write
82+
// However, this is not the case for macOS, despite what its manual
83+
// says: See https://github.com/apple-oss-distributions/xnu/blob/5c306bec31e314fa4d8bbdafb2f6f5a6b7e7b291/bsd/man/man2/write.2#L168-L186
84+
pipe_dirty: true,
8185
});
8286
}
8387
_ => {
@@ -233,7 +237,7 @@ mod tests {
233237
.expect_err("a copy crossing into inaccessible memory should fail");
234238

235239
assert_eq!(err.err.kind(), io::ErrorKind::WouldBlock);
236-
assert!(!err.pipe_dirty);
240+
assert!(err.pipe_dirty);
237241
// SAFETY: address and len came from mmap above.
238242
assert_eq!(unsafe { libc::munmap(address, len) }, 0);
239243
}

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

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use core::{
55
convert::TryInto,
66
marker::PhantomData,
77
mem::swap,
8-
ptr,
8+
ptr::{self, NonNull},
99
sync::atomic::{fence, AtomicPtr, AtomicU32, AtomicU64, Ordering},
1010
};
1111
use std::{
@@ -68,7 +68,7 @@ static PROCESS_CONTEXT_HANDLER: Mutex<Option<ProcessContextHandle>> = Mutex::new
6868

6969
pub(super) trait HeaderMemoryHolder: Sized {
7070
fn new() -> io::Result<Self>;
71-
fn as_ptr(&self) -> *mut MappingHeader;
71+
fn as_ptr(&self) -> Option<NonNull<MappingHeader>>;
7272
fn make_discoverable(&mut self);
7373
fn unpublish_and_release(self) -> io::Result<()>;
7474
fn after_fork(self);
@@ -84,9 +84,6 @@ struct ProcessContextHandleGen<M: HeaderMemoryHolder, T: MonotonicTime> {
8484
/// Once published, and until the next update is complete, the backing allocation of
8585
/// `payload` might be read and thus must not move (e.g. by resizing or drop).
8686
payload: Vec<u8>,
87-
/// The process id of the last publisher. This is useful to detect forks(), and publish a
88-
/// new context accordingly.
89-
pid: u32,
9087
monotonic_clock: PhantomData<T>,
9188
}
9289

@@ -101,7 +98,11 @@ impl<M: HeaderMemoryHolder, T: MonotonicTime> ProcessContextHandleGen<M, T> {
10198
let mut mapping = M::new()?;
10299
let published_at_ns = T::monotonic_time_ns()?;
103100

104-
let header = mapping.as_ptr();
101+
let header = mapping
102+
.as_ptr()
103+
// should never happen; as_ptr should only return None after a fork
104+
.ok_or_else(|| io::Error::other("new process context header mapping is unavailable"))?
105+
.as_ptr();
105106

106107
// SAFETY: header points to a zero-filled, writable allocation of at least
107108
// mapping_size() bytes with MappingHeader alignment; field projections are in-bounds.
@@ -111,12 +112,12 @@ impl<M: HeaderMemoryHolder, T: MonotonicTime> ProcessContextHandleGen<M, T> {
111112
unsafe {
112113
ptr::addr_of_mut!((*header).signature).write(*SIGNATURE);
113114
ptr::addr_of_mut!((*header).version).write(PROCESS_CTX_VERSION);
114-
(*header)
115-
.payload_ptr
116-
.store(payload.as_ptr().cast_mut(), Ordering::Relaxed);
117115
(*header)
118116
.payload_size
119117
.store(payload_size, Ordering::Relaxed);
118+
(*header)
119+
.payload_ptr
120+
.store(payload.as_ptr().cast_mut(), Ordering::Relaxed);
120121

121122
fence(Ordering::SeqCst);
122123
(*header)
@@ -129,14 +130,22 @@ impl<M: HeaderMemoryHolder, T: MonotonicTime> ProcessContextHandleGen<M, T> {
129130
Ok(ProcessContextHandleGen {
130131
mapping,
131132
payload,
132-
pid: std::process::id(),
133133
monotonic_clock: PhantomData,
134134
})
135135
}
136136

137137
/// Updates the context after initial publication.
138138
fn update(&mut self, payload: Vec<u8>) -> io::Result<()> {
139-
let header = self.mapping.as_ptr();
139+
let header = self
140+
.mapping
141+
.as_ptr()
142+
.ok_or_else(|| {
143+
io::Error::new(
144+
io::ErrorKind::InvalidInput,
145+
"process context header mapping is unavailable after fork",
146+
)
147+
})?
148+
.as_ptr();
140149

141150
let monotonic_published_at_ns = T::monotonic_time_ns()?;
142151
let payload_size: u32 = payload.len().try_into().map_err(|_| {
@@ -157,7 +166,10 @@ impl<M: HeaderMemoryHolder, T: MonotonicTime> ProcessContextHandleGen<M, T> {
157166
.swap(UNPUBLISHED_OR_UPDATING, Ordering::Relaxed)
158167
};
159168
if last_monotonic_published_at_ns == UNPUBLISHED_OR_UPDATING {
160-
panic!("concurrent update of the process context is not supported");
169+
return Err(io::Error::new(
170+
io::ErrorKind::WouldBlock,
171+
"context is already being updated",
172+
));
161173
}
162174

163175
let monotonic_published_at_ns =
@@ -172,12 +184,12 @@ impl<M: HeaderMemoryHolder, T: MonotonicTime> ProcessContextHandleGen<M, T> {
172184
self.payload = payload;
173185

174186
unsafe {
175-
(*header)
176-
.payload_ptr
177-
.store(self.payload.as_ptr().cast_mut(), Ordering::Relaxed);
178187
(*header)
179188
.payload_size
180189
.store(payload_size, Ordering::Relaxed);
190+
(*header)
191+
.payload_ptr
192+
.store(self.payload.as_ptr().cast_mut(), Ordering::Relaxed);
181193
}
182194

183195
// Prevent the final timestamp publication from moving above either the payload metadata
@@ -214,8 +226,7 @@ fn lock_context_handle() -> io::Result<MutexGuard<'static, Option<ProcessContext
214226
///
215227
/// - this is the first publication
216228
/// - [unpublish] has been called last
217-
/// - the previous context has been published from a different process id (that is, a `fork()`
218-
/// happened and we're the child process)
229+
/// - the previous header mapping is unavailable after `fork()`
219230
///
220231
/// Then we follow the Publish protocol of the OTel process context specification (allocating a
221232
/// fresh header).
@@ -239,7 +250,7 @@ pub(super) fn publish_raw_payload(payload: Vec<u8>) -> io::Result<()> {
239250
let mut guard = lock_context_handle()?;
240251

241252
match &mut *guard {
242-
Some(handler) if handler.pid == std::process::id() => handler.update(payload),
253+
Some(handler) if handler.mapping.as_ptr().is_some() => handler.update(payload),
243254
Some(handler) => {
244255
let mut local_handler = ProcessContextHandleGen::publish(payload)?;
245256
// If we've been forked, we need to prevent the mapping from being dropped
@@ -272,20 +283,22 @@ pub fn unpublish() -> io::Result<()> {
272283
mapping, payload, ..
273284
}) = guard.take()
274285
{
275-
// Mark the context as unavailable before freeing the mapping/payload. The fence forces
276-
// the writing CPU not to reorder the unavailable timestamp store and the deallocation
277-
// stores. This gives readers more of a chance (but no guarantee) to observe an
278-
// unavailable context before the publication is removed.
279-
//
280-
// SAFETY: the mapping is still live and valid, and the global mutex prevents
281-
// concurrent in-process writers from mutating the plain header fields.
282-
let header = mapping.as_ptr();
283-
unsafe {
284-
(*header)
285-
.monotonic_published_at_ns
286-
.store(UNPUBLISHED_OR_UPDATING, Ordering::Relaxed);
286+
if let Some(header) = mapping.as_ptr() {
287+
// Mark the context as unavailable before freeing the mapping/payload. The fence
288+
// forces the writing CPU not to reorder the unavailable timestamp store and the
289+
// deallocation stores. This gives readers more of a chance (but no guarantee) to
290+
// observe an unavailable context before the publication is removed.
291+
//
292+
// SAFETY: the mapping is still live and valid, and the global mutex prevents
293+
// concurrent in-process writers from mutating the plain header fields.
294+
let header = header.as_ptr();
295+
unsafe {
296+
(*header)
297+
.monotonic_published_at_ns
298+
.store(UNPUBLISHED_OR_UPDATING, Ordering::Relaxed);
299+
}
300+
fence(Ordering::SeqCst);
287301
}
288-
fence(Ordering::SeqCst);
289302

290303
// The payload will still drop if this fails, leaving a zero timestamp behind.
291304
mapping.unpublish_and_release()?;

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

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
use core::{
55
convert::TryInto,
66
ffi::{c_void, CStr},
7-
mem::{forget, ManuallyDrop},
7+
mem::ManuallyDrop,
88
ptr::{self, NonNull},
99
time::Duration,
1010
};
@@ -19,12 +19,14 @@ use std::{
1919
/// # Safety
2020
///
2121
/// The following invariants MUST always hold for safety and are guaranteed by [MemMapping]:
22-
/// - `start` is non-null, is coming from a previous call to `mmap` with a size value of
23-
/// `mapping_size()` and hasn't been unmmaped since.
22+
/// - when `only_for_pid` allows access from the current process, `start_addr` points to a live
23+
/// mapping of `mapping_size()` bytes created by `mmap`.
2424
/// - once `self` has been dropped, no memory access must be performed on the memory previously
25-
/// pointed to by `start`.
25+
/// pointed to by `start_addr`.
2626
pub(super) struct MemMapping {
2727
start_addr: NonNull<c_void>,
28+
/// `Some(pid)` when `MADV_DONTFORK` succeeded, otherwise `None`.
29+
only_for_pid: Option<u32>,
2830
}
2931

3032
// SAFETY: MemMapping represents ownership over the mapped region. It never leaks or
@@ -33,23 +35,18 @@ unsafe impl Send for MemMapping {}
3335

3436
impl super::HeaderMemoryHolder for MemMapping {
3537
fn new() -> io::Result<Self> {
36-
let mapping = Self::new()?;
37-
check_syscall_retval(
38-
// SAFETY: MemMapping owns a live mapping of mapping_size() bytes.
39-
unsafe {
40-
libc::madvise(
41-
mapping.start_addr.as_ptr(),
42-
super::mapping_size(),
43-
libc::MADV_DONTFORK,
44-
)
45-
},
46-
"madvise MADVISE_DONTFORK failed",
47-
)?;
48-
Ok(mapping)
38+
Self::new()
4939
}
5040

51-
fn as_ptr(&self) -> *mut super::MappingHeader {
52-
self.start_addr.as_ptr() as *mut super::MappingHeader
41+
fn as_ptr(&self) -> Option<NonNull<super::MappingHeader>> {
42+
if self
43+
.only_for_pid
44+
.is_some_and(|pid| pid != std::process::id())
45+
{
46+
None
47+
} else {
48+
Some(self.start_addr.cast())
49+
}
5350
}
5451

5552
fn make_discoverable(&mut self) {
@@ -63,7 +60,7 @@ impl super::HeaderMemoryHolder for MemMapping {
6360
}
6461

6562
fn after_fork(self) {
66-
forget(self);
63+
drop(self);
6764
}
6865
}
6966

@@ -92,7 +89,7 @@ impl MemMapping {
9289
fn new() -> io::Result<Self> {
9390
let size = super::mapping_size();
9491

95-
try_memfd(crate::otel_process_ctx::linux::MAPPING_NAME, libc::MFD_CLOEXEC | libc::MFD_NOEXEC_SEAL | libc::MFD_ALLOW_SEALING)
92+
let mut mapping = try_memfd(crate::otel_process_ctx::linux::MAPPING_NAME, libc::MFD_CLOEXEC | libc::MFD_NOEXEC_SEAL | libc::MFD_ALLOW_SEALING)
9693
.or_else(|_| try_memfd(crate::otel_process_ctx::linux::MAPPING_NAME, libc::MFD_CLOEXEC | libc::MFD_ALLOW_SEALING))
9794
.and_then(|fd| {
9895
// SAFETY: fd is a valid open file descriptor.
@@ -118,7 +115,10 @@ impl MemMapping {
118115
)?;
119116

120117
// We (implicitly) close the file descriptor right away, but this ok
121-
Ok(MemMapping { start_addr })
118+
Ok(MemMapping {
119+
start_addr,
120+
only_for_pid: None,
121+
})
122122
})
123123
// If any previous step failed, we fallback to an anonymous mapping
124124
.or_else(|_| {
@@ -137,8 +137,24 @@ impl MemMapping {
137137
"mmap failed: couldn't create a memfd or anonymous mmapped region for process context publication"
138138
)?;
139139

140-
Ok(MemMapping { start_addr })
141-
})
140+
Ok::<_, io::Error>(MemMapping {
141+
start_addr,
142+
only_for_pid: None,
143+
})
144+
})?;
145+
146+
// SAFETY: MemMapping owns a live mapping of mapping_size() bytes. Failure is harmless;
147+
// the mapping then follows the default inheritance behavior.
148+
mapping.only_for_pid = (unsafe {
149+
libc::madvise(
150+
mapping.start_addr.as_ptr(),
151+
super::mapping_size(),
152+
libc::MADV_DONTFORK,
153+
)
154+
} == 0)
155+
.then_some(std::process::id());
156+
157+
Ok(mapping)
142158
}
143159

144160
/// Makes this mapping discoverable by giving it a name.
@@ -190,6 +206,13 @@ impl MemMapping {
190206
/// Practically, `self` must be put in a `ManuallyDrop` wrapper and forgotten, or being in
191207
/// the process of being dropped.
192208
unsafe fn unmap(&mut self) -> io::Result<()> {
209+
if self
210+
.only_for_pid
211+
.is_some_and(|pid| pid != std::process::id())
212+
{
213+
return Ok(());
214+
}
215+
193216
check_syscall_retval(
194217
// SAFETY: upheld by the caller.
195218
unsafe { libc::munmap(self.start_addr.as_ptr(), super::mapping_size()) },

0 commit comments

Comments
 (0)