1use std::{
10 collections::{BTreeMap, BTreeSet},
11 fs::{self, File, OpenOptions},
12 path::Path,
13 sync::atomic::{AtomicU8, AtomicU64, Ordering},
14};
15
16use memmap2::{Mmap, MmapMut, MmapOptions};
17use serde::{Deserialize, Serialize};
18
19use crate::rust_runtime::{RustProbeObservation, valid_probe_id};
20
21pub const RUST_TRANSPORT_ENV: &str = "SUPERCOV_RUST_TRANSPORT_FILE";
22pub const RUST_TRANSPORT_TOKEN_ENV: &str = "SUPERCOV_RUST_TRANSPORT_TOKEN";
23pub const RUST_CONTEXT_ENV: &str = "SUPERCOV_RUST_CONTEXT_ID";
24
25const MAGIC: &[u8; 8] = b"SCVRUST3";
26const VERSION: u32 = 3;
27const HEADER_SIZE: usize = 128;
28const DESCRIPTOR_SIZE: usize = 40;
29const ENDIAN_MARKER: u32 = 0x0102_0304;
30const NEXT_DESCRIPTOR_OFFSET: usize = 32;
31const NEXT_PAYLOAD_OFFSET: usize = 40;
32const DROPPED_OFFSET: usize = 48;
33const TOKEN_OFFSET: usize = 56;
34const TOKEN_SIZE: usize = 16;
35const ATTACHMENTS_OFFSET: usize = 72;
36const NEXT_PHASE_OFFSET: usize = 80;
37
38const COMMIT_OFFSET: usize = 0;
39const KIND_OFFSET: usize = 1;
40const OUTCOME_OFFSET: usize = 2;
41const PID_OFFSET: usize = 4;
42const CONTEXT_OFFSET: usize = 8;
43const PAYLOAD_OFFSET_OFFSET: usize = 16;
44const PAYLOAD_LENGTH_OFFSET: usize = 20;
45const ID_LENGTH_OFFSET: usize = 24;
46const VALUE_LENGTH_OFFSET: usize = 28;
47const CHECKSUM_OFFSET: usize = 32;
48
49const KIND_HIT: u8 = 1;
50const KIND_DECISION: u8 = 2;
51const KIND_ORDINAL_HIT: u8 = 3;
52const KIND_PHASE: u8 = 4;
53const KIND_THREAD_PHASE: u8 = 5;
54const KIND_THREAD_END: u8 = 6;
55const KIND_TEST_BOUNDARY: u8 = 7;
56
57const RUNTIME_TEMPLATE: &str = include_str!("../runtime-assets/rust-mmap-runtime.rs");
58
59#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
60#[serde(rename_all = "camelCase", deny_unknown_fields)]
61pub struct RustTransportRead {
62 pub observations: Vec<RustTransportObservation>,
63 pub ordinal_hits: Vec<RustOrdinalHit>,
64 pub phases: Vec<RustPhaseContext>,
65 pub thread_phases: Vec<RustThreadPhase>,
66 pub thread_ends: Vec<RustThreadEnd>,
67 pub test_boundaries: Vec<RustTestBoundary>,
68 pub committed: u64,
69 pub incomplete: u64,
70 pub dropped: u64,
71 pub attachments: u64,
72}
73
74impl RustTransportRead {
75 pub fn empty() -> Self {
76 Self {
77 observations: Vec::new(),
78 ordinal_hits: Vec::new(),
79 phases: Vec::new(),
80 thread_phases: Vec::new(),
81 thread_ends: Vec::new(),
82 test_boundaries: Vec::new(),
83 committed: 0,
84 incomplete: 0,
85 dropped: 0,
86 attachments: 0,
87 }
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
92#[serde(rename_all = "camelCase", deny_unknown_fields)]
93pub struct RustTransportObservation {
94 pub process_id: u32,
95 pub context_id: u64,
96 pub observation: RustProbeObservation,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
100#[serde(rename_all = "camelCase", deny_unknown_fields)]
101pub struct RustOrdinalHit {
102 pub process_id: u32,
103 pub context_id: u64,
104 pub ordinal: u64,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
108#[serde(rename_all = "camelCase", deny_unknown_fields)]
109pub struct RustPhaseContext {
110 pub process_id: u32,
111 pub child_context_id: u64,
112 pub parent_context_id: u64,
113 pub invocation_nonce: u64,
114 pub decision_id: String,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
121#[serde(rename_all = "camelCase", deny_unknown_fields)]
122pub struct RustThreadPhase {
123 pub process_id: u32,
124 pub child_context_id: u64,
125 pub parent_context_id: u64,
126 pub invocation_nonce: u64,
127 pub commit_index: u64,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
132#[serde(rename_all = "camelCase", deny_unknown_fields)]
133pub struct RustThreadEnd {
134 pub process_id: u32,
135 pub context_id: u64,
136 pub commit_index: u64,
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
141#[serde(rename_all = "camelCase", deny_unknown_fields)]
142pub struct RustTestBoundary {
143 pub process_id: u32,
144 pub context_id: u64,
145 pub commit_index: u64,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct RustTransportPartition {
150 pub attributed: BTreeMap<u64, RustTransportRead>,
151 pub background: RustTransportRead,
152 pub thread_scope_limitations: BTreeSet<String>,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub enum RustTransportError {
159 Io(String),
160 UnsafeFile(String),
161 InvalidHeader,
162 InvalidLength,
163 InvalidDescriptor(u64),
164 InvalidRecord(u64),
165 InvalidAssertionContext(String),
166 InvalidAttribution(String),
167}
168
169impl std::fmt::Display for RustTransportError {
170 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 match self {
172 Self::Io(error) => write!(formatter, "Rust transport I/O failed: {error}"),
173 Self::UnsafeFile(path) => write!(formatter, "unsafe Rust transport file: {path}"),
174 Self::InvalidHeader => write!(formatter, "invalid Rust transport header"),
175 Self::InvalidLength => write!(formatter, "invalid Rust transport length"),
176 Self::InvalidDescriptor(index) => {
177 write!(formatter, "invalid Rust transport descriptor {index}")
178 }
179 Self::InvalidRecord(index) => {
180 write!(formatter, "invalid Rust transport record {index}")
181 }
182 Self::InvalidAssertionContext(reason) => {
183 write!(formatter, "invalid Rust assertion context: {reason}")
184 }
185 Self::InvalidAttribution(reason) => {
186 write!(formatter, "invalid Rust transport attribution: {reason}")
187 }
188 }
189 }
190}
191
192impl std::error::Error for RustTransportError {}
193
194pub fn rust_assertion_context_id(
195 parent: u64,
196 decision_id: &str,
197 invocation_nonce: u64,
198) -> Result<u64, RustTransportError> {
199 if parent == 0 {
200 return Ok(0);
201 }
202 if parent == u64::MAX {
203 return Err(RustTransportError::InvalidAssertionContext(
204 "the reserved nesting sentinel cannot be a parent".into(),
205 ));
206 }
207 let digest = decision_id
208 .strip_prefix("rs:decision:")
209 .filter(|digest| digest.len() == 24 && digest.bytes().all(|byte| byte.is_ascii_hexdigit()))
210 .ok_or_else(|| {
211 RustTransportError::InvalidAssertionContext(format!(
212 "invalid decision ID {decision_id}"
213 ))
214 })?;
215 let id_high = u64::from_str_radix(&digest[..16], 16).map_err(|error| {
216 RustTransportError::InvalidAssertionContext(format!(
217 "invalid decision ID {decision_id}: {error}"
218 ))
219 })?;
220 let id_low = u32::from_str_radix(&digest[16..], 16).map_err(|error| {
221 RustTransportError::InvalidAssertionContext(format!(
222 "invalid decision ID {decision_id}: {error}"
223 ))
224 })?;
225 let mut value = 0xcbf2_9ce4_8422_2325_u64;
226 for byte in b"supercov-rust-assertion-phase-v2"
227 .iter()
228 .copied()
229 .chain(parent.to_le_bytes())
230 .chain(id_high.to_le_bytes())
231 .chain(id_low.to_le_bytes())
232 .chain(invocation_nonce.to_le_bytes())
233 {
234 value ^= u64::from(byte);
235 value = value.wrapping_mul(0x0000_0100_0000_01b3);
236 }
237 Ok(if matches!(value, 0 | u64::MAX) {
238 value ^ 0xa5a5_5a5a_d3c3_b4b4
239 } else {
240 value
241 })
242}
243
244pub fn rust_thread_context_id(parent: u64, invocation_nonce: u64) -> u64 {
248 let mut value = 0xcbf2_9ce4_8422_2325_u64;
249 for byte in b"supercov-rust-thread-phase-v1\0"
250 .iter()
251 .copied()
252 .chain(parent.to_le_bytes())
253 .chain(invocation_nonce.to_le_bytes())
254 {
255 value ^= u64::from(byte);
256 value = value.wrapping_mul(0x0000_0100_0000_01b3);
257 }
258 if matches!(value, 0 | u64::MAX) {
259 value ^ 0xa5a5_5a5a_d3c3_b4b4
260 } else {
261 value
262 }
263}
264
265fn put_u32(target: &mut [u8], offset: usize, value: u32) {
266 target[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
267}
268
269fn get_u32(source: &[u8], offset: usize) -> Option<u32> {
270 Some(u32::from_le_bytes(
271 source.get(offset..offset + 4)?.try_into().ok()?,
272 ))
273}
274
275fn get_u64(source: &[u8], offset: usize) -> Option<u64> {
276 Some(u64::from_le_bytes(
277 source.get(offset..offset + 8)?.try_into().ok()?,
278 ))
279}
280
281fn le_u64(field: &[u8]) -> u64 {
286 u64::from_le_bytes(
287 field
288 .try_into()
289 .expect("the record was checked to hold this field"),
290 )
291}
292
293fn bounded_u32(source: &[u8], offset: usize) -> u32 {
294 get_u32(source, offset).expect("the file was checked to hold this field")
295}
296
297fn bounded_u64(source: &[u8], offset: usize) -> u64 {
298 get_u64(source, offset).expect("the file was checked to hold this field")
299}
300
301fn total_size(descriptors: u32, payload: u32) -> Option<usize> {
302 HEADER_SIZE
303 .checked_add(
304 usize::try_from(descriptors)
305 .ok()?
306 .checked_mul(DESCRIPTOR_SIZE)?,
307 )?
308 .checked_add(usize::try_from(payload).ok()?)
309}
310
311fn map_mut(file: &File) -> Result<MmapMut, RustTransportError> {
312 unsafe { MmapOptions::new().map_mut(file) }
315 .map_err(|error| RustTransportError::Io(error.to_string()))
316}
317
318fn map(file: &File) -> Result<Mmap, RustTransportError> {
319 unsafe { MmapOptions::new().map(file) }
321 .map_err(|error| RustTransportError::Io(error.to_string()))
322}
323
324pub fn create_rust_transport(
325 path: &Path,
326 token: [u8; TOKEN_SIZE],
327 descriptor_capacity: u32,
328 payload_capacity: u32,
329) -> Result<(), RustTransportError> {
330 if descriptor_capacity == 0 || payload_capacity == 0 {
331 return Err(RustTransportError::InvalidLength);
332 }
333 let total = total_size(descriptor_capacity, payload_capacity)
334 .ok_or(RustTransportError::InvalidLength)?;
335 let mut options = OpenOptions::new();
336 options.read(true).write(true).create_new(true);
337 #[cfg(unix)]
338 {
339 use std::os::unix::fs::OpenOptionsExt as _;
340 options.mode(0o600);
341 }
342 let file = options
343 .open(path)
344 .map_err(|error| RustTransportError::Io(error.to_string()))?;
345 file.set_len(u64::try_from(total).map_err(|_| RustTransportError::InvalidLength)?)
346 .map_err(|error| RustTransportError::Io(error.to_string()))?;
347 let mut mapping = map_mut(&file)?;
348 mapping[..MAGIC.len()].copy_from_slice(MAGIC);
349 put_u32(&mut mapping, 8, VERSION);
350 put_u32(&mut mapping, 12, HEADER_SIZE as u32);
351 put_u32(&mut mapping, 16, DESCRIPTOR_SIZE as u32);
352 put_u32(&mut mapping, 20, descriptor_capacity);
353 put_u32(&mut mapping, 24, payload_capacity);
354 put_u32(&mut mapping, 28, ENDIAN_MARKER);
355 mapping[TOKEN_OFFSET..TOKEN_OFFSET + TOKEN_SIZE].copy_from_slice(&token);
356 mapping
357 .flush_range(0, HEADER_SIZE)
358 .map_err(|error| RustTransportError::Io(error.to_string()))
359}
360
361fn regular_file(path: &Path) -> Result<File, RustTransportError> {
362 let metadata =
363 fs::symlink_metadata(path).map_err(|error| RustTransportError::Io(error.to_string()))?;
364 if !metadata.file_type().is_file() {
365 return Err(RustTransportError::UnsafeFile(path.display().to_string()));
366 }
367 let mut options = OpenOptions::new();
368 options.read(true);
369 #[cfg(any(target_os = "linux", target_os = "macos"))]
370 {
371 use std::os::unix::fs::OpenOptionsExt as _;
372 #[cfg(target_os = "linux")]
373 const O_NOFOLLOW: i32 = 0x2_0000;
374 #[cfg(target_os = "macos")]
375 const O_NOFOLLOW: i32 = 0x100;
376 options.custom_flags(O_NOFOLLOW);
377 }
378 let file = options
379 .open(path)
380 .map_err(|error| RustTransportError::Io(error.to_string()))?;
381 if !file
382 .metadata()
383 .map_err(|error| RustTransportError::Io(error.to_string()))?
384 .file_type()
385 .is_file()
386 {
387 return Err(RustTransportError::UnsafeFile(path.display().to_string()));
388 }
389 Ok(file)
390}
391
392fn atomic_u64(mapping: &[u8], offset: usize) -> Result<&AtomicU64, RustTransportError> {
393 let pointer = mapping
394 .get(offset..offset + 8)
395 .ok_or(RustTransportError::InvalidHeader)?
396 .as_ptr();
397 if !(pointer as usize).is_multiple_of(std::mem::align_of::<AtomicU64>()) {
398 return Err(RustTransportError::InvalidHeader);
399 }
400 Ok(unsafe { &*pointer.cast::<AtomicU64>() })
403}
404
405fn atomic_u8(mapping: &[u8], offset: usize) -> Result<&AtomicU8, RustTransportError> {
406 let pointer = mapping
407 .get(offset)
408 .ok_or(RustTransportError::InvalidLength)? as *const u8;
409 Ok(unsafe { &*pointer.cast::<AtomicU8>() })
411}
412
413#[allow(clippy::too_many_arguments)]
414fn checksum(
415 kind: u8,
416 outcome: u8,
417 pid: u32,
418 context: u64,
419 payload_offset: u32,
420 payload_length: u32,
421 id_length: u32,
422 value_length: u32,
423 id: &[u8],
424 values: &[u8],
425) -> u64 {
426 let mut value = 0xcbf2_9ce4_8422_2325_u64;
427 for byte in [kind, outcome]
428 .into_iter()
429 .chain(pid.to_le_bytes())
430 .chain(context.to_le_bytes())
431 .chain(payload_offset.to_le_bytes())
432 .chain(payload_length.to_le_bytes())
433 .chain(id_length.to_le_bytes())
434 .chain(value_length.to_le_bytes())
435 .chain(id.iter().copied())
436 .chain(values.iter().copied())
437 {
438 value ^= u64::from(byte);
439 value = value.wrapping_mul(0x0000_0100_0000_01b3);
440 }
441 value
442}
443
444pub fn read_rust_transport(
445 path: &Path,
446 expected_token: &[u8; TOKEN_SIZE],
447) -> Result<RustTransportRead, RustTransportError> {
448 let file = regular_file(path)?;
449 let mapping = map(&file)?;
450 if mapping.get(..8) != Some(MAGIC.as_slice())
451 || get_u32(&mapping, 8) != Some(VERSION)
452 || get_u32(&mapping, 12) != Some(HEADER_SIZE as u32)
453 || get_u32(&mapping, 16) != Some(DESCRIPTOR_SIZE as u32)
454 || get_u32(&mapping, 28) != Some(ENDIAN_MARKER)
455 || mapping.get(TOKEN_OFFSET..TOKEN_OFFSET + TOKEN_SIZE) != Some(expected_token.as_slice())
456 || mapping.get(52..56).is_none_or(|bytes| bytes != [0; 4])
457 || mapping
458 .get(NEXT_PHASE_OFFSET + 8..HEADER_SIZE)
459 .is_none_or(|bytes| bytes != [0; 40])
460 {
461 return Err(RustTransportError::InvalidHeader);
462 }
463 let descriptors = bounded_u32(&mapping, 20);
464 let payload_capacity = bounded_u32(&mapping, 24);
465 if descriptors == 0 || payload_capacity == 0 {
466 return Err(RustTransportError::InvalidHeader);
467 }
468 if mapping.len()
469 != total_size(descriptors, payload_capacity).ok_or(RustTransportError::InvalidLength)?
470 {
471 return Err(RustTransportError::InvalidLength);
472 }
473 let next = atomic_u64(&mapping, NEXT_DESCRIPTOR_OFFSET)?.load(Ordering::Acquire);
474 let next_payload = atomic_u64(&mapping, NEXT_PAYLOAD_OFFSET)?.load(Ordering::Acquire);
475 let recorded_dropped = atomic_u64(&mapping, DROPPED_OFFSET)?.load(Ordering::Acquire);
476 let attachments = atomic_u64(&mapping, ATTACHMENTS_OFFSET)?.load(Ordering::Acquire);
477 let inspect = next.min(u64::from(descriptors));
478 let payload_base = HEADER_SIZE + descriptors as usize * DESCRIPTOR_SIZE;
479 let mut observations = Vec::new();
480 let mut ordinal_hits = Vec::new();
481 let mut phases = Vec::new();
482 let mut thread_phases = Vec::new();
483 let mut thread_ends = Vec::new();
484 let mut test_boundaries = Vec::new();
485 let mut phase_definitions = BTreeMap::<u64, (u8, u64, u64, String)>::new();
486 let mut thread_end_contexts = BTreeSet::<u64>::new();
487 let mut boundary_contexts = BTreeSet::<u64>::new();
488 let mut committed = 0_u64;
489 for index in 0..inspect {
490 let descriptor = HEADER_SIZE + index as usize * DESCRIPTOR_SIZE;
491 match atomic_u8(&mapping, descriptor + COMMIT_OFFSET)?.load(Ordering::Acquire) {
492 0 => continue,
493 1 => {}
494 _ => return Err(RustTransportError::InvalidDescriptor(index)),
495 }
496 committed += 1;
497 let kind = mapping[descriptor + KIND_OFFSET];
498 let outcome = mapping[descriptor + OUTCOME_OFFSET];
499 if mapping[descriptor + 3] != 0 {
500 return Err(RustTransportError::InvalidDescriptor(index));
501 }
502 let pid = bounded_u32(&mapping, descriptor + PID_OFFSET);
503 let context_id = bounded_u64(&mapping, descriptor + CONTEXT_OFFSET);
504 let payload_offset = bounded_u32(&mapping, descriptor + PAYLOAD_OFFSET_OFFSET) as usize;
505 let payload_length = bounded_u32(&mapping, descriptor + PAYLOAD_LENGTH_OFFSET) as usize;
506 let id_length = bounded_u32(&mapping, descriptor + ID_LENGTH_OFFSET) as usize;
507 let value_length = bounded_u32(&mapping, descriptor + VALUE_LENGTH_OFFSET) as usize;
508 let expected_checksum = bounded_u64(&mapping, descriptor + CHECKSUM_OFFSET);
509 let end = payload_offset
510 .checked_add(payload_length)
511 .filter(|end| *end <= payload_capacity as usize)
512 .ok_or(RustTransportError::InvalidDescriptor(index))?;
513 if payload_length != id_length.saturating_add(value_length)
514 || u64::from(end as u32) > next_payload
515 {
516 return Err(RustTransportError::InvalidDescriptor(index));
517 }
518 let payload = &mapping[payload_base + payload_offset..payload_base + end];
519 let (id, values) = payload.split_at(id_length);
520 if checksum(
521 kind,
522 outcome,
523 pid,
524 context_id,
525 payload_offset as u32,
526 payload_length as u32,
527 id_length as u32,
528 value_length as u32,
529 id,
530 values,
531 ) != expected_checksum
532 {
533 return Err(RustTransportError::InvalidRecord(index));
534 }
535 let id = std::str::from_utf8(id).map_err(|_| RustTransportError::InvalidRecord(index))?;
536 if matches!(kind, KIND_HIT | KIND_DECISION | KIND_PHASE) && !valid_probe_id(id) {
537 return Err(RustTransportError::InvalidRecord(index));
538 }
539 match kind {
540 KIND_HIT if outcome == 0 && values.is_empty() => {
541 observations.push(RustTransportObservation {
542 process_id: pid,
543 context_id,
544 observation: RustProbeObservation::Hit { id: id.into() },
545 });
546 }
547 KIND_DECISION
548 if matches!(outcome, 0 | 1)
549 && id.starts_with("rs:decision:")
550 && !values.is_empty()
551 && values.iter().all(|value| matches!(*value, 0..=2)) =>
552 {
553 observations.push(RustTransportObservation {
554 process_id: pid,
555 context_id,
556 observation: RustProbeObservation::Decision {
557 id: id.into(),
558 values: values
559 .iter()
560 .map(|value| match value {
561 0 => None,
562 1 => Some(false),
563 2 => Some(true),
564 _ => unreachable!(),
565 })
566 .collect(),
567 outcome: outcome == 1,
568 },
569 });
570 }
571 KIND_ORDINAL_HIT if outcome == 0 && id.is_empty() && values.len() == 8 => {
572 ordinal_hits.push(RustOrdinalHit {
573 process_id: pid,
574 context_id,
575 ordinal: le_u64(values),
576 });
577 }
578 KIND_PHASE
579 if outcome == 0
580 && id.starts_with("rs:decision:")
581 && values.len() == 16
582 && !matches!(context_id, 0 | u64::MAX) =>
583 {
584 let parent_context_id = le_u64(&values[..8]);
585 let invocation_nonce = le_u64(&values[8..]);
586 if matches!(parent_context_id, 0 | u64::MAX)
587 || rust_assertion_context_id(parent_context_id, id, invocation_nonce)?
588 != context_id
589 {
590 return Err(RustTransportError::InvalidAssertionContext(format!(
591 "phase record {index} does not derive child {context_id:016x} from parent {parent_context_id:016x} and {id}"
592 )));
593 }
594 let definition = (
595 KIND_PHASE,
596 parent_context_id,
597 invocation_nonce,
598 id.to_owned(),
599 );
600 if phase_definitions
601 .insert(context_id, definition.clone())
602 .is_some_and(|existing| existing != definition)
603 {
604 return Err(RustTransportError::InvalidAssertionContext(format!(
605 "child {context_id:016x} has conflicting phase definitions"
606 )));
607 }
608 phases.push(RustPhaseContext {
609 process_id: pid,
610 child_context_id: context_id,
611 parent_context_id,
612 invocation_nonce,
613 decision_id: id.into(),
614 });
615 }
616 KIND_THREAD_PHASE
617 if outcome == 0
618 && id.is_empty()
619 && values.len() == 16
620 && !matches!(context_id, 0 | u64::MAX) =>
621 {
622 let parent_context_id = le_u64(&values[..8]);
623 let invocation_nonce = le_u64(&values[8..]);
624 if matches!(parent_context_id, 0 | u64::MAX)
625 || rust_thread_context_id(parent_context_id, invocation_nonce) != context_id
626 {
627 return Err(RustTransportError::InvalidAssertionContext(format!(
628 "thread phase record {index} does not derive child {context_id:016x} from parent {parent_context_id:016x} and nonce {invocation_nonce}"
629 )));
630 }
631 let definition = (
632 KIND_THREAD_PHASE,
633 parent_context_id,
634 invocation_nonce,
635 String::new(),
636 );
637 if phase_definitions
638 .insert(context_id, definition.clone())
639 .is_some_and(|existing| existing != definition)
640 {
641 return Err(RustTransportError::InvalidAssertionContext(format!(
642 "child {context_id:016x} has conflicting phase definitions"
643 )));
644 }
645 thread_phases.push(RustThreadPhase {
646 process_id: pid,
647 child_context_id: context_id,
648 parent_context_id,
649 invocation_nonce,
650 commit_index: index,
651 });
652 }
653 KIND_THREAD_END
654 if outcome == 0
655 && id.is_empty()
656 && values.is_empty()
657 && !matches!(context_id, 0 | u64::MAX) =>
658 {
659 if !thread_end_contexts.insert(context_id) {
660 return Err(RustTransportError::InvalidRecord(index));
661 }
662 thread_ends.push(RustThreadEnd {
663 process_id: pid,
664 context_id,
665 commit_index: index,
666 });
667 }
668 KIND_TEST_BOUNDARY
669 if outcome == 0
670 && id.is_empty()
671 && values.is_empty()
672 && !matches!(context_id, 0 | u64::MAX) =>
673 {
674 if !boundary_contexts.insert(context_id) {
675 return Err(RustTransportError::InvalidRecord(index));
676 }
677 test_boundaries.push(RustTestBoundary {
678 process_id: pid,
679 context_id,
680 commit_index: index,
681 });
682 }
683 _ => return Err(RustTransportError::InvalidRecord(index)),
684 }
685 }
686 let overflow = next.saturating_sub(u64::from(descriptors));
687 Ok(RustTransportRead {
688 observations,
689 ordinal_hits,
690 phases,
691 thread_phases,
692 thread_ends,
693 test_boundaries,
694 committed,
695 incomplete: inspect.saturating_sub(committed),
696 dropped: recorded_dropped.max(overflow),
697 attachments,
698 })
699}
700
701pub fn validate_rust_phase_contexts(
702 base_context_id: u64,
703 read: &RustTransportRead,
704) -> Result<(), RustTransportError> {
705 if matches!(base_context_id, 0 | u64::MAX) {
706 return Err(RustTransportError::InvalidAssertionContext(
707 "the supervisor base context must be nonzero and not reserved".into(),
708 ));
709 }
710 let mut definitions = BTreeMap::<u64, (u8, u64, u64, &str)>::new();
711 for phase in &read.phases {
712 let definition = (
713 KIND_PHASE,
714 phase.parent_context_id,
715 phase.invocation_nonce,
716 phase.decision_id.as_str(),
717 );
718 if definitions
719 .insert(phase.child_context_id, definition)
720 .is_some_and(|existing| existing != definition)
721 {
722 return Err(RustTransportError::InvalidAssertionContext(format!(
723 "child {:016x} has conflicting phase definitions",
724 phase.child_context_id
725 )));
726 }
727 }
728 for phase in &read.thread_phases {
729 if rust_thread_context_id(phase.parent_context_id, phase.invocation_nonce)
730 != phase.child_context_id
731 {
732 return Err(RustTransportError::InvalidAssertionContext(format!(
733 "thread phase child {:016x} does not derive from parent {:016x} and nonce {}",
734 phase.child_context_id, phase.parent_context_id, phase.invocation_nonce
735 )));
736 }
737 let definition = (
738 KIND_THREAD_PHASE,
739 phase.parent_context_id,
740 phase.invocation_nonce,
741 "",
742 );
743 if definitions
744 .insert(phase.child_context_id, definition)
745 .is_some_and(|existing| existing != definition)
746 {
747 return Err(RustTransportError::InvalidAssertionContext(format!(
748 "child {:016x} has conflicting phase definitions",
749 phase.child_context_id
750 )));
751 }
752 }
753
754 let used_contexts = read
755 .observations
756 .iter()
757 .map(|observation| observation.context_id)
758 .chain(read.ordinal_hits.iter().map(|hit| hit.context_id))
759 .chain(read.thread_ends.iter().map(|end| end.context_id))
760 .chain(
761 read.test_boundaries
762 .iter()
763 .map(|boundary| boundary.context_id),
764 )
765 .filter(|context| *context != 0 && *context != base_context_id)
766 .collect::<BTreeSet<_>>();
767 for start in used_contexts {
768 let mut context = start;
769 let mut path = BTreeSet::new();
770 while context != base_context_id {
771 if !path.insert(context) {
772 return Err(RustTransportError::InvalidAssertionContext(format!(
773 "phase context cycle at {context:016x}"
774 )));
775 }
776 let (_, parent, _, _) = definitions.get(&context).ok_or_else(|| {
777 RustTransportError::InvalidAssertionContext(format!(
778 "context {context:016x} does not resolve to base {base_context_id:016x}"
779 ))
780 })?;
781 context = *parent;
782 if matches!(context, 0 | u64::MAX) {
783 return Err(RustTransportError::InvalidAssertionContext(format!(
784 "context {start:016x} crosses the supervisor attempt boundary"
785 )));
786 }
787 }
788 }
789 Ok(())
790}
791
792pub fn partition_rust_transport_by_test_contexts(
796 read: &RustTransportRead,
797 base_contexts: &BTreeSet<u64>,
798) -> Result<RustTransportPartition, RustTransportError> {
799 if base_contexts
800 .iter()
801 .any(|context| matches!(*context, 0 | u64::MAX))
802 {
803 return Err(RustTransportError::InvalidAttribution(
804 "known test roots must be nonzero and non-reserved".into(),
805 ));
806 }
807 if read.incomplete != 0 || read.dropped != 0 {
808 return Err(RustTransportError::InvalidAttribution(format!(
809 "shared transport is incomplete (incomplete={}, dropped={})",
810 read.incomplete, read.dropped
811 )));
812 }
813
814 let mut parents = BTreeMap::<u64, u64>::new();
815 let mut thread_children = BTreeSet::<u64>::new();
816 for (child, parent) in read
817 .phases
818 .iter()
819 .map(|phase| (phase.child_context_id, phase.parent_context_id))
820 .chain(
821 read.thread_phases
822 .iter()
823 .map(|phase| (phase.child_context_id, phase.parent_context_id)),
824 )
825 {
826 if matches!(child, 0 | u64::MAX)
827 || matches!(parent, 0 | u64::MAX)
828 || base_contexts.contains(&child)
829 || parents.insert(child, parent).is_some()
830 {
831 return Err(RustTransportError::InvalidAttribution(format!(
832 "phase {child:016x} has an invalid or repeated ownership definition"
833 )));
834 }
835 }
836 for phase in &read.thread_phases {
837 thread_children.insert(phase.child_context_id);
838 }
839 let mut thread_end_index = BTreeMap::<u64, u64>::new();
840 for end in &read.thread_ends {
841 if !thread_children.contains(&end.context_id)
842 || thread_end_index
843 .insert(end.context_id, end.commit_index)
844 .is_some()
845 {
846 return Err(RustTransportError::InvalidAttribution(format!(
847 "thread end {:016x} has no unique thread-phase definition",
848 end.context_id
849 )));
850 }
851 }
852 let mut boundary_index = BTreeMap::<u64, u64>::new();
853 for boundary in &read.test_boundaries {
854 if !base_contexts.contains(&boundary.context_id)
855 || boundary_index
856 .insert(boundary.context_id, boundary.commit_index)
857 .is_some()
858 {
859 return Err(RustTransportError::InvalidAttribution(format!(
860 "test boundary {:016x} does not identify one known test root",
861 boundary.context_id
862 )));
863 }
864 }
865
866 enum Resolution {
869 Attributed(u64),
870 Background,
871 Quarantined { root: u64, escaped: Vec<u64> },
872 }
873 let resolve = |start: u64| -> Result<Resolution, RustTransportError> {
874 if start == 0 {
875 return Ok(Resolution::Background);
876 }
877 if start == u64::MAX {
878 return Err(RustTransportError::InvalidAttribution(
879 "evidence used the reserved context sentinel".into(),
880 ));
881 }
882 let mut context = start;
883 let mut seen = BTreeSet::new();
884 let mut chain_threads = Vec::new();
885 loop {
886 if thread_children.contains(&context) {
887 chain_threads.push(context);
888 }
889 let Some(parent) = parents.get(&context) else {
890 break;
891 };
892 if !seen.insert(context) {
893 return Err(RustTransportError::InvalidAttribution(format!(
894 "phase context cycle at {context:016x}"
895 )));
896 }
897 context = *parent;
898 }
899 if !base_contexts.contains(&context) {
900 return Err(RustTransportError::InvalidAttribution(format!(
901 "context {start:016x} resolves to unknown test root {context:016x}"
902 )));
903 }
904 let boundary = boundary_index.get(&context).copied();
905 let escaped = chain_threads
906 .into_iter()
907 .filter(|thread| {
908 !matches!(
909 (thread_end_index.get(thread), boundary),
910 (Some(end), Some(boundary)) if *end < boundary
911 )
912 })
913 .collect::<Vec<_>>();
914 if escaped.is_empty() {
915 Ok(Resolution::Attributed(context))
916 } else {
917 Ok(Resolution::Quarantined {
918 root: context,
919 escaped,
920 })
921 }
922 };
923
924 let mut attributed = base_contexts
925 .iter()
926 .map(|context| (*context, RustTransportRead::empty()))
927 .collect::<BTreeMap<_, _>>();
928 let mut background = RustTransportRead::empty();
929 background.attachments = read.attachments;
930 let mut thread_scope_limitations = BTreeSet::new();
931 let mut note_escape = |root: u64, escaped: Vec<u64>| {
932 for thread in escaped {
933 thread_scope_limitations.insert(format!(
934 "RUST_THREAD_OUTLIVED_TEST: thread phase {thread:016x} escaped test {root:016x}"
935 ));
936 }
937 };
938
939 for observation in &read.observations {
940 match resolve(observation.context_id)? {
941 Resolution::Attributed(root) => attributed
942 .get_mut(&root)
943 .expect("resolved root was preallocated")
944 .observations
945 .push(observation.clone()),
946 Resolution::Background => background.observations.push(observation.clone()),
947 Resolution::Quarantined { root, escaped } => {
948 note_escape(root, escaped);
949 background.observations.push(observation.clone());
950 }
951 }
952 }
953 for hit in &read.ordinal_hits {
954 match resolve(hit.context_id)? {
955 Resolution::Attributed(root) => attributed
956 .get_mut(&root)
957 .expect("resolved root was preallocated")
958 .ordinal_hits
959 .push(*hit),
960 Resolution::Background => background.ordinal_hits.push(*hit),
961 Resolution::Quarantined { root, escaped } => {
962 note_escape(root, escaped);
963 background.ordinal_hits.push(*hit);
964 }
965 }
966 }
967 for phase in &read.phases {
968 match resolve(phase.child_context_id)? {
969 Resolution::Attributed(root) => attributed
970 .get_mut(&root)
971 .expect("resolved root was preallocated")
972 .phases
973 .push(phase.clone()),
974 Resolution::Background => {
975 return Err(RustTransportError::InvalidAttribution(format!(
976 "phase {:016x} resolved to background",
977 phase.child_context_id
978 )));
979 }
980 Resolution::Quarantined { root, escaped } => {
981 note_escape(root, escaped);
982 background.phases.push(phase.clone());
983 }
984 }
985 }
986 for phase in &read.thread_phases {
987 match resolve(phase.child_context_id)? {
988 Resolution::Attributed(root) => attributed
989 .get_mut(&root)
990 .expect("resolved root was preallocated")
991 .thread_phases
992 .push(*phase),
993 Resolution::Background => {
994 return Err(RustTransportError::InvalidAttribution(format!(
995 "thread phase {:016x} resolved to background",
996 phase.child_context_id
997 )));
998 }
999 Resolution::Quarantined { root, escaped } => {
1000 note_escape(root, escaped);
1001 background.thread_phases.push(*phase);
1002 }
1003 }
1004 }
1005 for end in &read.thread_ends {
1006 match resolve(end.context_id)? {
1007 Resolution::Attributed(root) => attributed
1008 .get_mut(&root)
1009 .expect("resolved root was preallocated")
1010 .thread_ends
1011 .push(*end),
1012 Resolution::Background => {
1013 return Err(RustTransportError::InvalidAttribution(format!(
1014 "thread end {:016x} resolved to background",
1015 end.context_id
1016 )));
1017 }
1018 Resolution::Quarantined { root, escaped } => {
1019 note_escape(root, escaped);
1020 background.thread_ends.push(*end);
1021 }
1022 }
1023 }
1024 for boundary in &read.test_boundaries {
1025 attributed
1026 .get_mut(&boundary.context_id)
1027 .expect("boundary contexts were checked against the known roots")
1028 .test_boundaries
1029 .push(*boundary);
1030 }
1031
1032 let set_committed = |transport: &mut RustTransportRead| -> Result<u64, RustTransportError> {
1033 transport.committed = u64::try_from(
1034 transport.observations.len()
1035 + transport.ordinal_hits.len()
1036 + transport.phases.len()
1037 + transport.thread_phases.len()
1038 + transport.thread_ends.len()
1039 + transport.test_boundaries.len(),
1040 )
1041 .map_err(|_| {
1042 RustTransportError::InvalidAttribution("partition record count exceeds u64".into())
1043 })?;
1044 Ok(transport.committed)
1045 };
1046 let mut assigned = set_committed(&mut background)?;
1047 for transport in attributed.values_mut() {
1048 assigned = assigned
1049 .checked_add(set_committed(transport)?)
1050 .ok_or_else(|| {
1051 RustTransportError::InvalidAttribution(
1052 "partition record count overflowed u64".into(),
1053 )
1054 })?;
1055 }
1056 if assigned != read.committed {
1057 return Err(RustTransportError::InvalidAttribution(format!(
1058 "partition assigned {assigned} of {} committed records",
1059 read.committed
1060 )));
1061 }
1062 for (base, transport) in &attributed {
1063 validate_rust_phase_contexts(*base, transport)?;
1064 }
1065 Ok(RustTransportPartition {
1066 attributed,
1067 background,
1068 thread_scope_limitations,
1069 })
1070}
1071
1072pub fn render_rust_mmap_runtime(module_name: &str) -> Result<String, String> {
1073 let valid_identifier = !module_name.is_empty()
1074 && module_name.bytes().enumerate().all(|(index, byte)| {
1075 byte == b'_' || byte.is_ascii_alphabetic() || (index > 0 && byte.is_ascii_digit())
1076 });
1077 if !valid_identifier {
1078 return Err("invalid Rust runtime module name".into());
1079 }
1080 Ok(RUNTIME_TEMPLATE.replace("__SUPERCOV_MODULE__", module_name))
1081}
1082
1083#[cfg(test)]
1084mod tests {
1085 use std::{
1086 fs,
1087 io::{BufRead as _, BufReader},
1088 process::{Command, Stdio},
1089 sync::atomic::{AtomicUsize, Ordering},
1090 time::{SystemTime, UNIX_EPOCH},
1091 };
1092
1093 use super::*;
1094
1095 const TOKEN: [u8; TOKEN_SIZE] = [0x42; TOKEN_SIZE];
1096 const CONTEXT: u64 = 42;
1097
1098 fn token_hex() -> String {
1099 TOKEN.iter().map(|byte| format!("{byte:02x}")).collect()
1100 }
1101
1102 fn temporary_directory(name: &str) -> std::path::PathBuf {
1103 let nonce = SystemTime::now()
1104 .duration_since(UNIX_EPOCH)
1105 .unwrap()
1106 .as_nanos();
1107 let path = std::env::temp_dir().join(format!(
1108 "supercov-rust-transport-{}-{nonce}-{name}",
1109 std::process::id()
1110 ));
1111 fs::create_dir(&path).unwrap();
1112 path
1113 }
1114
1115 fn compile_fixture(directory: &Path) -> std::path::PathBuf {
1116 let source = directory.join("main.rs");
1117 let binary = directory.join("program");
1118 let runtime = render_rust_mmap_runtime("__supercov_runtime_v1").unwrap();
1119 fs::write(
1120 &source,
1121 format!(
1122 r#"{runtime}
1123fn main() {{
1124 let mode = std::env::args().nth(1).unwrap_or_default();
1125 if mode == "threads" {{
1126 let mut threads = Vec::new();
1127 for _ in 0..8 {{
1128 threads.push(std::thread::spawn(|| {{
1129 for _ in 0..100 {{ __supercov_runtime_v1::hit("rs:statement:0123456789abcdef01234567"); }}
1130 }}));
1131 }}
1132 for thread in threads {{ thread.join().unwrap(); }}
1133 let mut frame = __supercov_runtime_v1::DecisionFrame::new("rs:decision:0123456789abcdef01234567", 2);
1134 let first = __supercov_runtime_v1::condition(true, &mut frame, 0);
1135 let second = __supercov_runtime_v1::condition(false, &mut frame, 1);
1136 __supercov_runtime_v1::decision(first && second, &mut frame);
1137 __supercov_runtime_v1::ordinal_hit(7);
1138 }} else if mode == "contexts" {{
1139 __supercov_runtime_v1::hit("rs:statement:0123456789abcdef01234567");
1140 let outer = __supercov_runtime_v1::enter_context(100);
1141 __supercov_runtime_v1::hit("rs:statement:0123456789abcdef01234567");
1142 let inner = __supercov_runtime_v1::enter_context(200);
1143 __supercov_runtime_v1::hit("rs:statement:0123456789abcdef01234567");
1144 __supercov_runtime_v1::exit_context(inner);
1145 __supercov_runtime_v1::hit("rs:statement:0123456789abcdef01234567");
1146 __supercov_runtime_v1::exit_context(outer);
1147 __supercov_runtime_v1::hit("rs:statement:0123456789abcdef01234567");
1148 let assertion = __supercov_runtime_v1::enter_assertion_context(
1149 0x0123_4567_89ab_cdef,
1150 0x0123_4567,
1151 );
1152 __supercov_runtime_v1::hit("rs:statement:0123456789abcdef01234567");
1153 __supercov_runtime_v1::exit_context(assertion);
1154 __supercov_runtime_v1::hit("rs:statement:0123456789abcdef01234567");
1155 let repeated_assertion = __supercov_runtime_v1::enter_assertion_context(
1156 0x0123_4567_89ab_cdef,
1157 0x0123_4567,
1158 );
1159 __supercov_runtime_v1::hit("rs:statement:0123456789abcdef01234567");
1160 __supercov_runtime_v1::exit_context(repeated_assertion);
1161 __supercov_runtime_v1::hit("rs:statement:0123456789abcdef01234567");
1162 }} else if mode == "mir-decisions" {{
1163 let before_outer = __supercov_runtime_v1::enter_context(901);
1164 let outer = __supercov_runtime_v1::mir_decision_start(
1165 0x0123_4567_89ab_cdef,
1166 0x0123_4567,
1167 2,
1168 );
1169 __supercov_runtime_v1::mir_decision_condition(outer, 0, true);
1170 let before_inner = __supercov_runtime_v1::enter_context(902);
1171 let inner = __supercov_runtime_v1::mir_decision_start(
1172 0xfedc_ba98_7654_3210,
1173 0xfedc_ba98,
1174 1,
1175 );
1176 let migrated = std::thread::spawn(move || {{
1177 __supercov_runtime_v1::mir_decision_condition(inner, 0, false);
1178 __supercov_runtime_v1::mir_decision_finish(inner, false);
1179 }});
1180 __supercov_runtime_v1::exit_context(before_inner);
1181 __supercov_runtime_v1::mir_decision_condition(outer, 1, true);
1182 __supercov_runtime_v1::exit_context(before_outer);
1183 __supercov_runtime_v1::mir_decision_finish(outer, true);
1184 migrated.join().unwrap();
1185 let before_branch = __supercov_runtime_v1::enter_context(903);
1186 let branch = __supercov_runtime_v1::mir_branch_start();
1187 __supercov_runtime_v1::exit_context(before_branch);
1188 let migrated_branch = std::thread::spawn(move || {{
1189 __supercov_runtime_v1::mir_branch_hit(branch, 777);
1190 __supercov_runtime_v1::mir_branch_hit(branch, 888);
1191 }});
1192 migrated_branch.join().unwrap();
1193 }} else if mode == "kill" {{
1194 __supercov_runtime_v1::hit("rs:function:fedcba9876543210fedcba98");
1195 let interrupted = __supercov_runtime_v1::mir_decision_start(
1196 0x1111_1111_1111_1111,
1197 0x2222_2222,
1198 2,
1199 );
1200 __supercov_runtime_v1::mir_decision_condition(interrupted, 0, true);
1201 let _interrupted_branch = __supercov_runtime_v1::mir_branch_start();
1202 println!("ready");
1203 use std::io::Write as _;
1204 std::io::stdout().flush().unwrap();
1205 std::thread::sleep(std::time::Duration::from_secs(30));
1206 }} else {{
1207 for _ in 0..3 {{ __supercov_runtime_v1::hit("rs:branch:aaaaaaaaaaaaaaaaaaaaaaaa"); }}
1208 }}
1209}}
1210"#
1211 ),
1212 )
1213 .unwrap();
1214 let output = Command::new("rustc")
1215 .args(["--edition=2024"])
1216 .arg(&source)
1217 .arg("-o")
1218 .arg(&binary)
1219 .output()
1220 .unwrap();
1221 assert!(
1222 output.status.success(),
1223 "{}",
1224 String::from_utf8_lossy(&output.stderr)
1225 );
1226 binary
1227 }
1228
1229 #[test]
1230 fn implementation_matches_frozen_transport_contract() {
1231 let contract = supercov_contracts::rust_probe_transport_v3_contract().unwrap();
1232 assert_eq!(MAGIC.as_slice(), contract.magic.as_bytes());
1233 assert_eq!(VERSION, contract.protocol_version);
1234 assert_eq!(HEADER_SIZE, contract.header_size);
1235 assert_eq!(DESCRIPTOR_SIZE, contract.descriptor_size);
1236 assert_eq!(TOKEN_SIZE, contract.token_size);
1237 assert_eq!(ENDIAN_MARKER, contract.endian_marker);
1238 assert_eq!(
1239 NEXT_DESCRIPTOR_OFFSET,
1240 contract.header_offsets.next_descriptor
1241 );
1242 assert_eq!(NEXT_PAYLOAD_OFFSET, contract.header_offsets.next_payload);
1243 assert_eq!(DROPPED_OFFSET, contract.header_offsets.dropped);
1244 assert_eq!(TOKEN_OFFSET, contract.header_offsets.token);
1245 assert_eq!(ATTACHMENTS_OFFSET, contract.header_offsets.attachments);
1246 assert_eq!(Some(NEXT_PHASE_OFFSET), contract.header_offsets.next_phase);
1247 assert_eq!(COMMIT_OFFSET, contract.descriptor_offsets.commit);
1248 assert_eq!(PID_OFFSET, contract.descriptor_offsets.process_id);
1249 assert_eq!(CONTEXT_OFFSET, contract.descriptor_offsets.context_id);
1250 assert_eq!(
1251 PAYLOAD_OFFSET_OFFSET,
1252 contract.descriptor_offsets.payload_offset
1253 );
1254 assert_eq!(CHECKSUM_OFFSET, contract.descriptor_offsets.checksum);
1255 assert_eq!(KIND_HIT, contract.record_kinds.hit);
1256 assert_eq!(KIND_DECISION, contract.record_kinds.decision);
1257 assert_eq!(KIND_ORDINAL_HIT, contract.record_kinds.ordinal_hit);
1258 assert_eq!(Some(KIND_PHASE), contract.record_kinds.phase);
1259 assert_eq!(Some(KIND_THREAD_PHASE), contract.record_kinds.thread_phase);
1260 assert_eq!(Some(KIND_THREAD_END), contract.record_kinds.thread_end);
1261 assert_eq!(
1262 Some(KIND_TEST_BOUNDARY),
1263 contract.record_kinds.test_boundary
1264 );
1265 assert!(RUNTIME_TEMPLATE.contains("b\"SCVRUST3\""));
1266 assert!(RUNTIME_TEMPLATE.contains("const DESCRIPTOR_SIZE: usize = 40;"));
1267 }
1268
1269 #[test]
1270 fn assertion_context_derivation_is_exact_nested_and_never_promotes_background() {
1271 let first =
1272 rust_assertion_context_id(CONTEXT, "rs:decision:0123456789abcdef01234567", 0).unwrap();
1273 let nested =
1274 rust_assertion_context_id(first, "rs:decision:fedcba9876543210fedcba98", 1).unwrap();
1275 assert_ne!(first, CONTEXT);
1276 assert_ne!(nested, first);
1277 assert_eq!(
1278 rust_assertion_context_id(0, "rs:decision:0123456789abcdef01234567", 2).unwrap(),
1279 0
1280 );
1281 assert!(matches!(
1282 rust_assertion_context_id(CONTEXT, "not-a-decision", 2),
1283 Err(RustTransportError::InvalidAssertionContext(_))
1284 ));
1285 assert!(matches!(
1286 rust_assertion_context_id(u64::MAX, "rs:decision:0123456789abcdef01234567", 2),
1287 Err(RustTransportError::InvalidAssertionContext(_))
1288 ));
1289 }
1290
1291 #[test]
1292 fn phase_context_chains_resolve_exactly_to_the_supervisor_attempt() {
1293 let outer_id = "rs:decision:0123456789abcdef01234567";
1294 let inner_id = "rs:decision:fedcba9876543210fedcba98";
1295 let outer = rust_assertion_context_id(CONTEXT, outer_id, 10).unwrap();
1296 let inner = rust_assertion_context_id(outer, inner_id, 11).unwrap();
1297 let read = RustTransportRead {
1298 observations: vec![RustTransportObservation {
1299 process_id: 7,
1300 context_id: inner,
1301 observation: RustProbeObservation::Hit {
1302 id: "rs:statement:0123456789abcdef01234567".into(),
1303 },
1304 }],
1305 ordinal_hits: Vec::new(),
1306 phases: vec![
1307 RustPhaseContext {
1308 process_id: 7,
1309 child_context_id: outer,
1310 parent_context_id: CONTEXT,
1311 invocation_nonce: 10,
1312 decision_id: outer_id.into(),
1313 },
1314 RustPhaseContext {
1315 process_id: 7,
1316 child_context_id: inner,
1317 parent_context_id: outer,
1318 invocation_nonce: 11,
1319 decision_id: inner_id.into(),
1320 },
1321 ],
1322 thread_phases: Vec::new(),
1323 thread_ends: Vec::new(),
1324 test_boundaries: Vec::new(),
1325 committed: 3,
1326 incomplete: 0,
1327 dropped: 0,
1328 attachments: 1,
1329 };
1330 validate_rust_phase_contexts(CONTEXT, &read).unwrap();
1331
1332 let mut threaded = read.clone();
1333 let thread_child = rust_thread_context_id(inner, 7);
1334 threaded.thread_phases.push(RustThreadPhase {
1335 process_id: 7,
1336 child_context_id: thread_child,
1337 parent_context_id: inner,
1338 invocation_nonce: 7,
1339 commit_index: 3,
1340 });
1341 threaded.observations[0].context_id = thread_child;
1342 threaded.committed = 4;
1343 validate_rust_phase_contexts(CONTEXT, &threaded).unwrap();
1344
1345 let mut tampered_thread = threaded.clone();
1346 tampered_thread.thread_phases[0].invocation_nonce = 8;
1347 assert!(matches!(
1348 validate_rust_phase_contexts(CONTEXT, &tampered_thread),
1349 Err(RustTransportError::InvalidAssertionContext(_))
1350 ));
1351
1352 let mut invalid = read.clone();
1353 invalid.phases[0].parent_context_id = 99;
1354 assert!(matches!(
1355 validate_rust_phase_contexts(CONTEXT, &invalid),
1356 Err(RustTransportError::InvalidAssertionContext(_))
1357 ));
1358
1359 let mut unused = read.clone();
1360 let unused_child = rust_assertion_context_id(CONTEXT, inner_id, 12).unwrap();
1361 unused.phases.push(RustPhaseContext {
1362 process_id: 7,
1363 child_context_id: unused_child,
1364 parent_context_id: CONTEXT,
1365 invocation_nonce: 12,
1366 decision_id: inner_id.into(),
1367 });
1368 validate_rust_phase_contexts(CONTEXT, &unused).unwrap();
1369
1370 let mut cycle = read;
1371 cycle.phases[0].parent_context_id = inner;
1372 assert!(matches!(
1373 validate_rust_phase_contexts(CONTEXT, &cycle),
1374 Err(RustTransportError::InvalidAssertionContext(_))
1375 ));
1376 }
1377
1378 #[test]
1379 fn shared_transport_partitions_each_record_once_by_exact_test_root() {
1380 const SECOND: u64 = 84;
1381 let outer_id = "rs:decision:0123456789abcdef01234567";
1382 let inner_id = "rs:decision:fedcba9876543210fedcba98";
1383 let outer = rust_assertion_context_id(CONTEXT, outer_id, 10).unwrap();
1384 let inner = rust_assertion_context_id(outer, inner_id, 11).unwrap();
1385 let read = RustTransportRead {
1386 observations: vec![
1387 RustTransportObservation {
1388 process_id: 7,
1389 context_id: inner,
1390 observation: RustProbeObservation::Hit {
1391 id: "rs:statement:0123456789abcdef01234567".into(),
1392 },
1393 },
1394 RustTransportObservation {
1395 process_id: 7,
1396 context_id: SECOND,
1397 observation: RustProbeObservation::Hit {
1398 id: "rs:function:fedcba9876543210fedcba98".into(),
1399 },
1400 },
1401 RustTransportObservation {
1402 process_id: 7,
1403 context_id: 0,
1404 observation: RustProbeObservation::Hit {
1405 id: "rs:statement:aaaaaaaaaaaaaaaaaaaaaaaa".into(),
1406 },
1407 },
1408 ],
1409 ordinal_hits: vec![RustOrdinalHit {
1410 process_id: 7,
1411 context_id: CONTEXT,
1412 ordinal: 9,
1413 }],
1414 phases: vec![
1415 RustPhaseContext {
1416 process_id: 7,
1417 child_context_id: outer,
1418 parent_context_id: CONTEXT,
1419 invocation_nonce: 10,
1420 decision_id: outer_id.into(),
1421 },
1422 RustPhaseContext {
1423 process_id: 7,
1424 child_context_id: inner,
1425 parent_context_id: outer,
1426 invocation_nonce: 11,
1427 decision_id: inner_id.into(),
1428 },
1429 ],
1430 thread_phases: Vec::new(),
1431 thread_ends: Vec::new(),
1432 test_boundaries: Vec::new(),
1433 committed: 6,
1434 incomplete: 0,
1435 dropped: 0,
1436 attachments: 3,
1437 };
1438 let bases = BTreeSet::from([CONTEXT, SECOND]);
1439 let partition = partition_rust_transport_by_test_contexts(&read, &bases).unwrap();
1440 assert_eq!(partition.attributed[&CONTEXT].committed, 4);
1441 assert_eq!(partition.attributed[&SECOND].committed, 1);
1442 assert_eq!(partition.background.committed, 1);
1443 assert_eq!(partition.background.attachments, 3);
1444 assert!(partition.thread_scope_limitations.is_empty());
1445 assert_eq!(
1446 partition
1447 .attributed
1448 .values()
1449 .map(|transport| transport.committed)
1450 .sum::<u64>()
1451 + partition.background.committed,
1452 read.committed
1453 );
1454
1455 let mut foreign = read.clone();
1456 foreign.observations[1].context_id = 99;
1457 assert!(matches!(
1458 partition_rust_transport_by_test_contexts(&foreign, &bases),
1459 Err(RustTransportError::InvalidAttribution(_))
1460 ));
1461
1462 let mut incomplete = read.clone();
1463 incomplete.dropped = 1;
1464 assert!(matches!(
1465 partition_rust_transport_by_test_contexts(&incomplete, &bases),
1466 Err(RustTransportError::InvalidAttribution(_))
1467 ));
1468
1469 let mut cycle = read;
1470 cycle.phases[0].parent_context_id = inner;
1471 assert!(matches!(
1472 partition_rust_transport_by_test_contexts(&cycle, &bases),
1473 Err(RustTransportError::InvalidAttribution(_))
1474 ));
1475 }
1476
1477 #[test]
1478 fn thread_phases_are_join_bounded_by_the_test_boundary() {
1479 const SECOND: u64 = 84;
1480 let bases = BTreeSet::from([CONTEXT, SECOND]);
1481 let joined = rust_thread_context_id(CONTEXT, 0);
1482 let escaped = rust_thread_context_id(CONTEXT, 1);
1483 let hit = |context_id: u64| RustTransportObservation {
1484 process_id: 7,
1485 context_id,
1486 observation: RustProbeObservation::Hit {
1487 id: "rs:statement:0123456789abcdef01234567".into(),
1488 },
1489 };
1490 let read = RustTransportRead {
1491 observations: vec![hit(joined), hit(escaped), hit(CONTEXT)],
1492 thread_phases: vec![
1493 RustThreadPhase {
1494 process_id: 7,
1495 child_context_id: joined,
1496 parent_context_id: CONTEXT,
1497 invocation_nonce: 0,
1498 commit_index: 0,
1499 },
1500 RustThreadPhase {
1501 process_id: 7,
1502 child_context_id: escaped,
1503 parent_context_id: CONTEXT,
1504 invocation_nonce: 1,
1505 commit_index: 1,
1506 },
1507 ],
1508 thread_ends: vec![RustThreadEnd {
1509 process_id: 7,
1510 context_id: joined,
1511 commit_index: 5,
1512 }],
1513 test_boundaries: vec![
1514 RustTestBoundary {
1515 process_id: 7,
1516 context_id: CONTEXT,
1517 commit_index: 6,
1518 },
1519 RustTestBoundary {
1520 process_id: 7,
1521 context_id: SECOND,
1522 commit_index: 7,
1523 },
1524 ],
1525 committed: 8,
1526 attachments: 1,
1527 ..RustTransportRead::empty()
1528 };
1529 let partition = partition_rust_transport_by_test_contexts(&read, &bases).unwrap();
1530 assert_eq!(partition.attributed[&CONTEXT].committed, 5);
1533 assert_eq!(partition.attributed[&CONTEXT].thread_phases.len(), 1);
1534 assert_eq!(partition.attributed[&CONTEXT].thread_ends.len(), 1);
1535 assert_eq!(partition.attributed[&CONTEXT].test_boundaries.len(), 1);
1536 assert_eq!(partition.attributed[&SECOND].committed, 1);
1537 assert_eq!(partition.background.committed, 2);
1538 assert_eq!(partition.background.thread_phases.len(), 1);
1539 assert_eq!(
1540 partition.background.observations,
1541 vec![hit(escaped)],
1542 "the escaped thread's record must be background"
1543 );
1544 assert_eq!(
1545 partition.thread_scope_limitations,
1546 BTreeSet::from([format!(
1547 "RUST_THREAD_OUTLIVED_TEST: thread phase {escaped:016x} escaped test {CONTEXT:016x}"
1548 )])
1549 );
1550
1551 let mut late_end = read.clone();
1553 late_end.thread_ends.push(RustThreadEnd {
1554 process_id: 7,
1555 context_id: escaped,
1556 commit_index: 8,
1557 });
1558 late_end.committed = 9;
1559 let partition = partition_rust_transport_by_test_contexts(&late_end, &bases).unwrap();
1560 assert_eq!(partition.thread_scope_limitations.len(), 1);
1561 assert_eq!(partition.background.committed, 3);
1562
1563 let mut unbounded = read.clone();
1565 unbounded.test_boundaries.remove(0);
1566 unbounded.committed = 7;
1567 let partition = partition_rust_transport_by_test_contexts(&unbounded, &bases).unwrap();
1568 assert_eq!(partition.attributed[&CONTEXT].committed, 1);
1569 assert_eq!(partition.thread_scope_limitations.len(), 2);
1570
1571 let mut duplicate_end = read.clone();
1572 duplicate_end.thread_ends.push(RustThreadEnd {
1573 process_id: 7,
1574 context_id: joined,
1575 commit_index: 9,
1576 });
1577 duplicate_end.committed = 9;
1578 assert!(matches!(
1579 partition_rust_transport_by_test_contexts(&duplicate_end, &bases),
1580 Err(RustTransportError::InvalidAttribution(_))
1581 ));
1582
1583 let mut orphan_end = read.clone();
1584 orphan_end.thread_ends.push(RustThreadEnd {
1585 process_id: 7,
1586 context_id: 99,
1587 commit_index: 9,
1588 });
1589 orphan_end.committed = 9;
1590 assert!(matches!(
1591 partition_rust_transport_by_test_contexts(&orphan_end, &bases),
1592 Err(RustTransportError::InvalidAttribution(_))
1593 ));
1594
1595 let mut duplicate_boundary = read.clone();
1596 duplicate_boundary.test_boundaries.push(RustTestBoundary {
1597 process_id: 7,
1598 context_id: CONTEXT,
1599 commit_index: 9,
1600 });
1601 duplicate_boundary.committed = 9;
1602 assert!(matches!(
1603 partition_rust_transport_by_test_contexts(&duplicate_boundary, &bases),
1604 Err(RustTransportError::InvalidAttribution(_))
1605 ));
1606
1607 let mut unknown_boundary = read.clone();
1608 unknown_boundary.test_boundaries.push(RustTestBoundary {
1609 process_id: 7,
1610 context_id: 99,
1611 commit_index: 9,
1612 });
1613 unknown_boundary.committed = 9;
1614 assert!(matches!(
1615 partition_rust_transport_by_test_contexts(&unknown_boundary, &bases),
1616 Err(RustTransportError::InvalidAttribution(_))
1617 ));
1618
1619 let mut duplicate_definition = read;
1620 duplicate_definition.phases.push(RustPhaseContext {
1621 process_id: 7,
1622 child_context_id: joined,
1623 parent_context_id: CONTEXT,
1624 invocation_nonce: 0,
1625 decision_id: "rs:decision:0123456789abcdef01234567".into(),
1626 });
1627 duplicate_definition.committed = 9;
1628 assert!(matches!(
1629 partition_rust_transport_by_test_contexts(&duplicate_definition, &bases),
1630 Err(RustTransportError::InvalidAttribution(_))
1631 ));
1632 }
1633
1634 #[test]
1635 fn a_damaged_transport_is_refused_with_what_is_wrong() {
1636 let directory = temporary_directory("damaged");
1639 let id: &[u8] = b"rs:statement:0123456789abcdef01234567";
1640 let case = AtomicUsize::new(0);
1641 let write = |bytes: &[u8]| {
1642 let path = directory.join(format!(
1643 "case-{}.transport",
1644 case.fetch_add(1, Ordering::Relaxed)
1645 ));
1646 fs::write(&path, bytes).unwrap();
1647 path
1648 };
1649 let base = directory.join("base.transport");
1650 create_rust_transport(&base, TOKEN, 4, 512).unwrap();
1651 let empty = fs::read(&base).unwrap();
1652 let with = |records: &[(u8, u8, &[u8], &[u8])]| {
1653 let mut bytes = empty.clone();
1654 for (kind, outcome, id, values) in records {
1655 append_record(&mut bytes, *kind, *outcome, 1, id, values);
1656 }
1657 bytes
1658 };
1659 let valid = with(&[(KIND_HIT, 0, id, b"")]);
1660 let read = |bytes: &[u8], token: &[u8; TOKEN_SIZE]| match read_rust_transport(
1661 &write(bytes),
1662 token,
1663 ) {
1664 Ok(_) => "accepted".to_owned(),
1665 Err(error) => error.to_string(),
1666 };
1667 assert_eq!(read(&valid, &TOKEN), "accepted");
1668 let set = |at: usize, value: &[u8]| {
1669 let mut bytes = valid.clone();
1670 bytes[at..at + value.len()].copy_from_slice(value);
1671 bytes
1672 };
1673 let descriptor = HEADER_SIZE;
1674 let mut longer = valid.clone();
1675 longer.push(0);
1676 for (bytes, token, reason) in [
1677 (set(0, b"X"), TOKEN, "invalid Rust transport header"),
1678 (
1679 valid.clone(),
1680 [0x41; TOKEN_SIZE],
1681 "invalid Rust transport header",
1682 ),
1683 (set(52, &[1]), TOKEN, "invalid Rust transport header"),
1684 (
1685 set(20, &0_u32.to_le_bytes()),
1686 TOKEN,
1687 "invalid Rust transport header",
1688 ),
1689 (longer, TOKEN, "invalid Rust transport length"),
1690 (
1691 set(descriptor + COMMIT_OFFSET, &[2]),
1692 TOKEN,
1693 "invalid Rust transport descriptor 0",
1694 ),
1695 (
1696 set(descriptor + 3, &[1]),
1697 TOKEN,
1698 "invalid Rust transport descriptor 0",
1699 ),
1700 (
1701 set(descriptor + PAYLOAD_LENGTH_OFFSET, &99_u32.to_le_bytes()),
1702 TOKEN,
1703 "invalid Rust transport descriptor 0",
1704 ),
1705 (
1706 set(descriptor + PAYLOAD_OFFSET_OFFSET, &4_000_u32.to_le_bytes()),
1707 TOKEN,
1708 "invalid Rust transport descriptor 0",
1709 ),
1710 (
1711 set(descriptor + CHECKSUM_OFFSET, &0_u64.to_le_bytes()),
1712 TOKEN,
1713 "invalid Rust transport record 0",
1714 ),
1715 (
1716 with(&[(KIND_HIT, 0, b"not-a-probe", b"")]),
1717 TOKEN,
1718 "invalid Rust transport record 0",
1719 ),
1720 (
1721 with(&[(KIND_HIT, 0, &[0xff, 0xfe], b"")]),
1722 TOKEN,
1723 "invalid Rust transport record 0",
1724 ),
1725 (
1726 with(&[(KIND_HIT, 0, id, b"x")]),
1727 TOKEN,
1728 "invalid Rust transport record 0",
1729 ),
1730 (
1731 with(&[(99, 0, id, b"")]),
1732 TOKEN,
1733 "invalid Rust transport record 0",
1734 ),
1735 ] {
1736 let said = read(&bytes, &token);
1737 assert!(said.contains(reason), "{reason}: {said}");
1738 }
1739 let uncommitted = {
1742 let mut bytes = valid.clone();
1743 bytes[descriptor + COMMIT_OFFSET] = 0;
1744 bytes
1745 };
1746 assert_eq!(read(&uncommitted, &TOKEN), "accepted");
1747 fs::remove_dir_all(&directory).ok();
1748 }
1749
1750 fn append_record(
1751 bytes: &mut [u8],
1752 kind: u8,
1753 outcome: u8,
1754 context_id: u64,
1755 id: &[u8],
1756 values: &[u8],
1757 ) {
1758 let descriptor_capacity = get_u32(bytes, 20).unwrap() as usize;
1759 let payload_base = HEADER_SIZE + descriptor_capacity * DESCRIPTOR_SIZE;
1760 let next = get_u64(bytes, NEXT_DESCRIPTOR_OFFSET).unwrap();
1761 let next_payload = get_u64(bytes, NEXT_PAYLOAD_OFFSET).unwrap();
1762 let descriptor = HEADER_SIZE + usize::try_from(next).unwrap() * DESCRIPTOR_SIZE;
1763 let payload_length = (id.len() + values.len()) as u32;
1764 bytes[descriptor + KIND_OFFSET] = kind;
1765 bytes[descriptor + OUTCOME_OFFSET] = outcome;
1766 bytes[descriptor + 3] = 0;
1767 put_u32(bytes, descriptor + PID_OFFSET, 7);
1768 bytes[descriptor + CONTEXT_OFFSET..descriptor + CONTEXT_OFFSET + 8]
1769 .copy_from_slice(&context_id.to_le_bytes());
1770 put_u32(
1771 bytes,
1772 descriptor + PAYLOAD_OFFSET_OFFSET,
1773 next_payload as u32,
1774 );
1775 put_u32(bytes, descriptor + PAYLOAD_LENGTH_OFFSET, payload_length);
1776 put_u32(bytes, descriptor + ID_LENGTH_OFFSET, id.len() as u32);
1777 put_u32(bytes, descriptor + VALUE_LENGTH_OFFSET, values.len() as u32);
1778 let record_checksum = checksum(
1779 kind,
1780 outcome,
1781 7,
1782 context_id,
1783 next_payload as u32,
1784 payload_length,
1785 id.len() as u32,
1786 values.len() as u32,
1787 id,
1788 values,
1789 );
1790 bytes[descriptor + CHECKSUM_OFFSET..descriptor + CHECKSUM_OFFSET + 8]
1791 .copy_from_slice(&record_checksum.to_le_bytes());
1792 let payload = payload_base + usize::try_from(next_payload).unwrap();
1793 bytes[payload..payload + id.len()].copy_from_slice(id);
1794 bytes[payload + id.len()..payload + id.len() + values.len()].copy_from_slice(values);
1795 bytes[descriptor + COMMIT_OFFSET] = 1;
1796 bytes[NEXT_DESCRIPTOR_OFFSET..NEXT_DESCRIPTOR_OFFSET + 8]
1797 .copy_from_slice(&(next + 1).to_le_bytes());
1798 bytes[NEXT_PAYLOAD_OFFSET..NEXT_PAYLOAD_OFFSET + 8]
1799 .copy_from_slice(&(next_payload + u64::from(payload_length)).to_le_bytes());
1800 }
1801
1802 #[test]
1803 fn thread_kind_records_are_authenticated_unique_and_strict() {
1804 let directory = temporary_directory("thread-kinds");
1805 let child = rust_thread_context_id(CONTEXT, 3);
1806 let mut definition = [0_u8; 16];
1807 definition[..8].copy_from_slice(&CONTEXT.to_le_bytes());
1808 definition[8..].copy_from_slice(&3_u64.to_le_bytes());
1809
1810 type SyntheticRecord<'bytes> = (u8, u8, u64, &'bytes [u8], &'bytes [u8]);
1811 let case = AtomicUsize::new(0);
1812 let build = |records: &[SyntheticRecord<'_>]| {
1813 let path = directory.join(format!(
1814 "case-{}.transport",
1815 case.fetch_add(1, Ordering::Relaxed)
1816 ));
1817 create_rust_transport(&path, TOKEN, 16, 4_096).unwrap();
1818 let mut bytes = fs::read(&path).unwrap();
1819 for (kind, outcome, context, id, values) in records {
1820 append_record(&mut bytes, *kind, *outcome, *context, id, values);
1821 }
1822 fs::write(&path, bytes).unwrap();
1823 path
1824 };
1825
1826 let valid = build(&[
1827 (KIND_THREAD_PHASE, 0, child, b"", &definition),
1828 (KIND_THREAD_END, 0, child, b"", b""),
1829 (KIND_TEST_BOUNDARY, 0, CONTEXT, b"", b""),
1830 ]);
1831 let read = read_rust_transport(&valid, &TOKEN).unwrap();
1832 assert_eq!(read.committed, 3);
1833 assert_eq!(
1834 read.thread_phases,
1835 vec![RustThreadPhase {
1836 process_id: 7,
1837 child_context_id: child,
1838 parent_context_id: CONTEXT,
1839 invocation_nonce: 3,
1840 commit_index: 0,
1841 }]
1842 );
1843 assert_eq!(
1844 read.thread_ends,
1845 vec![RustThreadEnd {
1846 process_id: 7,
1847 context_id: child,
1848 commit_index: 1,
1849 }]
1850 );
1851 assert_eq!(
1852 read.test_boundaries,
1853 vec![RustTestBoundary {
1854 process_id: 7,
1855 context_id: CONTEXT,
1856 commit_index: 2,
1857 }]
1858 );
1859
1860 let tampered = build(&[(KIND_THREAD_PHASE, 0, child ^ 1, b"", &definition)]);
1863 assert!(matches!(
1864 read_rust_transport(&tampered, &TOKEN),
1865 Err(RustTransportError::InvalidAssertionContext(_))
1866 ));
1867
1868 let conflicting_definition = {
1869 let mut other = [0_u8; 16];
1870 other[..8].copy_from_slice(&CONTEXT.to_le_bytes());
1871 other[8..].copy_from_slice(&4_u64.to_le_bytes());
1872 build(&[
1873 (KIND_THREAD_PHASE, 0, child, b"", &definition),
1874 (KIND_THREAD_PHASE, 0, child, b"", &other),
1875 ])
1876 };
1877 assert!(matches!(
1878 read_rust_transport(&conflicting_definition, &TOKEN),
1879 Err(RustTransportError::InvalidAssertionContext(_))
1880 ));
1881
1882 let duplicate_end = build(&[
1883 (KIND_THREAD_PHASE, 0, child, b"", &definition),
1884 (KIND_THREAD_END, 0, child, b"", b""),
1885 (KIND_THREAD_END, 0, child, b"", b""),
1886 ]);
1887 assert_eq!(
1888 read_rust_transport(&duplicate_end, &TOKEN),
1889 Err(RustTransportError::InvalidRecord(2))
1890 );
1891
1892 let duplicate_boundary = build(&[
1893 (KIND_TEST_BOUNDARY, 0, CONTEXT, b"", b""),
1894 (KIND_TEST_BOUNDARY, 0, CONTEXT, b"", b""),
1895 ]);
1896 assert_eq!(
1897 read_rust_transport(&duplicate_boundary, &TOKEN),
1898 Err(RustTransportError::InvalidRecord(1))
1899 );
1900
1901 for invalid in [
1902 build(&[(KIND_THREAD_END, 0, 0, b"", b"")]),
1904 build(&[(KIND_TEST_BOUNDARY, 0, u64::MAX, b"", b"")]),
1905 build(&[(KIND_THREAD_END, 0, CONTEXT, b"x", b"")]),
1907 build(&[(KIND_TEST_BOUNDARY, 0, CONTEXT, b"", b"y")]),
1908 build(&[(KIND_THREAD_PHASE, 0, child, b"", &definition[..8])]),
1909 build(&[(KIND_THREAD_END, 1, CONTEXT, b"", b"")]),
1911 ] {
1912 assert_eq!(
1913 read_rust_transport(&invalid, &TOKEN),
1914 Err(RustTransportError::InvalidRecord(0))
1915 );
1916 }
1917
1918 fs::remove_dir_all(directory).unwrap();
1919 }
1920
1921 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
1926 #[test]
1927 fn runtime_stub_compiles_and_records_nothing_elsewhere() {
1928 let directory = temporary_directory("stub");
1929 let binary = compile_fixture(&directory);
1930 let transport = directory.join("stub.transport");
1931 create_rust_transport(&transport, TOKEN, 1_024, 128 * 1024).unwrap();
1932 let output = Command::new(&binary)
1933 .arg("threads")
1934 .env(RUST_TRANSPORT_ENV, &transport)
1935 .env(RUST_TRANSPORT_TOKEN_ENV, token_hex())
1936 .env(RUST_CONTEXT_ENV, format!("{CONTEXT:016x}"))
1937 .output()
1938 .unwrap();
1939 assert!(output.status.success());
1940 let read = read_rust_transport(&transport, &TOKEN).unwrap();
1941 assert_eq!(read.committed, 0);
1942 assert_eq!(read.attachments, 0);
1943 fs::remove_dir_all(directory).unwrap();
1944 }
1945
1946 #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
1947 #[test]
1948 fn mmap_transport_is_concurrent_bounded_strict_and_kill_resilient() {
1949 let directory = temporary_directory("all");
1950 let binary = compile_fixture(&directory);
1951
1952 let concurrent = directory.join("concurrent.transport");
1953 create_rust_transport(&concurrent, TOKEN, 1_024, 128 * 1024).unwrap();
1954 let output = Command::new(&binary)
1955 .arg("threads")
1956 .env(RUST_TRANSPORT_ENV, &concurrent)
1957 .env(RUST_TRANSPORT_TOKEN_ENV, token_hex())
1958 .env(RUST_CONTEXT_ENV, format!("{CONTEXT:016x}"))
1959 .output()
1960 .unwrap();
1961 assert!(output.status.success());
1962 let read = read_rust_transport(&concurrent, &TOKEN).unwrap();
1963 assert_eq!(read.committed, 818);
1964 assert_eq!(read.incomplete, 0);
1965 assert_eq!(read.dropped, 0);
1966 assert_eq!(read.attachments, 1);
1967 assert_eq!(read.observations.len(), 801);
1968 assert!(
1969 matches!(read.observations.last(), Some(RustTransportObservation { context_id: CONTEXT, observation: RustProbeObservation::Decision { values, outcome: false, .. }, .. }) if values == &[Some(true), Some(false)])
1970 );
1971 assert_eq!(read.thread_phases.len(), 8);
1974 assert!(
1975 read.thread_phases
1976 .iter()
1977 .all(|phase| phase.parent_context_id == CONTEXT)
1978 );
1979 assert_eq!(read.thread_ends.len(), 8);
1980 let thread_contexts = read
1981 .thread_phases
1982 .iter()
1983 .map(|phase| phase.child_context_id)
1984 .collect::<BTreeSet<_>>();
1985 assert_eq!(thread_contexts.len(), 8);
1986 assert_eq!(
1987 read.thread_ends
1988 .iter()
1989 .map(|end| end.context_id)
1990 .collect::<BTreeSet<_>>(),
1991 thread_contexts
1992 );
1993 assert!(read.observations.iter().all(|item| {
1994 item.context_id == CONTEXT || thread_contexts.contains(&item.context_id)
1995 }));
1996 assert_eq!(
1997 read.observations
1998 .iter()
1999 .filter(|item| thread_contexts.contains(&item.context_id))
2000 .count(),
2001 800
2002 );
2003 validate_rust_phase_contexts(CONTEXT, &read).unwrap();
2004 assert_eq!(read.ordinal_hits.len(), 1);
2005 assert_eq!(read.ordinal_hits[0].ordinal, 7);
2006 assert_eq!(read.ordinal_hits[0].context_id, CONTEXT);
2007 assert_eq!(
2008 read_rust_transport(&concurrent, &[0x43; TOKEN_SIZE]),
2009 Err(RustTransportError::InvalidHeader),
2010 "a supervisor must never accept evidence from another task token"
2011 );
2012
2013 let mir_decisions = directory.join("mir-decisions.transport");
2014 create_rust_transport(&mir_decisions, TOKEN, 16, 4_096).unwrap();
2015 let output = Command::new(&binary)
2016 .arg("mir-decisions")
2017 .env(RUST_TRANSPORT_ENV, &mir_decisions)
2018 .env(RUST_TRANSPORT_TOKEN_ENV, token_hex())
2019 .env(RUST_CONTEXT_ENV, format!("{CONTEXT:016x}"))
2020 .output()
2021 .unwrap();
2022 assert!(output.status.success());
2023 let read = read_rust_transport(&mir_decisions, &TOKEN).unwrap();
2024 assert_eq!(
2025 read.committed, 7,
2026 "two migrated threads add one thread phase and one thread end each"
2027 );
2028 assert_eq!(read.thread_phases.len(), 2);
2029 assert_eq!(read.thread_ends.len(), 2);
2030 assert_eq!(read.dropped, 0);
2031 assert_eq!(read.incomplete, 0);
2032 assert!(read.observations.iter().any(|observation| matches!(
2033 observation,
2034 RustTransportObservation {
2035 context_id: 901,
2036 observation: RustProbeObservation::Decision { id, values, outcome: true },
2037 ..
2038 }
2039 if id == "rs:decision:0123456789abcdef01234567"
2040 && values == &[Some(true), Some(true)]
2041 )));
2042 assert!(
2043 read.ordinal_hits
2044 .iter()
2045 .any(|hit| hit.context_id == 903 && hit.ordinal == 777)
2046 );
2047 assert!(!read.ordinal_hits.iter().any(|hit| hit.ordinal == 888));
2048 assert!(read.observations.iter().any(|observation| matches!(
2049 observation,
2050 RustTransportObservation {
2051 context_id: 902,
2052 observation: RustProbeObservation::Decision { id, values, outcome: false },
2053 ..
2054 }
2055 if id == "rs:decision:fedcba9876543210fedcba98"
2056 && values == &[Some(false)]
2057 )));
2058
2059 let processes = directory.join("processes.transport");
2060 create_rust_transport(&processes, TOKEN, 64, 8 * 1024).unwrap();
2061 let mut children = (0..8)
2062 .map(|_| {
2063 Command::new(&binary)
2064 .env(RUST_TRANSPORT_ENV, &processes)
2065 .env(RUST_TRANSPORT_TOKEN_ENV, token_hex())
2066 .env(RUST_CONTEXT_ENV, format!("{CONTEXT:016x}"))
2067 .spawn()
2068 .unwrap()
2069 })
2070 .collect::<Vec<_>>();
2071 for child in &mut children {
2072 assert!(child.wait().unwrap().success());
2073 }
2074 let read = read_rust_transport(&processes, &TOKEN).unwrap();
2075 assert_eq!(read.attachments, 8);
2076 assert_eq!(read.committed, 24);
2077 assert_eq!(read.incomplete, 0);
2078 assert_eq!(read.dropped, 0);
2079 assert_eq!(
2080 read.observations
2081 .iter()
2082 .map(|item| item.process_id)
2083 .collect::<std::collections::BTreeSet<_>>()
2084 .len(),
2085 8
2086 );
2087
2088 let nested = directory.join("nested-context.transport");
2089 create_rust_transport(&nested, TOKEN, 16, 4_096).unwrap();
2090 let output = Command::new(&binary)
2091 .arg("contexts")
2092 .env(RUST_TRANSPORT_ENV, &nested)
2093 .env(RUST_TRANSPORT_TOKEN_ENV, token_hex())
2094 .env(RUST_CONTEXT_ENV, format!("{CONTEXT:016x}"))
2095 .output()
2096 .unwrap();
2097 assert!(output.status.success());
2098 let read = read_rust_transport(&nested, &TOKEN).unwrap();
2099 assert_eq!(
2100 read.observations
2101 .iter()
2102 .map(|item| item.context_id)
2103 .collect::<Vec<_>>(),
2104 [
2105 CONTEXT,
2106 100,
2107 200,
2108 100,
2109 CONTEXT,
2110 rust_assertion_context_id(CONTEXT, "rs:decision:0123456789abcdef01234567", 0)
2111 .unwrap(),
2112 CONTEXT,
2113 rust_assertion_context_id(CONTEXT, "rs:decision:0123456789abcdef01234567", 1)
2114 .unwrap(),
2115 CONTEXT,
2116 ]
2117 );
2118 let assertion =
2119 rust_assertion_context_id(CONTEXT, "rs:decision:0123456789abcdef01234567", 0).unwrap();
2120 assert_eq!(read.phases.len(), 2);
2121 assert_eq!(read.phases[0].child_context_id, assertion);
2122 assert_eq!(read.phases[0].parent_context_id, CONTEXT);
2123 assert_eq!(read.phases[0].invocation_nonce, 0);
2124 assert_eq!(
2125 read.phases[0].decision_id,
2126 "rs:decision:0123456789abcdef01234567"
2127 );
2128 assert_ne!(
2129 read.phases[0].child_context_id,
2130 read.phases[1].child_context_id
2131 );
2132 assert_eq!(read.phases[1].parent_context_id, CONTEXT);
2133 assert_eq!(read.phases[1].invocation_nonce, 1);
2134 assert_eq!(read.phases[1].decision_id, read.phases[0].decision_id);
2135 assert_eq!(read.committed, 11);
2136
2137 let invalid_phase = directory.join("invalid-phase.transport");
2138 fs::copy(&nested, &invalid_phase).unwrap();
2139 let mut bytes = fs::read(&invalid_phase).unwrap();
2140 let descriptor_capacity = get_u32(&bytes, 20).unwrap();
2141 let payload_base = HEADER_SIZE + descriptor_capacity as usize * DESCRIPTOR_SIZE;
2142 let descriptor = (0..read.committed as usize)
2143 .map(|index| HEADER_SIZE + index * DESCRIPTOR_SIZE)
2144 .find(|offset| bytes[*offset + KIND_OFFSET] == KIND_PHASE)
2145 .unwrap();
2146 let payload_offset = get_u32(&bytes, descriptor + PAYLOAD_OFFSET_OFFSET).unwrap();
2147 let payload_length = get_u32(&bytes, descriptor + PAYLOAD_LENGTH_OFFSET).unwrap();
2148 let id_length = get_u32(&bytes, descriptor + ID_LENGTH_OFFSET).unwrap();
2149 let value_length = get_u32(&bytes, descriptor + VALUE_LENGTH_OFFSET).unwrap();
2150 let payload = payload_base + payload_offset as usize;
2151 let id = bytes[payload..payload + id_length as usize].to_vec();
2152 let values =
2153 bytes[payload + id_length as usize..payload + payload_length as usize].to_vec();
2154 let invalid_child = 7_u64;
2155 bytes[descriptor + CONTEXT_OFFSET..descriptor + CONTEXT_OFFSET + 8]
2156 .copy_from_slice(&invalid_child.to_le_bytes());
2157 let replacement_checksum = checksum(
2158 KIND_PHASE,
2159 0,
2160 get_u32(&bytes, descriptor + PID_OFFSET).unwrap(),
2161 invalid_child,
2162 payload_offset,
2163 payload_length,
2164 id_length,
2165 value_length,
2166 &id,
2167 &values,
2168 );
2169 bytes[descriptor + CHECKSUM_OFFSET..descriptor + CHECKSUM_OFFSET + 8]
2170 .copy_from_slice(&replacement_checksum.to_le_bytes());
2171 fs::write(&invalid_phase, bytes).unwrap();
2172 assert!(matches!(
2173 read_rust_transport(&invalid_phase, &TOKEN),
2174 Err(RustTransportError::InvalidAssertionContext(_))
2175 ));
2176
2177 let rejected = directory.join("rejected-token.transport");
2178 create_rust_transport(&rejected, TOKEN, 16, 4_096).unwrap();
2179 let output = Command::new(&binary)
2180 .env(RUST_TRANSPORT_ENV, &rejected)
2181 .env(RUST_TRANSPORT_TOKEN_ENV, "00".repeat(TOKEN_SIZE))
2182 .env(RUST_CONTEXT_ENV, format!("{CONTEXT:016x}"))
2183 .output()
2184 .unwrap();
2185 assert!(output.status.success());
2186 let read = read_rust_transport(&rejected, &TOKEN).unwrap();
2187 assert_eq!(read.attachments, 0);
2188 assert_eq!(read.committed, 0);
2189 assert_eq!(read.dropped, 0);
2190
2191 let malformed_context = directory.join("malformed-context.transport");
2192 create_rust_transport(&malformed_context, TOKEN, 16, 4_096).unwrap();
2193 let output = Command::new(&binary)
2194 .env(RUST_TRANSPORT_ENV, &malformed_context)
2195 .env(RUST_TRANSPORT_TOKEN_ENV, token_hex())
2196 .env(RUST_CONTEXT_ENV, "not-a-context")
2197 .output()
2198 .unwrap();
2199 assert!(output.status.success());
2200 let read = read_rust_transport(&malformed_context, &TOKEN).unwrap();
2201 assert_eq!(read.attachments, 0);
2202 assert_eq!(read.committed, 0);
2203
2204 let bounded = directory.join("bounded.transport");
2205 create_rust_transport(&bounded, TOKEN, 2, 256).unwrap();
2206 let output = Command::new(&binary)
2207 .env(RUST_TRANSPORT_ENV, &bounded)
2208 .env(RUST_TRANSPORT_TOKEN_ENV, token_hex())
2209 .env(RUST_CONTEXT_ENV, format!("{CONTEXT:016x}"))
2210 .output()
2211 .unwrap();
2212 assert!(output.status.success());
2213 let read = read_rust_transport(&bounded, &TOKEN).unwrap();
2214 assert_eq!(read.committed, 2);
2215 assert_eq!(read.dropped, 1);
2216
2217 let payload_bounded = directory.join("payload-bounded.transport");
2218 create_rust_transport(&payload_bounded, TOKEN, 8, 16).unwrap();
2219 let output = Command::new(&binary)
2220 .env(RUST_TRANSPORT_ENV, &payload_bounded)
2221 .env(RUST_TRANSPORT_TOKEN_ENV, token_hex())
2222 .env(RUST_CONTEXT_ENV, format!("{CONTEXT:016x}"))
2223 .output()
2224 .unwrap();
2225 assert!(output.status.success());
2226 let read = read_rust_transport(&payload_bounded, &TOKEN).unwrap();
2227 assert_eq!(read.committed, 0);
2228 assert_eq!(read.incomplete, 3);
2229 assert_eq!(read.dropped, 3);
2230
2231 let killed = directory.join("killed.transport");
2232 create_rust_transport(&killed, TOKEN, 16, 4_096).unwrap();
2233 let mut child = Command::new(&binary)
2234 .arg("kill")
2235 .env(RUST_TRANSPORT_ENV, &killed)
2236 .env(RUST_TRANSPORT_TOKEN_ENV, token_hex())
2237 .env(RUST_CONTEXT_ENV, format!("{CONTEXT:016x}"))
2238 .stdout(Stdio::piped())
2239 .spawn()
2240 .unwrap();
2241 let mut ready = String::new();
2242 BufReader::new(child.stdout.take().unwrap())
2243 .read_line(&mut ready)
2244 .unwrap();
2245 assert_eq!(ready, "ready\n");
2246 child.kill().unwrap();
2247 child.wait().unwrap();
2248 let read = read_rust_transport(&killed, &TOKEN).unwrap();
2249 assert_eq!(read.committed, 1);
2250 assert_eq!(read.incomplete, 2);
2251 assert_eq!(read.dropped, 0);
2252 assert_eq!(
2253 read.observations,
2254 [RustTransportObservation {
2255 process_id: read.observations[0].process_id,
2256 context_id: CONTEXT,
2257 observation: RustProbeObservation::Hit {
2258 id: "rs:function:fedcba9876543210fedcba98".into()
2259 }
2260 }]
2261 );
2262
2263 let corrupt = directory.join("corrupt.transport");
2264 create_rust_transport(&corrupt, TOKEN, 2, 256).unwrap();
2265 let mut bytes = fs::read(&corrupt).unwrap();
2266 bytes[0] ^= 1;
2267 fs::write(&corrupt, bytes).unwrap();
2268 assert_eq!(
2269 read_rust_transport(&corrupt, &TOKEN),
2270 Err(RustTransportError::InvalidHeader)
2271 );
2272
2273 let invalid_commit = directory.join("invalid-commit.transport");
2274 fs::copy(&bounded, &invalid_commit).unwrap();
2275 let mut bytes = fs::read(&invalid_commit).unwrap();
2276 bytes[HEADER_SIZE + COMMIT_OFFSET] = 2;
2277 fs::write(&invalid_commit, bytes).unwrap();
2278 assert_eq!(
2279 read_rust_transport(&invalid_commit, &TOKEN),
2280 Err(RustTransportError::InvalidDescriptor(0))
2281 );
2282
2283 let invalid_flags = directory.join("invalid-flags.transport");
2284 fs::copy(&bounded, &invalid_flags).unwrap();
2285 let mut bytes = fs::read(&invalid_flags).unwrap();
2286 bytes[HEADER_SIZE + 3] = 1;
2287 fs::write(&invalid_flags, bytes).unwrap();
2288 assert_eq!(
2289 read_rust_transport(&invalid_flags, &TOKEN),
2290 Err(RustTransportError::InvalidDescriptor(0))
2291 );
2292
2293 let invalid_checksum = directory.join("invalid-checksum.transport");
2294 fs::copy(&bounded, &invalid_checksum).unwrap();
2295 let mut bytes = fs::read(&invalid_checksum).unwrap();
2296 bytes[HEADER_SIZE + CHECKSUM_OFFSET] ^= 1;
2297 fs::write(&invalid_checksum, bytes).unwrap();
2298 assert_eq!(
2299 read_rust_transport(&invalid_checksum, &TOKEN),
2300 Err(RustTransportError::InvalidRecord(0))
2301 );
2302
2303 #[cfg(unix)]
2304 {
2305 use std::os::unix::fs::symlink;
2306
2307 let linked = directory.join("linked.transport");
2308 symlink(&bounded, &linked).unwrap();
2309 assert_eq!(
2310 read_rust_transport(&linked, &TOKEN),
2311 Err(RustTransportError::UnsafeFile(linked.display().to_string()))
2312 );
2313 }
2314
2315 let truncated = directory.join("truncated.transport");
2316 create_rust_transport(&truncated, TOKEN, 2, 256).unwrap();
2317 OpenOptions::new()
2318 .write(true)
2319 .open(&truncated)
2320 .unwrap()
2321 .set_len(63)
2322 .unwrap();
2323 assert_eq!(
2324 read_rust_transport(&truncated, &TOKEN),
2325 Err(RustTransportError::InvalidHeader)
2326 );
2327
2328 let incomplete = directory.join("incomplete.transport");
2329 create_rust_transport(&incomplete, TOKEN, 2, 256).unwrap();
2330 let file = OpenOptions::new()
2331 .read(true)
2332 .write(true)
2333 .open(&incomplete)
2334 .unwrap();
2335 let mapping = map_mut(&file).unwrap();
2336 atomic_u64(&mapping, NEXT_DESCRIPTOR_OFFSET)
2337 .unwrap()
2338 .store(1, Ordering::Release);
2339 mapping.flush().unwrap();
2340 drop(mapping);
2341 let read = read_rust_transport(&incomplete, &TOKEN).unwrap();
2342 assert_eq!(read.committed, 0);
2343 assert_eq!(read.incomplete, 1);
2344 assert!(read.observations.is_empty());
2345 assert!(read.ordinal_hits.is_empty());
2346
2347 fs::remove_dir_all(directory).unwrap();
2348 }
2349}