Skip to content

Commit fedbeb2

Browse files
committed
test: add reading test for process context
1 parent b834f84 commit fedbeb2

1 file changed

Lines changed: 156 additions & 0 deletions

File tree

libdd-library-config/src/process_context.rs

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,4 +361,160 @@ pub mod linux {
361361

362362
Ok(())
363363
}
364+
365+
#[cfg(test)]
366+
mod tests {
367+
use super::MappingHeader;
368+
use anyhow::ensure;
369+
use std::{
370+
fs::File,
371+
io::{BufRead, BufReader},
372+
sync::atomic::{AtomicU64, Ordering},
373+
};
374+
375+
/// Parses the start address from a /proc/self/maps line
376+
fn parse_mapping_start(line: &str) -> Option<usize> {
377+
usize::from_str_radix(line.split('-').next()?, 16).ok()
378+
}
379+
380+
/// Parses start and end addresses from a /proc/self/maps line
381+
fn parse_mapping_range(line: &str) -> Option<(usize, usize)> {
382+
let mut parts = line.split_whitespace();
383+
let range = parts.next()?;
384+
let mut addrs = range.split('-');
385+
let start = usize::from_str_radix(addrs.next()?, 16).ok()?;
386+
let end = usize::from_str_radix(addrs.next()?, 16).ok()?;
387+
388+
Some((start, end))
389+
}
390+
391+
/// Checks if a /proc/self/maps line is a potential OTEL_CTX mapping
392+
///
393+
/// Accepts both read-only (" r--p ") and read-write (" rw-p "/" rw-s ") permissions
394+
/// for backwards compatibility (old writers used read-only, new writers use read-write).
395+
/// memfd mappings use MAP_SHARED so they show as " rw-s ".
396+
///
397+
/// Also accepts both 1-page (new) and 2-page (old) mapping sizes.
398+
fn is_otel_mapping_candidate(line: &str) -> bool {
399+
// Accept read-write private (anon), and read-write shared (memfd)
400+
if !(line.contains(" rw-p ") || line.contains(" rw-s ")) {
401+
return false;
402+
}
403+
404+
// Check that the size matches
405+
parse_mapping_range(line)
406+
.map(|(start, end)| end.saturating_sub(start) == super::mapping_size())
407+
.unwrap_or(false)
408+
}
409+
410+
/// Checks if a mapping line refers to the OTEL_CTX mapping by name
411+
///
412+
/// Handles both anonymous naming (`[anon:OTEL_CTX]`) and memfd naming
413+
/// (`/memfd:OTEL_CTX` which may have ` (deleted)` suffix).
414+
fn is_named_otel_mapping(line: &str) -> bool {
415+
let trimmed = line.trim_end();
416+
trimmed.ends_with("[anon:OTEL_CTX]")
417+
|| trimmed.contains("/memfd:OTEL_CTX")
418+
|| trimmed.contains("memfd:OTEL_CTX")
419+
}
420+
421+
/// Checks if a mapping line refers to the OTEL_CTX memfd mapping
422+
fn is_memfd_otel_mapping(line: &str) -> bool {
423+
line.contains("/memfd:OTEL_CTX")
424+
}
425+
426+
/// Reads the signature from a memory address to verify it's an OTEL_CTX mapping. This also
427+
/// establish proper synchronization/memory ordering through atomics since the reader is
428+
/// the same process in this test setup.
429+
fn verify_signature_at(addr: usize) -> bool {
430+
let ptr: *mut u64 = std::ptr::with_exposed_provenance_mut(addr);
431+
// Safety: We're reading from our own process memory at an address
432+
// we found in /proc/self/maps. This should be safe as long as the
433+
// mapping exists and has read permissions.
434+
//
435+
// The atomic alignment constraints are checked during publication.
436+
let signature = unsafe { AtomicU64::from_ptr(ptr).load(Ordering::Acquire) };
437+
&signature.to_ne_bytes() == super::SIGNATURE
438+
}
439+
440+
/// Find the OTEL_CTX mapping in /proc/self/maps
441+
fn find_otel_mapping() -> anyhow::Result<usize> {
442+
let file = File::open("/proc/self/maps")?;
443+
let reader = BufReader::new(file);
444+
445+
for line in reader.lines() {
446+
let line = line?;
447+
448+
if !is_otel_mapping_candidate(&line) {
449+
continue;
450+
}
451+
452+
if is_named_otel_mapping(&line) {
453+
if let Some(addr) = parse_mapping_start(&line) {
454+
return Ok(addr);
455+
}
456+
}
457+
458+
if is_memfd_otel_mapping(&line) {
459+
if let Some(addr) = parse_mapping_start(&line) {
460+
return Ok(addr);
461+
}
462+
}
463+
464+
// For unnamed mappings, verify by reading the signature
465+
if let Some(addr) = parse_mapping_start(&line) {
466+
if verify_signature_at(addr) {
467+
return Ok(addr);
468+
}
469+
}
470+
}
471+
472+
Err(anyhow::anyhow!(
473+
"couldn't find the mapping of the process context"
474+
))
475+
}
476+
477+
/// Read the process context from the current process.
478+
///
479+
/// This searches `/proc/self/maps` for an OTEL_CTX mapping and decodes its contents.
480+
pub fn read_process_context() -> anyhow::Result<MappingHeader> {
481+
let mapping_addr = find_otel_mapping()?;
482+
let header_ptr = mapping_addr as *const MappingHeader;
483+
484+
// Note: verifying the signature ensures proper synchronization
485+
ensure!(
486+
verify_signature_at(mapping_addr),
487+
"verification of the signature failed"
488+
);
489+
490+
// Safety: we found this address in /proc/self/maps and verified the signature
491+
Ok(unsafe { std::ptr::read(header_ptr) })
492+
}
493+
494+
#[test]
495+
fn publish_then_read_context() {
496+
let payload = "example process context payload";
497+
498+
super::publish(payload.as_bytes().to_vec())
499+
.expect("couldn't publish the process context");
500+
let header = read_process_context().expect("couldn't read back the process contex");
501+
// Safety: the published context must have put valid bytes of size payload_size in the
502+
// context if the signature check succeded.
503+
let read_payload = unsafe {
504+
std::slice::from_raw_parts(header.payload_ptr, header.payload_size as usize)
505+
};
506+
507+
assert!(header.signature == *super::SIGNATURE, "wrong signature");
508+
assert!(
509+
header.version == super::PROCESS_CTX_VERSION,
510+
"wrong context version"
511+
);
512+
assert!(
513+
header.payload_size == payload.len() as u32,
514+
"wrong payload size"
515+
);
516+
assert!(header.published_at_ns > 0, "published_at_ns is zero");
517+
assert!(read_payload == payload.as_bytes(), "payload mismatch");
518+
}
519+
}
364520
}

0 commit comments

Comments
 (0)