1use serde::{Deserialize, Serialize};
8use std::{
9 collections::BTreeMap,
10 fs,
11 path::{Component, Path},
12};
13
14const RUST_PROBE_MAGIC: &str = "SUPERCOV-RUST-PROBE-1";
15
16#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
17#[serde(rename_all = "camelCase", tag = "kind")]
18pub enum RustProbeObservation {
19 Hit {
20 id: String,
21 },
22 Decision {
23 id: String,
24 values: Vec<Option<bool>>,
25 outcome: bool,
26 },
27 Assertion {
30 id: String,
31 },
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct RustProbeEntry {
40 pub thread: u64,
41 pub observation: RustProbeObservation,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum RustProbeReadError {
46 Io(String),
47 UnsafeEntry(String),
48 InvalidHeader,
49 InvalidRecord(usize),
50}
51
52impl std::fmt::Display for RustProbeReadError {
53 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 match self {
55 Self::Io(error) => write!(formatter, "Rust probe I/O failed: {error}"),
56 Self::UnsafeEntry(path) => write!(formatter, "unsafe Rust probe entry: {path}"),
57 Self::InvalidHeader => write!(formatter, "invalid Rust probe header"),
58 Self::InvalidRecord(line) => {
59 write!(formatter, "invalid Rust probe record at line {line}")
60 }
61 }
62 }
63}
64
65impl std::error::Error for RustProbeReadError {}
66
67fn valid_thread(value: &str) -> bool {
69 !value.is_empty()
70 && value.len() <= 20
71 && value.bytes().all(|byte| byte.is_ascii_digit())
72 && (value == "0" || !value.starts_with('0'))
73 && value.parse::<u64>().is_ok()
74}
75
76pub(crate) fn valid_probe_id(id: &str) -> bool {
77 let mut parts = id.split(':');
78 matches!(parts.next(), Some("rs"))
79 && matches!(
80 parts.next(),
81 Some("statement" | "function" | "decision" | "branch")
82 )
83 && parts.next().is_some_and(|digest| {
84 digest.len() == 24 && digest.bytes().all(|byte| byte.is_ascii_hexdigit())
85 })
86 && parts.all(|suffix| {
87 !suffix.is_empty()
88 && suffix
89 .bytes()
90 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
91 })
92}
93
94pub fn render_rust_runtime(module_name: &str, crate_key: &str) -> Result<String, String> {
95 let valid_identifier = !module_name.is_empty()
96 && module_name.bytes().enumerate().all(|(index, byte)| {
97 byte == b'_' || byte.is_ascii_alphabetic() || (index > 0 && byte.is_ascii_digit())
98 });
99 if !valid_identifier {
100 return Err("invalid Rust runtime module name".into());
101 }
102 if crate_key.len() != 24 || !crate_key.bytes().all(|byte| byte.is_ascii_hexdigit()) {
103 return Err("invalid Rust runtime crate key".into());
104 }
105
106 Ok(format!(
107 r#"
108#[doc(hidden)]
109// Injected code must be immune to the HOST crate's lint configuration: serde
110// builds with `#![deny(warnings)]`, so this module's fully-qualified imports
111// (required for no_std hosts) became hard errors as "unused imports".
112#[allow(warnings)]
113mod {module_name} {{
114 // The host crate may be `#![no_std]` -- `bytes` is, and so is much of the
115 // ecosystem's foundation. Nothing here can rely on the std prelude being in
116 // scope, so std is brought in explicitly and every prelude item below is
117 // written out in full. Without this the module does not compile and the
118 // whole build fails, which is a hard failure rather than a degradation.
119 extern crate std;
120 // Every name is imported explicitly: this module is appended to a crate
121 // root, and a crate root may be `no_std` or may turn the prelude off
122 // outright (tracing's macro tests carry `#![no_implicit_prelude]`).
123 use std::cmp::{{Ord, PartialEq, PartialOrd}};
124 use std::convert::From;
125 use std::fs::{{File, OpenOptions}};
126 use std::io::Write as _;
127 use std::iter::{{IntoIterator, Iterator}};
128 use std::marker::Sized;
129 use std::mem::drop;
130 use std::option::Option::{{self, None, Some}};
131 use std::result::Result::{{self, Err, Ok}};
132 use std::string::String;
133 use std::sync::atomic::{{AtomicBool, AtomicU8, AtomicU64, AtomicUsize, Ordering}};
134 use std::sync::{{Mutex, OnceLock}};
135 use std::cell::Cell;
136 use std::ops::ControlFlow;
137 use std::task::Poll;
138 use std::vec::Vec;
139
140 const MAGIC: &[u8] = b"{RUST_PROBE_MAGIC}\n";
141 const CRATE_KEY: &str = "{crate_key}";
142
143 // NOTHING ON THE PROBE PATH MAY ALLOCATE.
144 //
145 // The crate under test may install a `#[global_allocator]` whose body -- or
146 // anything it calls, at any depth -- carries probes. An allocating probe
147 // then re-enters the allocator, which probes, which allocates. bytes-1.12.1
148 // does this in tests/test_bytes_odd_alloc.rs and tests/test_bytes_vec_alloc.rs,
149 // and both died with SIGSEGV before libtest could list a single test.
150 //
151 // A reentrancy flag cannot fix this, and the attempt is instructive: on
152 // macOS the FIRST touch of a thread-local calls `_tlv_bootstrap`, which
153 // allocates -- so the guard recursed inside its own initialisation, before
154 // it could be consulted. A guard that must allocate to answer "am I already
155 // allocating?" is unfixable. Not allocating at all is.
156 //
157 // Records are therefore built in a stack buffer and written with one call.
158 // That also removes a malloc and a free from every probe, which is where
159 // most of a probe's cost used to be.
160 const RECORD_CAPACITY: usize = 256;
161
162 /// The widest condition vector a decision can carry.
163 ///
164 /// Beyond this the frame refuses to record rather than emit a vector whose
165 /// width disagrees with the manifest -- a malformed record the runner
166 /// rejects, which is a wrong number rather than a missing one.
167 const MAX_CONDITIONS: usize = 64;
168
169 // A statement or function hit answers "did this ever run", so only the FIRST
170 // sighting in a process carries information -- and each libtest case runs in
171 // its own process, so first-in-process is first-in-test. Without this, a loop
172 // writes one identical record per iteration: bytes'
173 // advance_bytes_mut_remaining_capacity runs ~2.8M iterations and was still
174 // writing syscalls after four minutes.
175 //
176 // The table is a fixed, open-addressed set of `&'static str` POINTERS, so it
177 // never allocates and never grows -- both of which the probe path forbids.
178 // A crowded table simply writes the record again: a duplicate costs time,
179 // never correctness, whereas dropping one would cost a real observation.
180 const SEEN_SLOTS: usize = 1 << 16;
181 static SEEN: [AtomicUsize; SEEN_SLOTS] = [const {{ AtomicUsize::new(0) }}; SEEN_SLOTS];
182
183 // Which thread wrote a record. Evidence is appended under one mutex, so
184 // the file is in execution order -- but a test that spawns threads
185 // interleaves them, and an assertion on one thread witnesses nothing on
186 // another. Ordinals are handed out on a thread's first record; the main
187 // test thread is 1.
188 static NEXT_THREAD: AtomicU64 = AtomicU64::new(1);
189 std::thread_local! {{
190 static THREAD_ORDINAL: Cell<u64> = const {{ Cell::new(0) }};
191 }}
192
193 fn thread_ordinal() -> u64 {{
194 THREAD_ORDINAL.with(|slot| {{
195 let existing = slot.get();
196 if existing != 0 {{
197 return existing;
198 }}
199 let assigned = NEXT_THREAD.fetch_add(1, Ordering::Relaxed);
200 slot.set(assigned);
201 assigned
202 }})
203 }}
204
205 /// Decimal digits of `value`, written into `record`.
206 fn push_number(record: &mut [u8; RECORD_CAPACITY], length: &mut usize, value: u64) -> bool {{
207 let mut digits = [0u8; 20];
208 let mut count = 0;
209 let mut rest = value;
210 loop {{
211 digits[count] = b'0' + (rest % 10) as u8;
212 count += 1;
213 rest /= 10;
214 if rest == 0 {{
215 break;
216 }}
217 }}
218 let mut index = count;
219 while index > 0 {{
220 index -= 1;
221 if !push(record, length, &[digits[index]]) {{
222 return false;
223 }}
224 }}
225 true
226 }}
227
228 // An assertion marker is deduped separately from hits: it names the
229 // statement that asserts, and that statement's own hit has already been
230 // recorded by the time control reaches the marker.
231 const ASSERTED_SLOTS: usize = 1 << 12;
232 static ASSERTED: [AtomicUsize; ASSERTED_SLOTS] =
233 [const {{ AtomicUsize::new(0) }}; ASSERTED_SLOTS];
234
235 #[inline(always)]
236 fn first_assertion(id: &'static str) -> bool {{
237 let key = id.as_ptr() as usize;
238 let mut slot = (key >> 4) & (ASSERTED_SLOTS - 1);
239 for _ in 0..8 {{
240 let seen = ASSERTED[slot].load(Ordering::Relaxed);
241 if seen == key {{
242 return false;
243 }}
244 if seen == 0 {{
245 match ASSERTED[slot].compare_exchange(0, key, Ordering::Relaxed, Ordering::Relaxed)
246 {{
247 Ok(_) => return true,
248 Err(now) if now == key => return false,
249 Err(_) => {{}}
250 }}
251 }}
252 slot = (slot + 1) & (ASSERTED_SLOTS - 1);
253 }}
254 true
255 }}
256
257 /// Control passed a statement that asserts, so every assertion in it
258 /// held: a failing one panics instead. Evidence this thread wrote before
259 /// this point was in scope for that check.
260 #[inline(always)]
261 pub fn assertion(id: &'static str) {{
262 if first_assertion(id) {{
263 record_assertion(id);
264 }}
265 }}
266
267 #[inline(never)]
268 fn record_assertion(id: &'static str) {{
269 let mut record = [0u8; RECORD_CAPACITY];
270 let mut length = 0;
271 if push(&mut record, &mut length, b"A\t")
272 && push_number(&mut record, &mut length, thread_ordinal())
273 && push(&mut record, &mut length, b"\t")
274 && push(&mut record, &mut length, id.as_bytes())
275 && push(&mut record, &mut length, b"\n")
276 {{
277 write_record(&record[..length]);
278 }}
279 }}
280
281 #[inline(always)]
282 fn first_sighting(id: &'static str) -> bool {{
283 let key = id.as_ptr() as usize;
284 let mut slot = (key >> 4) & (SEEN_SLOTS - 1);
285 for _ in 0..8 {{
286 // Almost every call is a repeat, and a plain load settles it; the
287 // CAS is for the one insertion.
288 let seen = SEEN[slot].load(Ordering::Relaxed);
289 if seen == key {{
290 return false;
291 }}
292 if seen == 0 {{
293 match SEEN[slot].compare_exchange(0, key, Ordering::Relaxed, Ordering::Relaxed) {{
294 Ok(_) => return true,
295 Err(now) if now == key => return false,
296 Err(_) => {{}}
297 }}
298 }}
299 slot = (slot + 1) & (SEEN_SLOTS - 1);
300 }}
301 true
302 }}
303
304 // Decisions cannot collapse by id the way hits do: MC/DC needs the SET of
305 // distinct condition vectors, so every vector must reach the log once. What
306 // carries nothing is the REPEAT of a vector already seen, and in a loop that
307 // is nearly all of them -- bytes' advance_bytes_mut_remaining_capacity took
308 // 40.6s against a 0.367s baseline writing one syscall per evaluation across
309 // ~2.8M iterations.
310 //
311 // Entries hold the whole record -- id pointer, outcome, width, values -- and
312 // are compared word for word. A hash would be smaller and faster, but a
313 // collision would silently drop a distinct vector and understate MC/DC, and
314 // that is exactly the kind of wrong number this project refuses to risk.
315 // A full probe chain falls back to writing, which costs a duplicate.
316 //
317 // The table is lock-free: a decision in a hot loop is evaluated millions
318 // of times, and a mutex per evaluation was most of what a probe cost. A
319 // slot's state goes 0 (empty) -> 1 (being written) -> 2 (readable); a
320 // reader compares only readable slots, so a slot mid-write reads as
321 // "different" and at worst costs one duplicate record.
322 const DECISION_SLOTS: usize = 1 << 11;
323 const DECISION_ENTRY: usize = 10 + MAX_CONDITIONS;
324 const DECISION_WORDS: usize = DECISION_ENTRY.div_ceil(8);
325
326 struct DecisionSlot {{
327 state: AtomicU8,
328 words: [AtomicU64; DECISION_WORDS],
329 }}
330
331 static DECISIONS: [DecisionSlot; DECISION_SLOTS] = [const {{
332 DecisionSlot {{
333 state: AtomicU8::new(0),
334 words: [const {{ AtomicU64::new(0) }}; DECISION_WORDS],
335 }}
336 }}; DECISION_SLOTS];
337
338 // This module is compiled in the crate's own profile -- unoptimized under
339 // `cargo test` -- so the paths below are plain indexed loops: iterator
340 // chains and closures cost real function calls there, and a decision in a
341 // hot loop pays them millions of times.
342 fn first_decision(frame: &DecisionFrame, outcome: bool) -> bool {{
343 let key = frame.id.as_ptr() as usize;
344 let conditions = if frame.conditions < MAX_CONDITIONS {{ frame.conditions }} else {{ MAX_CONDITIONS }};
345 // Word 0 is the id pointer; word 1 starts with outcome and width, then
346 // the condition values fill the remaining bytes. Entries of the same
347 // width are zero beyond `used` words, and the width byte sits in word
348 // 1, so comparing `used` words is exact.
349 let mut words = [0u64; DECISION_WORDS];
350 words[0] = key as u64;
351 words[1] = (if outcome {{ 2u64 }} else {{ 1u64 }}) | ((conditions as u64) << 8);
352 let mut index = 0;
353 while index < conditions {{
354 let byte = 10 + index;
355 words[byte / 8] |= (frame.value(index) as u64) << ((byte % 8) * 8);
356 index += 1;
357 }}
358 let used = (10 + conditions).div_ceil(8);
359 let mut slot = (key >> 4) & (DECISION_SLOTS - 1);
360 let mut attempts = 0;
361 while attempts < 16 {{
362 let cell = &DECISIONS[slot];
363 let state = cell.state.load(Ordering::Acquire);
364 if state == 2 {{
365 let mut same = true;
366 let mut word = 0;
367 while word < used {{
368 if cell.words[word].load(Ordering::Relaxed) != words[word] {{
369 same = false;
370 break;
371 }}
372 word += 1;
373 }}
374 if same {{
375 return false;
376 }}
377 }} else if state == 0 {{
378 if cell
379 .state
380 .compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
381 .is_ok()
382 {{
383 let mut word = 0;
384 while word < DECISION_WORDS {{
385 cell.words[word].store(words[word], Ordering::Relaxed);
386 word += 1;
387 }}
388 cell.state.store(2, Ordering::Release);
389 return true;
390 }}
391 // Another thread took this slot first: look at it again.
392 continue;
393 }}
394 slot = (slot + 1) & (DECISION_SLOTS - 1);
395 attempts += 1;
396 }}
397 true
398 }}
399
400 fn writer() -> Option<&'static Mutex<File>> {{
401 static WRITER: OnceLock<Option<Mutex<File>>> = OnceLock::new();
402 static OPENING: AtomicBool = AtomicBool::new(false);
403 if let Some(writer) = WRITER.get() {{
404 return writer.as_ref();
405 }}
406 // Opening the file is the one step that must allocate: an environment
407 // lookup, a path, a formatted file name. That allocation re-enters an
408 // instrumented allocator, whose probe arrives back here while the
409 // OnceLock is still unset. Declining for the duration of the open costs
410 // a few observations at startup and makes the recursion impossible.
411 if OPENING.swap(true, Ordering::SeqCst) {{
412 return None;
413 }}
414 let opened = WRITER.get_or_init(|| {{
415 let directory = std::env::var_os("SUPERCOV_RUST_EVIDENCE_DIR")?;
416 let directory = std::path::PathBuf::from(directory);
417 std::fs::create_dir_all(&directory).ok()?;
418 let path =
419 directory.join(std::format!("{{CRATE_KEY}}-{{}}.events", std::process::id()));
420 let empty = std::fs::metadata(&path).map_or(true, |metadata| metadata.len() == 0);
421 let mut file = OpenOptions::new().create(true).append(true).open(path).ok()?;
422 if empty {{
423 file.write_all(MAGIC).ok()?;
424 }}
425 let guarded = Mutex::new(file);
426 // `std::sync::Mutex` boxes a platform mutex on its FIRST lock, and
427 // that allocation would otherwise land on the probe path and
428 // re-enter the host allocator. Force it here, where `OPENING`
429 // already makes re-entry harmless.
430 drop(guarded.lock());
431 Some(guarded)
432 }});
433 OPENING.store(false, Ordering::SeqCst);
434 opened.as_ref()
435 }}
436
437 fn write_record(record: &[u8]) {{
438 let Some(writer) = writer() else {{ return }};
439 let Ok(mut writer) = writer.lock() else {{ return }};
440 let _ = writer.write_all(record);
441 }}
442
443 /// Append to a stack record, reporting whether it all fit.
444 fn push(record: &mut [u8; RECORD_CAPACITY], length: &mut usize, bytes: &[u8]) -> bool {{
445 let Some(slice) = record.get_mut(*length..*length + bytes.len()) else {{
446 return false;
447 }};
448 slice.copy_from_slice(bytes);
449 *length += bytes.len();
450 true
451 }}
452
453 // A frame lives on the stack of the function whose decision it records,
454 // once per decision, and a recursive descent parser carries every one of
455 // them down every level: serde_json's recursion-limit test overflowed the
456 // test thread's stack when each frame held two 64-byte arrays. Values are
457 // two bits each in a u128 (0 unevaluated, 1 false, 2 true) and reached
458 // marks one bit each, so a frame is 48 bytes.
459 pub struct DecisionFrame {{
460 id: &'static str,
461 values: u128,
462 /// Let-chain conditions the evaluation got to: a `let` cannot be
463 /// wrapped, so it is marked reached instead and resolved later.
464 reached: u64,
465 conditions: usize,
466 recordable: bool,
467 }}
468
469 impl DecisionFrame {{
470 pub fn new(id: &'static str, conditions: usize) -> Self {{
471 Self {{
472 id,
473 values: 0,
474 reached: 0,
475 conditions,
476 recordable: conditions <= MAX_CONDITIONS,
477 }}
478 }}
479
480 #[inline(always)]
481 fn value(&self, index: usize) -> u8 {{
482 ((self.values >> (2 * index)) & 3) as u8
483 }}
484
485 #[inline(always)]
486 fn set_value(&mut self, index: usize, value: u8) {{
487 let shift = 2 * index;
488 self.values = (self.values & !(3u128 << shift)) | ((value as u128) << shift);
489 }}
490
491 #[inline(always)]
492 fn is_reached(&self, index: usize) -> bool {{
493 index < MAX_CONDITIONS && (self.reached >> index) & 1 == 1
494 }}
495 }}
496
497 /// A let chain got to condition `index` (0 marks the chain evaluated at
498 /// all). Always true, so it sits in the chain as an operand.
499 #[inline(always)]
500 pub fn reached(frame: &mut DecisionFrame, index: usize) -> bool {{
501 if index < MAX_CONDITIONS {{
502 frame.reached |= 1u64 << index;
503 }}
504 true
505 }}
506
507 /// A let chain decided. A chain tries its conditions in order and stops
508 /// at the first that fails, so every reached `let` before the last
509 /// reached condition held, the last one held when the chain was taken and
510 /// failed when it was not, and conditions never reached stay unevaluated.
511 /// `operators` lists the `&&` whose left side holds a `let`, by the index
512 /// of their right side's first condition: reached means the operator
513 /// evaluated its right side, otherwise it short-circuited. The frame then
514 /// resets for the next evaluation, which a `while let` makes every turn.
515 pub fn decision_chain(
516 frame: &mut DecisionFrame,
517 outcome: bool,
518 operators: &[(usize, &'static str, &'static str)],
519 ) {{
520 if !frame.is_reached(0) {{
521 return;
522 }}
523 let conditions = frame.conditions.min(MAX_CONDITIONS);
524 let mut last = 0;
525 let mut index = 0;
526 while index < conditions {{
527 if frame.is_reached(index) || frame.value(index) != 0 {{
528 last = index;
529 }}
530 index += 1;
531 }}
532 let mut index = 0;
533 while index < conditions {{
534 if frame.value(index) == 0 && frame.is_reached(index) {{
535 frame.set_value(index, if index < last || outcome {{ 2 }} else {{ 1 }});
536 }}
537 index += 1;
538 }}
539 for (first, short_circuit, evaluated) in operators {{
540 let got_there = frame.is_reached(*first)
541 || (*first < MAX_CONDITIONS && frame.value(*first) != 0);
542 hit(if got_there {{ evaluated }} else {{ short_circuit }});
543 }}
544 decision(outcome, frame);
545 frame.values = 0;
546 frame.reached = 0;
547 }}
548
549 // The hot paths are inlined into every probe site, so nothing with a
550 // stack buffer may be: an inlined 256-byte record per site turned each
551 // instrumented function's frame into kilobytes, and serde_json's
552 // recursion-limit test overflowed. Writing a record is the rare path and
553 // stays a call of its own.
554 #[inline(always)]
555 pub fn hit(id: &'static str) {{
556 if first_sighting(id) {{
557 record_hit(id);
558 }}
559 }}
560
561 #[inline(never)]
562 fn record_hit(id: &'static str) {{
563 let mut record = [0u8; RECORD_CAPACITY];
564 let mut length = 0;
565 if push(&mut record, &mut length, b"H\t")
566 && push_number(&mut record, &mut length, thread_ordinal())
567 && push(&mut record, &mut length, b"\t")
568 && push(&mut record, &mut length, id.as_bytes())
569 && push(&mut record, &mut length, b"\n")
570 {{
571 write_record(&record[..length]);
572 }}
573 }}
574
575 /// One arm of a match was selected. `ids` holds each arm's `not selected`
576 /// and `selected` IDs in source order, so every arm before `selected` was
577 /// considered and passed over. Each ID is a distinct static string, which
578 /// is what `hit` dedupes on.
579 #[inline(always)]
580 pub fn arms(ids: &[&'static str], selected: usize) {{
581 for arm in 0..selected {{
582 if let Some(id) = ids.get(arm * 2) {{
583 hit(id);
584 }}
585 }}
586 if let Some(id) = ids.get(selected * 2 + 1) {{
587 hit(id);
588 }}
589 }}
590
591 /// The left operand of `&&` or `||`: it short-circuits when it equals
592 /// `short_circuits_when`, otherwise the right operand is about to run.
593 #[inline(always)]
594 pub fn logical(
595 left: bool,
596 short_circuits_when: bool,
597 short_circuit: &'static str,
598 evaluated: &'static str,
599 ) -> bool {{
600 hit(if left == short_circuits_when {{ short_circuit }} else {{ evaluated }});
601 left
602 }}
603
604 /// A `for` loop's iterator, recording on the first `next` whether the
605 /// body ran at all. `size_hint` passes through so collection sizing is
606 /// unchanged; nothing else about the iterator is observable to the loop.
607 pub struct ForLoop<I> {{
608 inner: I,
609 first: bool,
610 zero: &'static str,
611 entered: &'static str,
612 }}
613
614 impl<I: Iterator> Iterator for ForLoop<I> {{
615 type Item = I::Item;
616
617 #[inline(always)]
618 fn next(&mut self) -> Option<I::Item> {{
619 let item = self.inner.next();
620 if self.first {{
621 self.first = false;
622 hit(if item.is_some() {{ self.entered }} else {{ self.zero }});
623 }}
624 item
625 }}
626
627 #[inline(always)]
628 fn size_hint(&self) -> (usize, Option<usize>) {{
629 self.inner.size_hint()
630 }}
631 }}
632
633 #[inline(always)]
634 pub fn for_loop<I: IntoIterator>(
635 iterable: I,
636 zero: &'static str,
637 entered: &'static str,
638 ) -> ForLoop<I::IntoIter> {{
639 ForLoop {{ inner: iterable.into_iter(), first: true, zero, entered }}
640 }}
641
642 /// A `while` body ran: clear the loop's flag on the first entry.
643 #[inline(always)]
644 pub fn entered(first: &mut bool, id: &'static str) {{
645 if *first {{
646 *first = false;
647 hit(id);
648 }}
649 }}
650
651 /// A `while` loop is over: a flag still set means the body never ran.
652 #[inline(always)]
653 pub fn zero_iterations(first: bool, id: &'static str) {{
654 if first {{
655 hit(id);
656 }}
657 }}
658
659 /// The operand of `?`, recording which way the operator goes. Every type
660 /// `?` accepts on stable Rust implements this.
661 pub trait TryProbe: Sized {{
662 fn probe(self, continued: &'static str, returned: &'static str) -> Self;
663 }}
664
665 impl<T> TryProbe for Option<T> {{
666 #[inline(always)]
667 fn probe(self, continued: &'static str, returned: &'static str) -> Self {{
668 hit(if self.is_some() {{ continued }} else {{ returned }});
669 self
670 }}
671 }}
672
673 impl<T, E> TryProbe for Result<T, E> {{
674 #[inline(always)]
675 fn probe(self, continued: &'static str, returned: &'static str) -> Self {{
676 hit(if self.is_ok() {{ continued }} else {{ returned }});
677 self
678 }}
679 }}
680
681 impl<B, C> TryProbe for ControlFlow<B, C> {{
682 #[inline(always)]
683 fn probe(self, continued: &'static str, returned: &'static str) -> Self {{
684 hit(match self {{
685 ControlFlow::Continue(_) => continued,
686 _ => returned,
687 }});
688 self
689 }}
690 }}
691
692 impl<T, E> TryProbe for Poll<Result<T, E>> {{
693 #[inline(always)]
694 fn probe(self, continued: &'static str, returned: &'static str) -> Self {{
695 hit(match self {{
696 Poll::Ready(Err(_)) => returned,
697 _ => continued,
698 }});
699 self
700 }}
701 }}
702
703 impl<T, E> TryProbe for Poll<Option<Result<T, E>>> {{
704 #[inline(always)]
705 fn probe(self, continued: &'static str, returned: &'static str) -> Self {{
706 hit(match self {{
707 Poll::Ready(Some(Err(_))) => returned,
708 _ => continued,
709 }});
710 self
711 }}
712 }}
713
714 // Anything `!` turns into a bool: `assert!(was_seen)` compiles with a
715 // `&bool` (tokio iterates `for was_seen in &seen`), since the macro only
716 // negates its operand.
717 #[inline(always)]
718 pub fn condition<V: std::ops::Not<Output = bool>>(
719 value: V,
720 frame: &mut DecisionFrame,
721 index: usize,
722 ) -> bool {{
723 let value = !!value;
724 if index < frame.conditions && index < MAX_CONDITIONS {{
725 frame.set_value(index, if value {{ 2 }} else {{ 1 }});
726 }}
727 value
728 }}
729
730 #[inline(always)]
731 pub fn decision(value: bool, frame: &mut DecisionFrame) -> bool {{
732 if frame.recordable {{
733 record_decision(value, frame);
734 }}
735 value
736 }}
737
738 #[inline(never)]
739 fn record_decision(value: bool, frame: &DecisionFrame) {{
740 // `writer()` comes first so the file's mutex boxes its platform mutex
741 // while `OPENING` still makes re-entry harmless; the decision table
742 // itself is lock-free and allocates nothing.
743 if writer().is_none() || !first_decision(frame, value) {{
744 return;
745 }}
746 let mut record = [0u8; RECORD_CAPACITY];
747 let mut length = 0;
748 let mut fits = push(&mut record, &mut length, b"D\t")
749 && push_number(&mut record, &mut length, thread_ordinal())
750 && push(&mut record, &mut length, b"\t")
751 && push(&mut record, &mut length, frame.id.as_bytes())
752 && push(&mut record, &mut length, b"\t");
753 let mut index = 0;
754 while index < frame.conditions {{
755 fits = fits && push(&mut record, &mut length, &[b'0' + frame.value(index)]);
756 index += 1;
757 }}
758 fits = fits
759 && push(&mut record, &mut length, b"\t")
760 && push(&mut record, &mut length, if value {{ b"1" }} else {{ b"0" }})
761 && push(&mut record, &mut length, b"\n");
762 if fits {{
763 write_record(&record[..length]);
764 }}
765 }}
766}}
767"#
768 ))
769}
770
771pub fn parse_rust_probe_events(input: &[u8]) -> Result<Vec<RustProbeEntry>, RustProbeReadError> {
772 let text = std::str::from_utf8(input).map_err(|_| RustProbeReadError::InvalidHeader)?;
773 let mut lines = text.lines();
774 if lines.next() != Some(RUST_PROBE_MAGIC) {
775 return Err(RustProbeReadError::InvalidHeader);
776 }
777 let mut observations = Vec::new();
778 for (index, line) in lines.enumerate() {
779 let line_number = index + 2;
780 let fields = line.split('\t').collect::<Vec<_>>();
781 match fields.as_slice() {
782 ["H", thread, id] if valid_probe_id(id) && valid_thread(thread) => {
783 observations.push(RustProbeEntry {
784 thread: thread.parse().expect("a validated thread ordinal"),
785 observation: RustProbeObservation::Hit { id: (*id).into() },
786 })
787 }
788 ["A", thread, id] if valid_probe_id(id) && valid_thread(thread) => {
789 observations.push(RustProbeEntry {
790 thread: thread.parse().expect("a validated thread ordinal"),
791 observation: RustProbeObservation::Assertion { id: (*id).into() },
792 })
793 }
794 ["D", thread, id, digits, outcome]
795 if valid_probe_id(id)
796 && valid_thread(thread)
797 && id.starts_with("rs:decision:")
798 && !digits.is_empty()
799 && digits
800 .bytes()
801 .all(|digit| matches!(digit, b'0' | b'1' | b'2'))
802 && matches!(*outcome, "0" | "1") =>
803 {
804 observations.push(RustProbeEntry {
805 thread: thread.parse().expect("a validated thread ordinal"),
806 observation: RustProbeObservation::Decision {
807 id: (*id).into(),
808 values: digits
809 .bytes()
810 .map(|digit| match digit {
811 b'0' => None,
812 b'1' => Some(false),
813 b'2' => Some(true),
814 _ => unreachable!(),
815 })
816 .collect(),
817 outcome: *outcome == "1",
818 },
819 });
820 }
821 _ => return Err(RustProbeReadError::InvalidRecord(line_number)),
822 }
823 }
824 Ok(observations)
825}
826
827pub fn read_rust_probe_directory(
828 directory: &Path,
829) -> Result<BTreeMap<String, Vec<RustProbeEntry>>, RustProbeReadError> {
830 let mut files = fs::read_dir(directory)
831 .map_err(|error| RustProbeReadError::Io(error.to_string()))?
832 .collect::<Result<Vec<_>, _>>()
833 .map_err(|error| RustProbeReadError::Io(error.to_string()))?;
834 files.sort_by_key(|entry| entry.file_name());
835 let mut observations = BTreeMap::new();
836 for entry in files {
837 let name = entry
838 .file_name()
839 .into_string()
840 .map_err(|_| RustProbeReadError::UnsafeEntry("<non-utf8>".into()))?;
841 if Path::new(&name)
842 .components()
843 .any(|component| !matches!(component, Component::Normal(_)))
844 || !name.ends_with(".events")
845 {
846 return Err(RustProbeReadError::UnsafeEntry(name));
847 }
848 let metadata = fs::symlink_metadata(entry.path())
849 .map_err(|error| RustProbeReadError::Io(error.to_string()))?;
850 if !metadata.file_type().is_file() {
851 return Err(RustProbeReadError::UnsafeEntry(name));
852 }
853 let contents =
854 fs::read(entry.path()).map_err(|error| RustProbeReadError::Io(error.to_string()))?;
855 observations.insert(name, parse_rust_probe_events(&contents)?);
856 }
857 Ok(observations)
858}
859
860#[cfg(test)]
861mod tests {
862 use std::{
863 fs,
864 process::Command,
865 time::{SystemTime, UNIX_EPOCH},
866 };
867
868 use super::*;
869 use crate::rust_instrumenter::instrument_rust_source;
870
871 fn temporary_directory(name: &str) -> std::path::PathBuf {
872 let nonce = SystemTime::now()
873 .duration_since(UNIX_EPOCH)
874 .unwrap()
875 .as_nanos();
876 let path = std::env::temp_dir().join(format!(
877 "supercov-rust-runtime-{}-{nonce}-{name}",
878 std::process::id()
879 ));
880 fs::create_dir(&path).unwrap();
881 path
882 }
883
884 #[test]
885 fn generated_runtime_records_owned_points_and_exact_short_circuit_vectors() {
886 let source = r#"fn choose(first: bool, second: bool) -> i32 {
887 if first && second { 7 } else { 3 }
888}
889
890fn main() {
891 println!("{} {}", choose(false, true), choose(true, true));
892}
893"#;
894 let transformed =
895 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
896 let runtime =
897 render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
898 let directory = temporary_directory("record");
899 let input = directory.join("main.rs");
900 let binary = directory.join("program");
901 let evidence = directory.join("evidence");
902 fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
903 let compile = Command::new("rustc")
904 .arg("--edition=2024")
905 .arg(&input)
906 .arg("-o")
907 .arg(&binary)
908 .output()
909 .unwrap();
910 assert!(
911 compile.status.success(),
912 "{}",
913 String::from_utf8_lossy(&compile.stderr)
914 );
915 let output = Command::new(&binary)
916 .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
917 .output()
918 .unwrap();
919 assert!(output.status.success());
920 assert_eq!(output.stdout, b"3 7\n");
921 let files = read_rust_probe_directory(&evidence).unwrap();
922 assert_eq!(files.len(), 1);
923 let observations = files.values().next().unwrap();
924 let decisions = observations
925 .iter()
926 .filter_map(|entry| match &entry.observation {
927 RustProbeObservation::Decision {
928 values, outcome, ..
929 } => Some((values.clone(), *outcome)),
930 RustProbeObservation::Hit { .. } | RustProbeObservation::Assertion { .. } => None,
931 })
932 .collect::<Vec<_>>();
933 assert_eq!(
934 decisions,
935 [
936 (vec![Some(false), None], false),
937 (vec![Some(true), Some(true)], true)
938 ]
939 );
940 fs::remove_dir_all(directory).unwrap();
941 }
942
943 #[test]
944 fn generated_runtime_compiles_into_a_no_std_host_crate() {
945 let source = r#"#![no_std]
952
953extern crate std;
954
955fn choose(first: bool, second: bool) -> i32 {
956 if first && second { 7 } else { 3 }
957}
958
959fn main() {
960 std::println!("{} {}", choose(false, true), choose(true, true));
961}
962"#;
963 let transformed =
964 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
965 let runtime =
966 render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
967 let directory = temporary_directory("no-std");
968 let input = directory.join("main.rs");
969 let binary = directory.join("program");
970 let evidence = directory.join("evidence");
971 fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
972 let compile = Command::new("rustc")
973 .arg("--edition=2024")
974 .arg(&input)
975 .arg("-o")
976 .arg(&binary)
977 .output()
978 .unwrap();
979 assert!(
980 compile.status.success(),
981 "{}",
982 String::from_utf8_lossy(&compile.stderr)
983 );
984 let output = Command::new(&binary)
985 .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
986 .output()
987 .unwrap();
988 assert!(output.status.success());
989 assert_eq!(output.stdout, b"3 7\n");
990 let files = read_rust_probe_directory(&evidence).unwrap();
992 assert_eq!(files.len(), 1);
993 assert!(!files.values().next().unwrap().is_empty());
994 fs::remove_dir_all(directory).unwrap();
995 }
996
997 #[test]
998 fn probes_reached_through_a_global_allocator_do_not_recurse() {
999 let source = r#"use std::alloc::{GlobalAlloc, Layout, System};
1007use std::sync::atomic::{AtomicUsize, Ordering};
1008
1009static SEEN: AtomicUsize = AtomicUsize::new(0);
1010
1011fn note(size: usize) {
1012 if size > 0 {
1013 SEEN.fetch_add(1, Ordering::SeqCst);
1014 }
1015}
1016
1017struct Ledger;
1018
1019impl Ledger {
1020 fn record(&self, size: usize) {
1021 note(size);
1022 }
1023}
1024
1025unsafe impl GlobalAlloc for Ledger {
1026 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
1027 self.record(layout.size());
1028 System.alloc(layout)
1029 }
1030
1031 unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
1032 // dealloc must be instrumented too, or the test never exercises the
1033 // ladder that actually crashed: freeing the probe's OWN buffer re-enters
1034 // here, and a guard released before that free recurses without bound.
1035 self.record(layout.size());
1036 System.dealloc(pointer, layout);
1037 }
1038}
1039
1040#[global_allocator]
1041static LEDGER: Ledger = Ledger;
1042
1043fn classify(flag: bool) -> usize {
1044 if flag { 1 } else { 2 }
1045}
1046
1047fn main() {
1048 let held = std::vec![7u8; 32];
1049 println!("{} {}", classify(!held.is_empty()), held.len());
1050}
1051"#;
1052 let transformed =
1053 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
1054 assert!(
1057 transformed
1058 .code
1059 .contains("fn note(size: usize) {\ncrate::__supercov_runtime_v1::hit("),
1060 "the free function reached from the allocator should still be probed"
1061 );
1062 let runtime =
1063 render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
1064 let directory = temporary_directory("allocator-reentry");
1065 let input = directory.join("main.rs");
1066 let binary = directory.join("program");
1067 let evidence = directory.join("evidence");
1068 fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
1069 let compile = Command::new("rustc")
1070 .arg("--edition=2024")
1071 .arg(&input)
1072 .arg("-o")
1073 .arg(&binary)
1074 .output()
1075 .unwrap();
1076 assert!(
1077 compile.status.success(),
1078 "{}",
1079 String::from_utf8_lossy(&compile.stderr)
1080 );
1081 let output = Command::new(&binary)
1082 .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
1083 .output()
1084 .unwrap();
1085 assert!(
1086 output.status.success(),
1087 "instrumented allocator did not survive: {:?}",
1088 output.status
1089 );
1090 assert_eq!(output.stdout, b"1 32\n");
1091 let files = read_rust_probe_directory(&evidence).unwrap();
1092 let observations = files.values().next().unwrap();
1093 let decisions = observations
1094 .iter()
1095 .filter_map(|entry| match &entry.observation {
1096 RustProbeObservation::Decision {
1097 id,
1098 values,
1099 outcome,
1100 } => Some((id.clone(), values.clone(), *outcome)),
1101 RustProbeObservation::Hit { .. } | RustProbeObservation::Assertion { .. } => None,
1102 })
1103 .collect::<Vec<_>>();
1104 let classify = transformed
1107 .manifest
1108 .decisions
1109 .iter()
1110 .find(|decision| decision.source == "flag")
1111 .expect("classify's decision reached the manifest");
1112 assert!(
1113 decisions
1114 .iter()
1115 .any(|(id, values, outcome)| id == &classify.id
1116 && values == &[Some(true)]
1117 && *outcome),
1118 "classify's decision was lost: {decisions:?}"
1119 );
1120 let note = transformed
1123 .manifest
1124 .decisions
1125 .iter()
1126 .find(|decision| decision.source == "size > 0")
1127 .expect("note's decision reached the manifest");
1128 assert!(decisions.iter().any(|(id, ..)| id == ¬e.id));
1129 assert!(
1132 decisions.iter().all(|(_, values, _)| values.len() == 1),
1133 "a suppressed frame emitted a malformed vector: {decisions:?}"
1134 );
1135 fs::remove_dir_all(directory).unwrap();
1136 }
1137
1138 #[test]
1139 fn a_hit_in_a_loop_is_written_once_but_decisions_keep_every_vector() {
1140 let source = r#"fn step(value: usize) -> bool {
1146 let doubled = value * 2;
1147 doubled > 4
1148}
1149
1150fn main() {
1151 let mut seen = 0;
1152 for value in 0..64 {
1153 if step(value) { seen += 1; }
1154 }
1155 println!("{seen}");
1156}
1157"#;
1158 let transformed =
1159 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
1160 let runtime =
1161 render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
1162 let directory = temporary_directory("dedup");
1163 let input = directory.join("main.rs");
1164 let binary = directory.join("program");
1165 let evidence = directory.join("evidence");
1166 fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
1167 let compile = Command::new("rustc")
1168 .arg("--edition=2024")
1169 .arg(&input)
1170 .arg("-o")
1171 .arg(&binary)
1172 .output()
1173 .unwrap();
1174 assert!(
1175 compile.status.success(),
1176 "{}",
1177 String::from_utf8_lossy(&compile.stderr)
1178 );
1179 let output = Command::new(&binary)
1180 .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
1181 .output()
1182 .unwrap();
1183 assert_eq!(output.stdout, b"61\n");
1184 let files = read_rust_probe_directory(&evidence).unwrap();
1185 let observations = files.values().next().unwrap();
1186
1187 let mut hits = BTreeMap::<&str, usize>::new();
1189 for entry in observations {
1190 if let RustProbeObservation::Hit { id } = &entry.observation {
1191 *hits.entry(id.as_str()).or_default() += 1;
1192 }
1193 }
1194 assert!(!hits.is_empty(), "no hits recorded at all");
1195 assert!(
1196 hits.values().all(|count| *count == 1),
1197 "a repeated hit was written more than once: {hits:?}"
1198 );
1199
1200 let decisions = observations
1205 .iter()
1206 .filter_map(|entry| match &entry.observation {
1207 RustProbeObservation::Decision {
1208 values, outcome, ..
1209 } => Some((values.clone(), *outcome)),
1210 RustProbeObservation::Hit { .. } | RustProbeObservation::Assertion { .. } => None,
1211 })
1212 .collect::<Vec<_>>();
1213 assert_eq!(
1214 decisions,
1215 [(vec![Some(false)], false), (vec![Some(true)], true)],
1218 "both distinct vectors must survive, and neither may repeat"
1219 );
1220 fs::remove_dir_all(directory).unwrap();
1221 }
1222
1223 #[test]
1224 fn generated_runtime_survives_a_host_without_the_prelude() {
1225 let source = r#"#![no_implicit_prelude]
1230
1231fn parse(text: &str) -> ::std::option::Option<i32> {
1232 let value: i32 = text.parse().ok()?;
1233 ::std::option::Option::Some(value * 2)
1234}
1235
1236fn main() {
1237 ::std::println!("{:?} {:?}", parse("4"), parse("x"));
1238}
1239"#;
1240 let transformed =
1241 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
1242 assert!(transformed.code.contains("TryProbe"));
1244 let runtime =
1245 render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
1246 let directory = temporary_directory("no-implicit-prelude");
1247 let input = directory.join("main.rs");
1248 let binary = directory.join("program");
1249 fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
1250 let compile = Command::new("rustc")
1251 .arg("--edition=2024")
1252 .arg("--cap-lints=warn")
1253 .arg(&input)
1254 .arg("-o")
1255 .arg(&binary)
1256 .output()
1257 .unwrap();
1258 assert!(
1259 compile.status.success(),
1260 "{}",
1261 String::from_utf8_lossy(&compile.stderr)
1262 );
1263 let output = Command::new(&binary).output().unwrap();
1264 assert_eq!(output.stdout, b"Some(8) None\n");
1265 fs::remove_dir_all(directory).unwrap();
1266 }
1267
1268 #[test]
1269 fn generated_runtime_survives_a_deny_warnings_host() {
1270 let source = r#"#![deny(warnings)]
1275
1276fn choose(first: bool, second: bool) -> i32 {
1277 if first && second { 7 } else { 3 }
1278}
1279
1280fn main() {
1281 println!("{} {}", choose(false, true), choose(true, true));
1282}
1283"#;
1284 let transformed =
1285 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
1286 let runtime =
1287 render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
1288 let directory = temporary_directory("deny-warnings");
1289 let input = directory.join("main.rs");
1290 let binary = directory.join("program");
1291 fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
1292 let compile = Command::new("rustc")
1296 .arg("--edition=2024")
1297 .arg("--cap-lints=warn")
1298 .arg(&input)
1299 .arg("-o")
1300 .arg(&binary)
1301 .output()
1302 .unwrap();
1303 assert!(
1304 compile.status.success(),
1305 "{}",
1306 String::from_utf8_lossy(&compile.stderr)
1307 );
1308 let output = Command::new(&binary).output().unwrap();
1309 assert_eq!(output.stdout, b"3 7\n");
1310 fs::remove_dir_all(directory).unwrap();
1311 }
1312
1313 #[test]
1314 fn reader_rejects_truncation_invalid_digits_and_non_files() {
1315 assert_eq!(
1316 parse_rust_probe_events(
1317 b"SUPERCOV-RUST-PROBE-1\nD\trs:decision:0123456789abcdef01234567\t03\t1\n"
1318 ),
1319 Err(RustProbeReadError::InvalidRecord(2))
1320 );
1321 assert_eq!(
1322 parse_rust_probe_events(b"SUPERCOV-RUST-PROBE-"),
1323 Err(RustProbeReadError::InvalidHeader)
1324 );
1325
1326 let directory = temporary_directory("unsafe");
1327 fs::create_dir(directory.join("nested.events")).unwrap();
1328 assert!(matches!(
1329 read_rust_probe_directory(&directory),
1330 Err(RustProbeReadError::UnsafeEntry(_))
1331 ));
1332 fs::remove_dir_all(directory).unwrap();
1333 }
1334}