1use std::fmt;
25use std::io;
26use std::sync::Arc;
27use std::sync::Mutex;
28use std::sync::MutexGuard;
29
30use serde::Deserialize;
31use serde::Serialize;
32use sha2::Digest as _;
33use sha2::Sha256;
34
35use crate::Config;
36use crate::Detcore;
37use crate::FileMetadata;
38use crate::ThreadState;
39pub use crate::record_or_replay::NoopTool;
41use crate::types::DetPid;
42use crate::types::DetTid;
43use crate::types::FilesId;
44use crate::types::MmId;
45
46pub const DETCORE_EXEC_SNAPSHOT_VERSION: u16 = 2;
48
49pub const MAX_SNAPSHOT_BYTES: usize = 1024 * 1024;
51
52#[derive(Clone, Copy, Debug, Eq, PartialEq)]
54pub enum NativeExecCutError {
55 Identity(&'static str),
57 Configuration(&'static str),
59 ConfigDigest,
61 ConfigEncoding,
63 LiveCapability(&'static str),
65 LockUnavailable(&'static str),
67 PreparedState(&'static str),
69 Descriptor(&'static str),
71 Version,
73}
74
75impl fmt::Display for NativeExecCutError {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 let (category, field) = match *self {
78 Self::Identity(field) => ("identity mismatch", field),
79 Self::Configuration(field) => ("unsupported configuration", field),
80 Self::ConfigDigest => ("Config digest mismatch", "Config"),
81 Self::ConfigEncoding => ("Config encoding failed", "Config"),
82 Self::LiveCapability(field) => ("live runtime capability", field),
83 Self::LockUnavailable(field) => ("model locked or poisoned", field),
84 Self::PreparedState(field) => ("unprepared replacement state", field),
85 Self::Descriptor(field) => ("descriptor mismatch", field),
86 Self::Version => ("unsupported snapshot version", "version"),
87 };
88 write!(f, "native exec cut {category}: {field}")
89 }
90}
91
92impl std::error::Error for NativeExecCutError {}
93
94pub unsafe fn bind_native_initial_physical_identity(
102 thread: &mut ThreadState<()>,
103 physical_pid: i32,
104 physical_tid: i32,
105) -> Result<(), NativeExecCutError> {
106 if physical_tid <= 0
107 || physical_pid != physical_tid
108 || thread.dettid.as_raw() != physical_tid
109 || thread.physical_tid.is_some()
110 || thread.detpid.is_some()
111 || thread.thread_start_entered
112 || thread.mm_id != MmId::initial(thread.dettid)
113 || try_native_exec_lock(&thread.file_metadata, "initial file_metadata")?
114 .native_exec_files_id()
115 != FilesId::initial(thread.dettid)
116 {
117 return Err(NativeExecCutError::Identity("initial physical state"));
118 }
119 if thread.signal_task_identity.is_some() {
120 return Err(NativeExecCutError::LiveCapability("signal_task_identity"));
121 }
122 thread.physical_tid = Some(physical_tid);
123 Ok(())
124}
125
126pub fn bind_native_fresh_signal_identity(
130 thread: &mut ThreadState<()>,
131 mode: reverie::BackendSignalControlMode,
132 actual: Option<reverie::SignalTaskIdentity>,
133) -> Result<(), NativeExecCutError> {
134 if mode != reverie::BackendSignalControlMode::Unchanged
135 || thread.signal_task_identity.is_some()
136 || actual.is_some()
137 {
138 return Err(NativeExecCutError::LiveCapability("signal_task_identity"));
139 }
140 thread.signal_task_identity = actual;
141 Ok(())
142}
143
144#[derive(Clone, Copy, Debug, Eq, PartialEq)]
150pub struct NativeExecCutBinding {
151 pub dettid: DetTid,
153 pub detpid: DetPid,
155 pub physical_tid: i32,
157 pub old_mm: MmId,
159 pub old_files: FilesId,
161 pub config_digest: [u8; 32],
163}
164
165impl NativeExecCutBinding {
166 fn validate(&self) -> Result<(), NativeExecCutError> {
167 if self.dettid != self.detpid || self.detpid.as_raw() <= 0 {
168 return Err(NativeExecCutError::Identity("main-thread IDs"));
169 }
170 if self.physical_tid <= 0 {
171 return Err(NativeExecCutError::Identity("physical_tid"));
172 }
173 if self.old_mm.generation() == u64::MAX || self.old_files.generation() == u64::MAX {
174 return Err(NativeExecCutError::Identity("incarnation overflow"));
175 }
176 Ok(())
177 }
178
179 pub fn prepared_mm(&self) -> MmId {
181 self.old_mm.for_exec(self.detpid)
182 }
183
184 pub fn prepared_files(&self) -> FilesId {
186 self.old_files.for_exec(self.dettid)
187 }
188}
189
190#[derive(Debug)]
197pub struct NativeExecPreparationReceipt {
198 binding: NativeExecCutBinding,
199 old_table: Arc<Mutex<FileMetadata>>,
200}
201
202#[derive(Debug, Serialize)]
209pub struct DetcoreExecSnapshotRefV2<'a> {
210 version: u16,
211 dettid: DetTid,
212 detpid: DetPid,
213 prepared_mm: MmId,
214 config_digest: [u8; 32],
215 tool: &'a Detcore<NoopTool>,
216 thread: &'a ThreadState<()>,
217 #[serde(skip)]
218 _receipt: &'a NativeExecPreparationReceipt,
219}
220
221#[derive(Debug, Serialize, Deserialize)]
227pub struct DetcoreExecSnapshotV2 {
228 version: u16,
229 dettid: DetTid,
230 detpid: DetPid,
231 prepared_mm: MmId,
232 config_digest: [u8; 32],
233 tool: Detcore<NoopTool>,
234 thread: ThreadState<()>,
235}
236
237impl DetcoreExecSnapshotV2 {
238 pub fn validate(
245 &self,
246 binding: &NativeExecCutBinding,
247 expected_config: &Config,
248 ) -> Result<(), NativeExecCutError> {
249 binding.validate()?;
250 if self.version != DETCORE_EXEC_SNAPSHOT_VERSION {
251 return Err(NativeExecCutError::Version);
252 }
253 if self.dettid != binding.dettid || self.detpid != binding.detpid {
254 return Err(NativeExecCutError::Identity("snapshot IDs"));
255 }
256 if self.prepared_mm != binding.prepared_mm() {
257 return Err(NativeExecCutError::Identity("snapshot prepared_mm"));
258 }
259 validate_native_exec_host_configuration(expected_config)?;
263 if self.config_digest != binding.config_digest
264 || native_exec_config_digest(expected_config)? != binding.config_digest
265 {
266 return Err(NativeExecCutError::ConfigDigest);
267 }
268 validate_tool_and_thread(&self.tool, &self.thread, binding)?;
269 self.thread.validate_prepared_native_exec_cut(binding)
270 }
271
272 pub fn into_validated_parts(
274 self,
275 binding: &NativeExecCutBinding,
276 expected_config: &Config,
277 ) -> Result<(Detcore<NoopTool>, ThreadState<()>), NativeExecCutError> {
278 self.validate(binding, expected_config)?;
279 Ok((self.tool, self.thread))
280 }
281}
282
283pub fn validate_old_native_exec_cut(
287 tool: &Detcore<NoopTool>,
288 thread: &ThreadState<()>,
289 binding: &NativeExecCutBinding,
290) -> Result<NativeExecPreparationReceipt, NativeExecCutError> {
291 binding.validate()?;
292 validate_tool_and_thread(tool, thread, binding)?;
293 if thread.mm_id != binding.old_mm {
294 return Err(NativeExecCutError::Identity("old mm"));
295 }
296 {
297 let table = try_native_exec_lock(&thread.file_metadata, "old file_metadata")?;
298 table.validate_native_exec_cut(binding.old_files, false)?;
299 }
300 thread.validate_old_native_exec_robust_state()?;
301 Ok(NativeExecPreparationReceipt {
302 binding: *binding,
303 old_table: Arc::clone(&thread.file_metadata),
304 })
305}
306
307pub fn capture_prepared_native_exec<'a>(
312 tool: &'a Detcore<NoopTool>,
313 thread: &'a ThreadState<()>,
314 binding: &NativeExecCutBinding,
315 receipt: &'a NativeExecPreparationReceipt,
316) -> Result<DetcoreExecSnapshotRefV2<'a>, NativeExecCutError> {
317 binding.validate()?;
318 if receipt.binding != *binding {
319 return Err(NativeExecCutError::Identity("preparation receipt"));
320 }
321 {
322 let old = try_native_exec_lock(&receipt.old_table, "old file_metadata")?;
323 old.validate_native_exec_cut(binding.old_files, false)?;
324 }
325 if Arc::ptr_eq(&receipt.old_table, &thread.file_metadata) {
326 return Err(NativeExecCutError::PreparedState(
327 "replacement table ownership",
328 ));
329 }
330 validate_tool_and_thread(tool, thread, binding)?;
331 thread.validate_prepared_native_exec_cut(binding)?;
332 Ok(DetcoreExecSnapshotRefV2 {
333 version: DETCORE_EXEC_SNAPSHOT_VERSION,
334 dettid: binding.dettid,
335 detpid: binding.detpid,
336 prepared_mm: binding.prepared_mm(),
337 config_digest: binding.config_digest,
338 tool,
339 thread,
340 _receipt: receipt,
341 })
342}
343
344pub fn check_prepared_native_exec_bytes(
350 tool: &Detcore<NoopTool>,
351 thread: &ThreadState<()>,
352 binding: &NativeExecCutBinding,
353 receipt: &NativeExecPreparationReceipt,
354 expected: &[u8],
355) -> Result<(), NativeExecCutError> {
356 if expected.is_empty() || expected.len() > MAX_SNAPSHOT_BYTES {
357 return Err(NativeExecCutError::PreparedState("sealed snapshot bounds"));
358 }
359 let live = capture_prepared_native_exec(tool, thread, binding, receipt)?;
360 compare_encoding(&live, expected)
361}
362
363fn compare_encoding<T: Serialize + ?Sized>(
364 live: &T,
365 expected: &[u8],
366) -> Result<(), NativeExecCutError> {
367 struct CompareWriter<'a> {
368 expected: &'a [u8],
369 position: usize,
370 failed: bool,
371 }
372 impl io::Write for CompareWriter<'_> {
373 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
374 let end = self.position.checked_add(bytes.len());
375 if self.failed
376 || end.is_none_or(|end| self.expected.get(self.position..end) != Some(bytes))
377 {
378 self.failed = true;
379 return Err(io::ErrorKind::InvalidData.into());
380 }
381 self.position = end.unwrap();
382 Ok(bytes.len())
383 }
384 fn flush(&mut self) -> io::Result<()> {
385 Ok(())
386 }
387 }
388 let mut writer = CompareWriter {
389 expected,
390 position: 0,
391 failed: false,
392 };
393 let result =
394 bincode::serde::encode_into_std_write(live, &mut writer, bincode::config::legacy());
395 if result.is_err() || writer.failed || writer.position != expected.len() {
396 return Err(NativeExecCutError::PreparedState("sealed snapshot changed"));
397 }
398 Ok(())
399}
400
401fn validate_tool_and_thread(
402 tool: &Detcore<NoopTool>,
403 thread: &ThreadState<()>,
404 binding: &NativeExecCutBinding,
405) -> Result<(), NativeExecCutError> {
406 if tool.detpid != binding.detpid {
407 return Err(NativeExecCutError::Identity("Tool detpid"));
408 }
409 validate_native_exec_guest_configuration(&tool.cfg)?;
410 if native_exec_config_digest(&tool.cfg)? != binding.config_digest {
411 return Err(NativeExecCutError::ConfigDigest);
412 }
413 thread.validate_native_exec_cut(binding, &tool.cfg)
414}
415
416pub fn native_exec_config_digest(config: &Config) -> Result<[u8; 32], NativeExecCutError> {
421 struct HashWriter(Sha256);
422 impl io::Write for HashWriter {
423 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
424 self.0.update(bytes);
425 Ok(bytes.len())
426 }
427
428 fn flush(&mut self) -> io::Result<()> {
429 Ok(())
430 }
431 }
432 let mut writer = HashWriter(Sha256::new());
433 bincode::serde::encode_into_std_write(config, &mut writer, bincode::config::legacy())
434 .map_err(|_| NativeExecCutError::ConfigEncoding)?;
435 Ok(writer.0.finalize().into())
436}
437
438pub fn validate_native_exec_host_configuration(config: &Config) -> Result<(), NativeExecCutError> {
446 validate_native_exec_common_configuration(config)
447}
448
449pub fn validate_native_exec_guest_configuration(config: &Config) -> Result<(), NativeExecCutError> {
455 validate_native_exec_common_configuration(config)?;
456 if config.host_input_log.is_some() {
457 return Err(NativeExecCutError::Configuration("host_input_log"));
458 }
459 if config.fatal_core_capture.is_some() {
460 return Err(NativeExecCutError::Configuration("fatal_core_capture"));
461 }
462 Ok(())
463}
464
465fn validate_native_exec_common_configuration(config: &Config) -> Result<(), NativeExecCutError> {
466 if config.happens_before.is_some() {
469 return Err(NativeExecCutError::Configuration("happens_before"));
470 }
471 if config.untraced_code_range.is_some() {
473 return Err(NativeExecCutError::Configuration("untraced_code_range"));
474 }
475 if config.use_rcb_time() || config.max_timeslice.is_some() || config.target_timeslice.is_some()
476 {
477 return Err(NativeExecCutError::Configuration("RCB/timeslices"));
478 }
479 if config.imprecise_timers
480 || config.chaos
481 || config.chaos_target_races
482 || config.chaos_per_thread_slowdown
483 {
484 return Err(NativeExecCutError::Configuration("chaos/imprecise timers"));
485 }
486 if config.record_preemptions
487 || config.record_preemptions_to.is_some()
488 || config.replay_preemptions_from.is_some()
489 || config.replay_schedule_from.is_some()
490 || config.preemption_stacktrace
491 || config.preemption_stacktrace_log_file.is_some()
492 || !config.stacktrace_event.is_empty()
493 || config.stacktrace_signal.is_some()
494 {
495 return Err(NativeExecCutError::Configuration(
496 "preemption/replay/stacktrace",
497 ));
498 }
499 if config.recordreplay_modes || config.replaying || config.replay_data.is_some() {
500 return Err(NativeExecCutError::Configuration("record/replay"));
501 }
502 Ok(())
503}
504
505pub(crate) fn try_native_exec_lock<'a, T>(
506 lock: &'a Mutex<T>,
507 field: &'static str,
508) -> Result<MutexGuard<'a, T>, NativeExecCutError> {
509 lock.try_lock()
510 .map_err(|_| NativeExecCutError::LockUnavailable(field))
511}
512
513#[cfg(test)]
514pub(crate) mod tests {
515 use std::time::Duration;
516
517 use nix::fcntl::OFlag;
518 use rand::RngExt as _;
519
520 use super::*;
521 use crate::fd::DetFd;
522 use crate::fd::FdType;
523 use crate::memory::MemoryMetadata;
524 use crate::tool_local::ResourceLimit;
525 use crate::tool_local::RobustListWake;
526 use crate::types::FutexID;
527 use crate::types::LogicalTime;
528 use crate::types::OpenFileId;
529 use crate::types::RcbTimeMultiplier;
530 use crate::types::SharedMemoryObjectId;
531
532 pub(crate) fn fixture() -> (Detcore<NoopTool>, ThreadState<()>, NativeExecCutBinding) {
533 let owner = DetTid::from_raw(42);
534 let cfg = Config {
535 sequentialize_threads: true,
536 no_rcb_time: true,
537 max_timeslice: None,
538 target_timeslice: None,
539 rng_seed: Some(17),
540 sched_seed: Some(31),
541 clock_multiplier: Some(1.25),
542 ..Config::default()
543 };
544 let tool = Detcore {
545 detpid: owner,
546 cfg,
547 record_or_replay: NoopTool,
548 };
549 let mut thread = ThreadState::new(owner, &tool.cfg, ());
550 thread.detpid = Some(owner);
551 thread.physical_tid = Some(4242);
552 thread.thread_start_entered = true;
553 thread.past_global_first_execve = true;
554 let mut files = FileMetadata::new(owner);
555 for _ in 0..3 {
556 thread.mm_id = thread.mm_id.for_exec(owner);
557 files = files.for_exec(owner);
558 }
559 let old_files = files.files_id;
560 thread.file_metadata = Arc::new(Mutex::new(files));
561 let binding = NativeExecCutBinding {
562 dettid: owner,
563 detpid: owner,
564 physical_tid: 4242,
565 old_mm: thread.mm_id,
566 old_files,
567 config_digest: native_exec_config_digest(&tool.cfg).unwrap(),
568 };
569 (tool, thread, binding)
570 }
571
572 pub(crate) fn prepare(thread: &mut ThreadState<()>, binding: &NativeExecCutBinding) {
573 let files = thread
574 .file_metadata
575 .lock()
576 .unwrap()
577 .for_exec(binding.dettid);
578 thread.file_metadata = Arc::new(Mutex::new(files));
579 thread.mm_id = binding.prepared_mm();
580 thread.memory_metadata = Arc::new(Mutex::new(MemoryMetadata::new()));
581 thread.take_robust_list_for_exec();
582 }
583
584 pub(crate) fn state_bytes<T: Serialize + ?Sized>(state: &T) -> Vec<u8> {
589 bincode::serde::encode_to_vec(state, bincode::config::legacy()).unwrap()
590 }
591
592 fn encoded(
593 tool: &Detcore<NoopTool>,
594 thread: &ThreadState<()>,
595 binding: &NativeExecCutBinding,
596 receipt: &NativeExecPreparationReceipt,
597 ) -> Vec<u8> {
598 bincode::serde::encode_to_vec(
599 capture_prepared_native_exec(tool, thread, binding, receipt).unwrap(),
600 bincode::config::legacy(),
601 )
602 .unwrap()
603 }
604
605 pub(crate) fn decoded(bytes: &[u8]) -> DetcoreExecSnapshotV2 {
606 let (state, consumed) = bincode::serde::decode_from_slice(
607 bytes,
608 bincode::config::legacy().with_limit::<MAX_SNAPSHOT_BYTES>(),
609 )
610 .unwrap();
611 assert_eq!(consumed, bytes.len());
612 state
613 }
614
615 pub(crate) fn thread_for_test(snapshot: &mut DetcoreExecSnapshotV2) -> &mut ThreadState<()> {
616 &mut snapshot.thread
617 }
618
619 #[test]
620 fn native_exec_v2_refuses_v1_before_returning_owned_state() {
621 let (tool, mut thread, binding) = fixture();
622 let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
623 prepare(&mut thread, &binding);
624 let mut payload = decoded(&encoded(&tool, &thread, &binding, &receipt));
625 assert_eq!(payload.version, 2);
626 payload.version = 1;
627 assert_eq!(
628 payload.validate(&binding, &tool.cfg),
629 Err(NativeExecCutError::Version)
630 );
631 assert_eq!(
632 payload
633 .into_validated_parts(&binding, &tool.cfg)
634 .unwrap_err(),
635 NativeExecCutError::Version
636 );
637 }
638
639 #[test]
640 fn native_exec_rejects_a_replay_change_point_before_snapshot() {
641 let (tool, mut thread, binding) = fixture();
642 assert_eq!(thread.last_replay_change_point, None);
643 let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
644 thread.last_replay_change_point = Some(LogicalTime::from_nanos(1));
645 assert_eq!(
646 validate_old_native_exec_cut(&tool, &thread, &binding).unwrap_err(),
647 NativeExecCutError::LiveCapability("RCB/replay/chaos state")
648 );
649 thread.last_replay_change_point = None;
650 prepare(&mut thread, &binding);
651 capture_prepared_native_exec(&tool, &thread, &binding, &receipt).unwrap();
652 thread.last_replay_change_point = Some(LogicalTime::from_nanos(1));
653 assert_eq!(
654 capture_prepared_native_exec(&tool, &thread, &binding, &receipt).unwrap_err(),
655 NativeExecCutError::LiveCapability("RCB/replay/chaos state")
656 );
657 }
658
659 #[test]
660 fn native_exec_final_exact_comparison_keeps_state_and_refuses_every_byte_change() {
661 let (tool, mut thread, binding) = fixture();
662 let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
663 prepare(&mut thread, &binding);
664 let bytes = encoded(&tool, &thread, &binding, &receipt);
665 let before = state_bytes(&thread);
666 check_prepared_native_exec_bytes(&tool, &thread, &binding, &receipt, &bytes).unwrap();
667 assert_eq!(state_bytes(&thread), before);
668 for index in 0..bytes.len() {
669 let mut bad = bytes.clone();
670 bad[index] ^= 1;
671 assert!(
672 check_prepared_native_exec_bytes(&tool, &thread, &binding, &receipt, &bad).is_err(),
673 "byte {index}"
674 );
675 }
676 for end in 0..bytes.len() {
677 assert!(
678 check_prepared_native_exec_bytes(&tool, &thread, &binding, &receipt, &bytes[..end])
679 .is_err(),
680 "truncation {end}"
681 );
682 }
683 let mut extra = bytes.clone();
684 extra.push(0);
685 assert!(
686 check_prepared_native_exec_bytes(&tool, &thread, &binding, &receipt, &extra).is_err()
687 );
688 assert_eq!(state_bytes(&thread), before);
689 let _ = thread.prng.random::<u64>();
690 assert!(
691 check_prepared_native_exec_bytes(&tool, &thread, &binding, &receipt, &bytes).is_err()
692 );
693 let changed = encoded(&tool, &thread, &binding, &receipt);
694 check_prepared_native_exec_bytes(&tool, &thread, &binding, &receipt, &changed).unwrap();
695 thread.stats.count_syscall(reverie::syscalls::Sysno::getpid);
696 assert!(
697 check_prepared_native_exec_bytes(&tool, &thread, &binding, &receipt, &changed).is_err()
698 );
699 }
700
701 #[test]
702 fn native_exec_final_comparison_latches_ignored_serializer_errors() {
703 struct Ignores;
704 impl Serialize for Ignores {
705 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
706 use serde::ser::SerializeTuple;
707 let mut tuple = serializer.serialize_tuple(2)?;
708 let _ = tuple.serialize_element(&99u64);
709 let _ = tuple.serialize_element(&17u64);
710 tuple.end()
711 }
712 }
713 assert!(compare_encoding(&Ignores, &17u64.to_le_bytes()).is_err());
714 let valid = state_bytes(&(99u64, 17u64));
715 compare_encoding(&Ignores, &valid).unwrap();
716 }
717
718 #[test]
719 fn native_exec_roundtrip_continues_full_noninitial_clock_accounting_and_prngs() {
720 let (tool, mut thread, binding) = fixture();
721 for _ in 0..13 {
722 let _ = thread.prng.random::<u64>();
723 }
724 for _ in 0..19 {
725 let _ = thread.chaos_prng.random::<u64>();
726 }
727 thread.pedigree = thread.pedigree.fork().1.fork().0;
728 thread.stats.count_syscall(reverie::syscalls::Sysno::getpid);
729 thread.stats.count_syscall(reverie::syscalls::Sysno::getpid);
730 thread.stats.count_signal();
731 thread.stats.timeslice_stats.record(173);
732 thread.timer_slack_ns = 137;
733 thread.default_timer_slack_ns = 91;
734 thread.resource_limits.lock().unwrap().set(
735 libc::RLIMIT_NOFILE,
736 ResourceLimit {
737 current: 97,
738 maximum: 211,
739 },
740 );
741 thread.thread_logical_time.add_syscall_with_cost(137);
742 thread
743 .thread_logical_time
744 .add_rcbs_with_multiplier(7, RcbTimeMultiplier::from_f64(1.5));
745 thread.thread_logical_time.add_cpuid();
746 thread.thread_logical_time = thread.thread_logical_time.clone_for_child();
747 thread.thread_cpu_start_user_time = thread.thread_logical_time.user_cpu_time();
748 thread.thread_cpu_start_system_time = thread.thread_logical_time.system_cpu_time();
749 thread.account_process_cpu_time();
750 thread.thread_logical_time.add_syscall_with_cost(19); thread.thread_logical_time.add_rdtsc();
752 thread.committed_clock_value = 7;
753 let cut_time = thread.thread_logical_time.as_nanos() + Duration::from_nanos(43);
754 thread.thread_logical_time.advance_to(cut_time);
755 thread.observe_guest_clock(cut_time + Duration::from_nanos(23));
756 thread.end_of_timeslice = Some(cut_time + Duration::from_nanos(101));
757 thread.last_grant_deadline = Some(Duration::from_nanos(211));
758 let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
759 prepare(&mut thread, &binding);
760 let before = state_bytes(&thread);
761 let bytes = encoded(&tool, &thread, &binding, &receipt);
762 assert_eq!(
763 encoded(&tool, &thread, &binding, &receipt),
764 bytes,
765 "validation must not advance or normalize the live state"
766 );
767 let payload = decoded(&bytes);
768 payload.validate(&binding, &tool.cfg).unwrap();
769 assert_eq!(state_bytes(&payload.thread), before);
770 assert_ne!(
771 payload.thread.thread_logical_time.inherited_nanos(),
772 LogicalTime::ZERO
773 );
774 assert_eq!(payload.thread.thread_logical_time.as_nanos(), cut_time);
775 assert_eq!(
776 payload.thread.observe_guest_clock(LogicalTime::ZERO),
777 cut_time + Duration::from_nanos(23)
778 );
779 let mut oracle = decoded(&bytes).thread;
782 let (_, mut restored) = payload.into_validated_parts(&binding, &tool.cfg).unwrap();
783 for cost in [1, 7, 103, 29, 2, 71, 11, 43] {
784 assert_eq!(restored.prng.random::<u64>(), oracle.prng.random::<u64>());
785 assert_eq!(
786 restored.chaos_prng.random::<u64>(),
787 oracle.chaos_prng.random::<u64>()
788 );
789 let previous = restored.thread_logical_time.as_nanos();
790 restored.thread_logical_time.add_syscall_with_cost(cost);
791 oracle.thread_logical_time.add_syscall_with_cost(cost);
792 restored.thread_logical_time.add_cpuid();
793 oracle.thread_logical_time.add_cpuid();
794 assert!(restored.thread_logical_time.as_nanos() > previous);
795 assert_eq!(
796 state_bytes(&restored.thread_logical_time),
797 state_bytes(&oracle.thread_logical_time)
798 );
799 let now = restored.thread_logical_time.as_nanos();
800 assert_eq!(
801 restored.observe_guest_clock(now),
802 oracle.observe_guest_clock(now)
803 );
804 restored.account_process_cpu_time();
805 oracle.account_process_cpu_time();
806 let accounted = state_bytes(&*restored.process_cpu_time.lock().unwrap());
807 restored.account_process_cpu_time();
808 assert_eq!(
809 state_bytes(&*restored.process_cpu_time.lock().unwrap()),
810 accounted
811 );
812 assert_eq!(
813 accounted,
814 state_bytes(&*oracle.process_cpu_time.lock().unwrap())
815 );
816 assert_eq!(state_bytes(&restored), state_bytes(&oracle));
817 }
818 }
819
820 #[test]
821 fn native_exec_prepared_aliases_cloexec_and_old_receipt_are_preserved() {
822 let (tool, mut thread, binding) = fixture();
823 thread.add_fd(30, OFlag::O_RDWR, FdType::Rng, None).unwrap();
824 thread.dup_fd(30, 31, OFlag::empty()).unwrap();
825 thread.dup_fd(30, 32, OFlag::O_CLOEXEC).unwrap();
826 thread
827 .add_fd(33, OFlag::O_CLOEXEC, FdType::Rng, None)
828 .unwrap();
829 thread
830 .add_fd(34, OFlag::empty(), FdType::Rng, None)
831 .unwrap();
832 thread
833 .with_detfd(31, |fd| fd.advance_random_device_offset(37))
834 .unwrap();
835 let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
836 prepare(&mut thread, &binding);
837 assert!(
839 receipt
840 .old_table
841 .lock()
842 .unwrap()
843 .file_handles
844 .contains_key(&33)
845 );
846 let old_prepared = serde_json::to_value(&*thread.file_metadata.lock().unwrap()).unwrap();
847 let payload = decoded(&encoded(&tool, &thread, &binding, &receipt));
848 payload.validate(&binding, &tool.cfg).unwrap();
849 let table = payload.thread.file_metadata.lock().unwrap();
850 assert_eq!(serde_json::to_value(&*table).unwrap(), old_prepared);
851 assert!(!table.file_handles.contains_key(&32));
852 assert!(!table.file_handles.contains_key(&33));
853 let first = table.file_handles[&30]
854 .validate_native_exec_runtime_state()
855 .unwrap();
856 let alias = table.file_handles[&31]
857 .validate_native_exec_runtime_state()
858 .unwrap();
859 let distinct = table.file_handles[&34]
860 .validate_native_exec_runtime_state()
861 .unwrap();
862 assert_eq!(first, alias);
863 assert_ne!(first.0, distinct.0);
864 assert_ne!(first.1, distinct.1);
865 table.file_handles[&31].advance_random_device_offset(11);
866 assert_eq!(table.file_handles[&30].random_device_offset(), 48);
867 assert_eq!(table.file_handles[&34].random_device_offset(), 0);
868 drop(table);
869 for (slot, kind) in [(40, FdType::Rng), (41, FdType::Socket), (42, FdType::Rng)] {
870 thread.add_fd(slot, OFlag::empty(), kind, None).unwrap();
871 payload
872 .thread
873 .add_fd(slot, OFlag::empty(), kind, None)
874 .unwrap();
875 assert_eq!(
876 thread.with_detfd(slot, |fd| fd.open_file_id()).unwrap(),
877 payload
878 .thread
879 .with_detfd(slot, |fd| fd.open_file_id())
880 .unwrap(),
881 "both independent OFD allocators must continue their saved sequence"
882 );
883 }
884 let lock = receipt.old_table.lock().unwrap().file_handles[&33].directory_lock();
887 let lease = lock.try_lock().unwrap();
888 assert!(matches!(
889 capture_prepared_native_exec(&tool, &thread, &binding, &receipt),
890 Err(NativeExecCutError::LiveCapability("directory lease"))
891 ));
892 drop(lease);
893 capture_prepared_native_exec(&tool, &thread, &binding, &receipt).unwrap();
894 }
895
896 #[test]
897 fn native_exec_live_alias_conflict_is_refused_before_interning() {
898 let (tool, thread, binding) = fixture();
899 let id = OpenFileId::new(binding.dettid, 7);
900 let mut table = thread.file_metadata.lock().unwrap();
901 table
902 .file_handles
903 .insert(3, DetFd::new(3, OFlag::empty(), FdType::Rng, id));
904 table
905 .file_handles
906 .insert(4, DetFd::new(4, OFlag::empty(), FdType::Rng, id));
907 drop(table);
908 assert!(matches!(
909 validate_old_native_exec_cut(&tool, &thread, &binding),
910 Err(NativeExecCutError::Descriptor("OFD alias identity"))
911 ));
912 }
913
914 #[test]
915 fn native_exec_staged_robust_wakes_keep_old_keys_and_are_consumed_once() {
916 let (tool, mut thread, binding) = fixture();
917 thread.record_robust_list_head(Some(0x123000));
918 let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
919 prepare(&mut thread, &binding);
920 let shared = SharedMemoryObjectId::File {
921 device: 7,
922 inode: 11,
923 };
924 thread.stage_robust_list_wakes_for_exec(vec![(
925 binding.dettid,
926 vec![
927 RobustListWake {
928 futex: FutexID::private(binding.old_mm, 0x456000),
929 },
930 RobustListWake {
931 futex: FutexID::shared(shared, 64),
932 },
933 RobustListWake {
934 futex: FutexID::private(binding.old_mm, 0x456008),
935 },
936 ],
937 )]);
938 let payload = decoded(&encoded(&tool, &thread, &binding, &receipt));
939 payload.validate(&binding, &tool.cfg).unwrap();
940 assert_eq!(state_bytes(&payload.thread), state_bytes(&thread));
941 let actual = payload.thread.take_robust_list_wakes_for_exec();
942 assert_eq!(actual, thread.take_robust_list_wakes_for_exec());
943 assert_eq!(actual.len(), 3);
944 assert!(payload.thread.take_robust_list_wakes_for_exec().is_empty());
945 assert_ne!(
946 FutexID::private(binding.old_mm, 0x456000),
947 FutexID::private(binding.prepared_mm(), 0x456000)
948 );
949 }
950
951 #[test]
952 fn native_exec_binding_config_and_prepared_identity_mismatches_refuse() {
953 let (tool, mut thread, binding) = fixture();
954 let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
955 prepare(&mut thread, &binding);
956 let bytes = encoded(&tool, &thread, &binding, &receipt);
957 let mut changed_config = tool.cfg.clone();
958 changed_config.rng_seed = Some(23);
959 assert_eq!(
960 decoded(&bytes).validate(&binding, &changed_config),
961 Err(NativeExecCutError::ConfigDigest)
962 );
963 for field in 0..7 {
964 let mut payload = decoded(&bytes);
965 match field {
966 0 => payload.version += 1,
967 1 => payload.dettid = DetTid::from_raw(43),
968 2 => payload.detpid = DetTid::from_raw(43),
969 3 => payload.prepared_mm = binding.old_mm,
970 4 => payload.config_digest[0] ^= 1,
971 5 => payload.tool.detpid = DetTid::from_raw(43),
972 6 => payload.thread.physical_tid = Some(4243),
973 _ => unreachable!(),
974 }
975 assert!(
976 payload.validate(&binding, &tool.cfg).is_err(),
977 "field {field}"
978 );
979 }
980 let mut other_binding = binding;
981 other_binding.physical_tid += 1;
982 assert!(capture_prepared_native_exec(&tool, &thread, &other_binding, &receipt).is_err());
983 thread.mm_id = binding.old_mm;
984 assert!(capture_prepared_native_exec(&tool, &thread, &binding, &receipt).is_err());
985 }
986
987 #[test]
988 fn native_exec_guest_config_refuses_skipped_values_with_equal_wire_bytes() {
989 for field in [
990 "happens_before",
991 "host_input_log",
992 "fatal_core_capture",
993 "untraced_code_range",
994 ] {
995 let (mut tool, mut thread, binding) = fixture();
996 let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
997 prepare(&mut thread, &binding);
998 let mut payload = decoded(&encoded(&tool, &thread, &binding, &receipt));
999 let expected_config = tool.cfg.clone();
1000 let wire = state_bytes(&tool.cfg);
1001 set_skipped_config_field(&mut tool.cfg, field);
1002 set_skipped_config_field(&mut payload.tool.cfg, field);
1003 assert_eq!(state_bytes(&tool.cfg), wire, "field {field}");
1004 assert_eq!(state_bytes(&payload.tool.cfg), wire, "field {field}");
1005 assert_eq!(
1006 native_exec_config_digest(&tool.cfg).unwrap(),
1007 binding.config_digest,
1008 "the wire digest alone cannot establish this policy"
1009 );
1010 assert_eq!(
1011 capture_prepared_native_exec(&tool, &thread, &binding, &receipt).unwrap_err(),
1012 NativeExecCutError::Configuration(field),
1013 "producer must refuse {field} before encoding loses it"
1014 );
1015 assert_eq!(
1016 payload.validate(&binding, &expected_config),
1017 Err(NativeExecCutError::Configuration(field)),
1018 "consumer must refuse live skipped value {field}"
1019 );
1020 }
1021 }
1022
1023 #[test]
1024 fn native_exec_expected_config_cannot_hide_unsupported_host_state() {
1025 for field in ["happens_before", "untraced_code_range"] {
1026 let (tool, mut thread, binding) = fixture();
1027 let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
1028 prepare(&mut thread, &binding);
1029 let payload = decoded(&encoded(&tool, &thread, &binding, &receipt));
1030 let mut expected_config = tool.cfg.clone();
1031 set_skipped_config_field(&mut expected_config, field);
1032 assert_eq!(state_bytes(&expected_config), state_bytes(&tool.cfg));
1033 assert_eq!(
1034 payload.validate(&binding, &expected_config),
1035 Err(NativeExecCutError::Configuration(field)),
1036 "independent admission must not lose {field} through its digest"
1037 );
1038 }
1039 }
1040
1041 #[test]
1042 fn native_exec_expected_config_keeps_host_only_verification_outputs() {
1043 let (mut tool, mut thread, mut binding) = fixture();
1044 tool.cfg.record_host_inputs = true;
1045 binding.config_digest = native_exec_config_digest(&tool.cfg).unwrap();
1046 let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
1047 prepare(&mut thread, &binding);
1048 let payload = decoded(&encoded(&tool, &thread, &binding, &receipt));
1049 let mut expected_config = tool.cfg.clone();
1050 set_skipped_config_field(&mut expected_config, "host_input_log");
1051 set_skipped_config_field(&mut expected_config, "fatal_core_capture");
1052 let host_input_log = expected_config.host_input_log.clone();
1053 let fatal_core_capture = expected_config.fatal_core_capture.clone();
1054 assert_eq!(state_bytes(&expected_config), state_bytes(&tool.cfg));
1055 payload.validate(&binding, &expected_config).unwrap();
1056 assert_eq!(expected_config.host_input_log, host_input_log);
1057 assert_eq!(expected_config.fatal_core_capture, fatal_core_capture);
1058 assert!(payload.tool.cfg.host_input_log.is_none());
1059 assert!(payload.tool.cfg.fatal_core_capture.is_none());
1060 assert!(payload.tool.cfg.record_host_inputs);
1061 }
1062
1063 #[test]
1064 fn native_exec_empty_happens_before_is_still_an_unsupported_skipped_value() {
1065 let (mut tool, thread, binding) = fixture();
1066 tool.cfg.happens_before = Some(
1067 detcore_model::happens_before::HappensBeforeSpec::from_dsl("")
1068 .unwrap()
1069 .normalize()
1070 .unwrap(),
1071 );
1072 assert_eq!(
1073 native_exec_config_digest(&tool.cfg).unwrap(),
1074 binding.config_digest
1075 );
1076 assert_eq!(
1077 validate_old_native_exec_cut(&tool, &thread, &binding).unwrap_err(),
1078 NativeExecCutError::Configuration("happens_before")
1079 );
1080 }
1081
1082 fn set_skipped_config_field(config: &mut Config, field: &str) {
1083 match field {
1084 "happens_before" => {
1085 config.happens_before = Some(
1086 detcore_model::happens_before::HappensBeforeSpec::from_dsl("42:sc=1 < 42:sc=2")
1087 .unwrap()
1088 .normalize()
1089 .unwrap(),
1090 );
1091 }
1092 "host_input_log" => config.host_input_log = Some("native-inputs.jsonl".into()),
1093 "fatal_core_capture" => {
1094 config.fatal_core_capture = Some(detcore_model::config::FatalCoreCapture {
1095 dir: "native-core-output".into(),
1096 file_prefix: "native-exec-test-".into(),
1097 max_core_bytes: 1024,
1098 max_total_bytes: 4096,
1099 time_limit: Duration::from_secs(1),
1100 });
1101 }
1102 "untraced_code_range" => config.untraced_code_range = Some((0x1000, 0x2000)),
1103 _ => panic!("unknown skipped Config field {field}"),
1104 }
1105 }
1106
1107 #[test]
1108 fn native_exec_unsupported_configuration_remains_a_refusal() {
1109 for field in 0..10 {
1110 let (mut tool, thread, mut binding) = fixture();
1111 match field {
1112 0 => {
1113 tool.cfg.no_rcb_time = false;
1114 tool.cfg.max_timeslice = std::num::NonZeroU64::new(10);
1115 }
1116 1 => tool.cfg.max_timeslice = std::num::NonZeroU64::new(1),
1117 2 => tool.cfg.target_timeslice = std::num::NonZeroU64::new(1),
1118 3 => tool.cfg.chaos = true,
1119 4 => tool.cfg.imprecise_timers = true,
1120 5 => tool.cfg.record_preemptions = true,
1121 6 => tool.cfg.preemption_stacktrace = true,
1122 7 => tool.cfg.recordreplay_modes = true,
1123 8 => tool.cfg.replaying = true,
1124 9 => tool.cfg.replay_data = Some("unopened".into()),
1125 _ => unreachable!(),
1126 }
1127 binding.config_digest = native_exec_config_digest(&tool.cfg).unwrap();
1128 assert!(
1129 matches!(
1130 validate_old_native_exec_cut(&tool, &thread, &binding),
1131 Err(NativeExecCutError::Configuration(_))
1132 ),
1133 "field {field}"
1134 );
1135 }
1136 }
1137
1138 #[test]
1139 fn native_initial_binding_keeps_lifecycle_unstarted_and_is_one_use() {
1140 let owner = DetTid::from_raw(41);
1143 let mut thread = ThreadState::new(owner, &Config::default(), ());
1144 unsafe { bind_native_initial_physical_identity(&mut thread, 41, 41) }.unwrap();
1145 assert_eq!(thread.physical_tid, Some(41));
1146 assert_eq!(thread.detpid, None);
1147 assert!(!thread.thread_start_entered);
1148 assert_eq!(thread.mm_id, MmId::initial(owner));
1149 assert_eq!(
1150 thread.file_metadata.lock().unwrap().files_id,
1151 FilesId::initial(owner)
1152 );
1153 assert!(unsafe { bind_native_initial_physical_identity(&mut thread, 41, 41) }.is_err());
1154 for field in 0..6 {
1155 let mut thread = ThreadState::new(owner, &Config::default(), ());
1156 let (pid, tid) = match field {
1157 0 => (42, 41),
1158 1 => (42, 42),
1159 2 => (0, 0),
1160 3 => {
1161 thread.detpid = Some(owner);
1162 (41, 41)
1163 }
1164 4 => {
1165 thread.thread_start_entered = true;
1166 (41, 41)
1167 }
1168 5 => {
1169 thread.mm_id = thread.mm_id.for_exec(owner);
1170 (41, 41)
1171 }
1172 _ => unreachable!(),
1173 };
1174 assert!(
1175 unsafe { bind_native_initial_physical_identity(&mut thread, pid, tid) }.is_err(),
1176 "field {field}"
1177 );
1178 assert_eq!(thread.physical_tid, None);
1179 }
1180 }
1181
1182 #[test]
1183 fn native_fresh_signal_binding_refuses_carried_or_actual_capability() {
1184 let owner = DetTid::from_raw(41);
1185 let identity = reverie::SignalTaskIdentity {
1186 process: reverie::SignalProcessId {
1187 tgid: reverie::Pid::from_raw(41),
1188 generation: 0,
1189 },
1190 tid: reverie::Pid::from_raw(41),
1191 task_generation: 0,
1192 };
1193 let mut thread = ThreadState::new(owner, &Config::default(), ());
1194 bind_native_fresh_signal_identity(
1195 &mut thread,
1196 reverie::BackendSignalControlMode::Unchanged,
1197 None,
1198 )
1199 .unwrap();
1200 assert_eq!(thread.signal_task_identity, None);
1201 assert!(
1202 bind_native_fresh_signal_identity(
1203 &mut thread,
1204 reverie::BackendSignalControlMode::Unchanged,
1205 Some(identity)
1206 )
1207 .is_err()
1208 );
1209 assert_eq!(thread.signal_task_identity, None);
1210 thread.signal_task_identity = Some(identity);
1211 assert!(
1212 bind_native_fresh_signal_identity(
1213 &mut thread,
1214 reverie::BackendSignalControlMode::Unchanged,
1215 None
1216 )
1217 .is_err()
1218 );
1219 assert_eq!(thread.signal_task_identity, Some(identity));
1220 thread.signal_task_identity = None;
1221 assert!(
1222 bind_native_fresh_signal_identity(
1223 &mut thread,
1224 reverie::BackendSignalControlMode::ToolControlled,
1225 None
1226 )
1227 .is_err()
1228 );
1229 }
1230}