1use detcore_model::pid::GuestIds;
16use serde::Deserialize;
17use serde::Serialize;
18
19use super::PreemptionRecord;
20
21const CONTRACT: &str = "sealed-private-input-v1";
22const BASE: &str = "0x71010000";
23
24#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
29pub struct ReservedInputMetadata(Option<serde_json::Value>);
30
31impl ReservedInputMetadata {
32 pub(super) fn is_missing(&self) -> bool {
33 self.0.is_none()
34 }
35
36 pub fn as_json(&self) -> Option<&serde_json::Value> {
38 self.0.as_ref()
39 }
40
41 pub fn require_same_for_chosen(&self, other: &Self) -> Result<(), RecordAdmissionRefusal> {
44 let first = self.recognized("first transformation input")?;
45 let second = other.recognized("second transformation input")?;
46 if first != second {
47 return Err(provenance_refusal(
48 "transformation inputs",
49 "mixed reserved-input layouts",
50 ));
51 }
52 Ok(())
53 }
54
55 fn recognized(&self, source: &str) -> Result<ReservedInputLayout, RecordAdmissionRefusal> {
56 let value = self.0.as_ref().ok_or_else(|| {
57 provenance_refusal(
58 source,
59 "missing reserved_input provenance (legacy or unqualified recording)",
60 )
61 })?;
62 let layout: ReservedInputLayout =
63 serde_json::from_value(value.clone()).map_err(|error| {
64 provenance_refusal(
65 source,
66 &format!("malformed or unknown reserved_input descriptor: {error}"),
67 )
68 })?;
69 layout.require_supported(source)?;
70 Ok(layout)
71 }
72
73 pub(super) fn require_matching_layout(
74 &self,
75 source: &str,
76 expected: &ReservedInputLayout,
77 ) -> Result<(), RecordAdmissionRefusal> {
78 let actual = self.recognized(source)?;
79 if actual != *expected {
80 return Err(provenance_refusal(
81 source,
82 &format!(
83 "reserved-input layout differs: recorded ABI {} at {} with {} bytes, runtime ABI {} at {} with {} bytes",
84 actual.abi,
85 actual.base,
86 actual.page_bytes,
87 expected.abi,
88 expected.base,
89 expected.page_bytes,
90 ),
91 ));
92 }
93 Ok(())
94 }
95}
96
97impl Serialize for ReservedInputMetadata {
98 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
99 self.0.serialize(serializer)
100 }
101}
102
103impl<'de> Deserialize<'de> for ReservedInputMetadata {
104 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
105 serde_json::Value::deserialize(deserializer).map(|value| Self(Some(value)))
107 }
108}
109
110#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
114#[serde(deny_unknown_fields)]
115pub(crate) struct ReservedInputLayout {
116 contract: String,
117 base: String,
118 page_bytes: u64,
119 abi: String,
120}
121
122impl ReservedInputLayout {
123 pub(crate) fn from_authenticated_setup(
127 abi: &str,
128 base: u64,
129 page_bytes: u64,
130 ) -> Result<Self, RecordAdmissionRefusal> {
131 let layout = Self {
132 contract: CONTRACT.to_owned(),
133 base: format!("0x{base:x}"),
134 page_bytes,
135 abi: abi.to_owned(),
136 };
137 layout.require_supported("authenticated target setup")?;
138 Ok(layout)
139 }
140
141 fn require_supported(&self, source: &str) -> Result<(), RecordAdmissionRefusal> {
142 if self.contract != CONTRACT {
143 return Err(provenance_refusal(
144 source,
145 "unknown reserved-input contract",
146 ));
147 }
148 if self.base != BASE
149 || !matches!(self.abi.as_str(), "x86_64" | "aarch64")
150 || !matches!(self.page_bytes, 4096 | 16384 | 65536)
151 {
152 return Err(provenance_refusal(
153 source,
154 "unsupported reserved-input ABI, address or page extent",
155 ));
156 }
157 Ok(())
158 }
159
160 fn native() -> Result<Self, RecordAdmissionRefusal> {
161 let abi = std::env::consts::ARCH;
162 let page_bytes = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
165 let page_bytes = u64::try_from(page_bytes).map_err(|_| {
166 provenance_refusal("runtime", "cannot establish the native page extent")
167 })?;
168 Self::from_authenticated_setup(abi, 0x7101_0000, page_bytes)
169 }
170
171 fn metadata(&self) -> ReservedInputMetadata {
172 ReservedInputMetadata(Some(serde_json::to_value(self).expect("layout serializes")))
173 }
174}
175
176#[derive(Debug, Clone, Eq, PartialEq)]
178pub struct RecordAdmissionRefusal(pub String);
179
180impl std::fmt::Display for RecordAdmissionRefusal {
181 fn fmt(&self, output: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182 output.write_str(&self.0)
183 }
184}
185
186impl std::error::Error for RecordAdmissionRefusal {}
187
188fn provenance_refusal(source: &str, reason: &str) -> RecordAdmissionRefusal {
189 RecordAdmissionRefusal(format!(
190 "HERMIT_CHOSEN_JSON_INPUT_PROVENANCE: {source}: {reason}; Chosen replay requires a recording produced with this reserved-input layout. The guest has not started. Record the run again with this Hermit, or replay the original with its producing Hermit; editing or overriding provenance cannot qualify an old recording."
191 ))
192}
193
194impl PreemptionRecord {
195 pub fn with_reserved_input(mut self, metadata: ReservedInputMetadata) -> Self {
198 self.reserved_input = metadata;
199 self
200 }
201
202 pub fn reserved_input(&self) -> &ReservedInputMetadata {
204 &self.reserved_input
205 }
206
207 pub fn validate_replay_inputs(
210 &self,
211 source: &str,
212 guest_ids: GuestIds,
213 ) -> Result<(), RecordAdmissionRefusal> {
214 if let Some(reason) =
215 super::guest_ids_mismatch(source, self.guest_ids.unwrap_or_default(), guest_ids)
216 {
217 return Err(RecordAdmissionRefusal(reason));
218 }
219 if matches!(guest_ids, GuestIds::Chosen { .. }) {
220 self.reserved_input
221 .require_matching_layout(source, &ReservedInputLayout::native()?)?;
222 }
223 Ok(())
224 }
225
226 pub fn validate_combined_reserved_inputs(
230 &self,
231 other: &Self,
232 ) -> Result<(), RecordAdmissionRefusal> {
233 if !matches!(self.guest_ids, Some(GuestIds::Chosen { .. }))
234 && !matches!(other.guest_ids, Some(GuestIds::Chosen { .. }))
235 {
236 return Ok(());
237 }
238 self.reserved_input
239 .require_same_for_chosen(&other.reserved_input)
240 }
241}
242
243pub fn read_recorded_reserved_input(
245 path: &std::path::Path,
246) -> Result<ReservedInputMetadata, String> {
247 #[derive(Deserialize)]
248 struct MetadataOnly {
249 #[serde(default)]
250 reserved_input: ReservedInputMetadata,
251 }
252 Ok(super::boundary_recording::read_settings::<MetadataOnly>(path)?.reserved_input)
253}
254
255pub fn validate_config_replay_inputs(config: &crate::Config) -> anyhow::Result<()> {
259 for (option, path) in [
260 ("--replay-schedule-from", &config.replay_schedule_from),
261 ("--replay-preemptions-from", &config.replay_preemptions_from),
262 ] {
263 if let Some(path) = path {
264 let recording = super::boundary_recording::Recording::read(path)?;
265 recording.validate_replay_inputs(
266 &format!("{option} {}", path.display()),
267 config.guest_ids,
268 )?;
269 }
270 }
271 Ok(())
272}
273
274#[derive(Debug, Default)]
278pub(super) enum ProducerQualification {
279 #[default]
280 Unobserved,
281 Qualified(ReservedInputLayout),
282 Disqualified,
283}
284
285impl ProducerQualification {
286 pub(super) fn observe(&mut self, layout: Option<ReservedInputLayout>) -> ReservedInputMetadata {
287 *self = match (&*self, layout) {
288 (Self::Unobserved, Some(layout)) => Self::Qualified(layout),
289 (Self::Qualified(previous), Some(layout)) if *previous == layout => {
290 Self::Qualified(layout)
291 }
292 _ => Self::Disqualified,
293 };
294 match self {
295 Self::Qualified(layout) => layout.metadata(),
296 _ => ReservedInputMetadata::default(),
297 }
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use crate::preemptions::PreemptionReader;
305 use crate::preemptions::PreemptionWriter;
306 use crate::preemptions::TimeSkip;
307 use crate::preemptions::TimeSkipReason;
308 use crate::preemptions::boundary_recording::BoundaryRecording;
309 use crate::preemptions::boundary_recording::HistoryReader;
310 use crate::preemptions::boundary_recording::Recording;
311 use crate::types::DetTid;
312 use crate::types::LogicalTime;
313 use crate::types::SchedEvent;
314
315 fn chosen() -> GuestIds {
316 GuestIds::Chosen {
317 base: 5000,
318 refuse_host_ids: true,
319 }
320 }
321
322 fn marked() -> PreemptionRecord {
323 let layout = ReservedInputLayout::native().unwrap();
324 PreemptionRecord::from_sched_events(vec![SchedEvent::branches(DetTid::from_raw(5000), 2)])
325 .with_guest_ids(chosen())
326 .with_current_format()
327 .with_time_skips(vec![TimeSkip {
328 event: 0,
329 target: LogicalTime::from_nanos(1000),
330 reason: TimeSkipReason::IdleGuestPollers,
331 }])
332 .with_reserved_input(layout.metadata())
333 }
334
335 #[test]
336 fn absence_null_and_unknown_provenance_remain_distinct_in_both_envelopes() {
337 for value in [
338 None,
339 Some(serde_json::Value::Null),
340 Some(serde_json::json!({
341 "future_contract": [1, null, "opaque"], "nested": {"extra": false}
342 })),
343 ] {
344 let original = PreemptionRecord::default()
345 .with_reserved_input(ReservedInputMetadata(value.clone()));
346 for recording in [
347 Recording::Legacy(original.clone()),
348 Recording::Boundaries(
349 BoundaryRecording::new(
350 original.clone(),
351 detcore_model::schedule::boundary::Schedule::new(vec![]).unwrap(),
352 )
353 .unwrap(),
354 ),
355 ] {
356 let decoded = Recording::decode(&recording.encode().unwrap()).unwrap();
357 assert_eq!(decoded.reserved_input().as_json(), value.as_ref());
358 decoded
359 .validate_replay_inputs("non-Chosen", GuestIds::Namespace)
360 .unwrap();
361 assert_eq!(decoded.encode().unwrap(), recording.encode().unwrap());
362 let directory = tempfile::tempdir().unwrap();
363 let path = directory.path().join("record.json");
364 recording.write(&path).unwrap();
365 assert_eq!(
366 read_recorded_reserved_input(&path).unwrap().as_json(),
367 value.as_ref()
368 );
369 assert_eq!(
370 HistoryReader::read(&path).unwrap().to_string().as_bytes(),
371 recording.encode().unwrap()
372 );
373 }
374 }
375 }
376
377 #[test]
378 fn chosen_requires_recognized_matching_provenance_without_overrides() {
379 let record = marked();
380 record
381 .validate_replay_inputs("qualified", chosen())
382 .unwrap();
383 for value in [
384 None,
385 Some(serde_json::Value::Null),
386 Some(serde_json::json!(42)),
387 Some(serde_json::json!({"contract": "future"})),
388 ] {
389 let legacy = record
390 .clone()
391 .with_reserved_input(ReservedInputMetadata(value));
392 let refusal = legacy
393 .validate_replay_inputs("old fixture", chosen())
394 .unwrap_err();
395 assert!(
396 refusal
397 .0
398 .starts_with("HERMIT_CHOSEN_JSON_INPUT_PROVENANCE:")
399 );
400 assert!(refusal.0.contains("guest has not started"));
401 assert!(refusal.0.contains("Record the run again"));
402 assert!(
404 legacy
405 .validate_replay_inputs(
406 "old fixture",
407 GuestIds::Chosen {
408 base: 5000,
409 refuse_host_ids: false,
410 }
411 )
412 .is_err()
413 );
414 }
415 for (field, value) in [
416 ("contract", serde_json::json!("sealed-private-input-v2")),
417 ("base", serde_json::json!("0x71020000")),
418 ("page_bytes", serde_json::json!(8192)),
419 ("abi", serde_json::json!("unsupported")),
420 ("extra", serde_json::json!(true)),
421 ] {
422 let mut raw = serde_json::to_value(&record).unwrap();
423 raw["reserved_input"][field] = value;
424 let changed: PreemptionRecord = serde_json::from_value(raw).unwrap();
425 let refusal = changed
426 .validate_replay_inputs("changed", chosen())
427 .unwrap_err();
428 assert!(
429 refusal
430 .0
431 .starts_with("HERMIT_CHOSEN_JSON_INPUT_PROVENANCE:")
432 );
433 }
434 let mut other = ReservedInputLayout::native().unwrap();
435 other.page_bytes = if other.page_bytes == 4096 {
436 16384
437 } else {
438 4096
439 };
440 let changed = record.with_reserved_input(other.metadata());
441 assert!(
442 changed
443 .validate_replay_inputs("different native page", chosen())
444 .unwrap_err()
445 .0
446 .contains("layout differs")
447 );
448 }
449
450 #[test]
451 fn numbering_gate_cannot_be_bypassed_by_an_unlabelled_or_relabelled_record() {
452 let record = marked();
453 assert!(
454 record
455 .validate_replay_inputs("wrong runtime", GuestIds::Namespace)
456 .is_err()
457 );
458 assert!(
459 record
460 .validate_replay_inputs(
461 "wrong base",
462 GuestIds::Chosen {
463 base: 6000,
464 refuse_host_ids: true,
465 }
466 )
467 .is_err()
468 );
469 let unlabelled = record.clone().with_guest_ids(GuestIds::Namespace);
470 assert!(
471 unlabelled
472 .validate_replay_inputs("unlabelled", chosen())
473 .is_err()
474 );
475 assert!(
476 record
477 .with_guest_ids(GuestIds::Host)
478 .validate_replay_inputs("host IDs", chosen())
479 .is_err()
480 );
481 }
482
483 #[test]
484 fn config_admission_checks_both_actual_inputs_and_preserves_error_classes() {
485 let directory = tempfile::tempdir().unwrap();
486 let good = directory.path().join("qualified.json");
487 let bad = directory.path().join("legacy.json");
488 marked().write_to_disk(&good).unwrap();
489 marked()
490 .with_reserved_input(ReservedInputMetadata::default())
491 .write_to_disk(&bad)
492 .unwrap();
493 for (schedule, priorities) in [
494 (Some(bad.clone()), None),
495 (None, Some(bad.clone())),
496 (Some(good.clone()), Some(bad.clone())),
497 (Some(bad.clone()), Some(good.clone())),
498 ] {
499 let config = crate::Config {
500 guest_ids: chosen(),
501 replay_schedule_from: schedule,
502 replay_preemptions_from: priorities,
503 ..crate::Config::default()
504 };
505 let error = validate_config_replay_inputs(&config).unwrap_err();
506 assert!(
507 error
508 .chain()
509 .any(|cause| cause.is::<RecordAdmissionRefusal>())
510 );
511 }
512 let config = crate::Config {
513 guest_ids: chosen(),
514 replay_schedule_from: Some(good.clone()),
515 replay_preemptions_from: Some(good.clone()),
516 ..crate::Config::default()
517 };
518 validate_config_replay_inputs(&config).unwrap();
519 std::fs::write(&good, b"{broken JSON").unwrap();
520 let error = validate_config_replay_inputs(&config).unwrap_err();
521 assert!(
522 !error
523 .chain()
524 .any(|cause| cause.is::<RecordAdmissionRefusal>())
525 );
526 }
527
528 #[test]
529 fn authoritative_history_admits_decoded_bytes_not_a_preflight_path() {
530 let directory = tempfile::tempdir().unwrap();
531 let path = directory.path().join("history.json");
532 marked().write_to_disk(&path).unwrap();
533 let config = crate::Config {
534 guest_ids: chosen(),
535 replay_preemptions_from: Some(path.clone()),
536 ..crate::Config::default()
537 };
538 validate_config_replay_inputs(&config).unwrap();
539 marked()
540 .with_reserved_input(ReservedInputMetadata::default())
541 .write_to_disk(&path)
542 .unwrap();
543 let error = HistoryReader::read_for_replay(&path, chosen()).unwrap_err();
544 assert!(
545 error
546 .chain()
547 .any(|cause| cause.is::<RecordAdmissionRefusal>())
548 );
549 }
550
551 #[test]
552 fn identity_interpolation_validates_all_sources_before_either_endpoint() {
553 let original = marked();
554 let legacy = original
555 .clone()
556 .with_reserved_input(ReservedInputMetadata::default());
557 for (first, second) in [(&original, &legacy), (&legacy, &original)] {
558 let error = first
559 .interpolate_events(second, original.global.clone())
560 .unwrap_err();
561 assert!(error.starts_with("HERMIT_CHOSEN_JSON_INPUT_PROVENANCE:"));
562 }
563 let mut mixed = original.clone();
564 let mut descriptor = ReservedInputLayout::native().unwrap();
565 descriptor.abi = if descriptor.abi == "x86_64" {
566 "aarch64"
567 } else {
568 "x86_64"
569 }
570 .to_owned();
571 mixed.reserved_input = descriptor.metadata();
572 assert!(
573 original
574 .interpolate_events(&mixed, original.global.clone())
575 .is_err()
576 );
577 let identity = original
578 .interpolate_events(&original, original.global.clone())
579 .unwrap();
580 assert_eq!(identity.to_string(), original.to_string());
581 assert_eq!(identity.format_version(), Some(3));
582 assert_eq!(identity.time_skips(), original.time_skips());
583 }
584
585 #[test]
586 fn non_chosen_interpolation_preserves_opaque_metadata_without_new_admission() {
587 let first = PreemptionRecord::from_sched_events(vec![
588 SchedEvent::branches(DetTid::from_raw(3), 2),
589 SchedEvent::branches(DetTid::from_raw(5), 3),
590 ])
591 .with_current_format()
592 .with_time_skips(vec![])
593 .with_reserved_input(ReservedInputMetadata(Some(
594 serde_json::json!({"future": [null, 17]}),
595 )));
596 let second = PreemptionRecord::from_sched_events(vec![
597 SchedEvent::branches(DetTid::from_raw(7), 2),
598 SchedEvent::branches(DetTid::from_raw(9), 3),
599 ])
600 .with_current_format()
601 .with_time_skips(vec![])
602 .with_reserved_input(ReservedInputMetadata(Some(serde_json::Value::Null)));
603 for source in [&first, &second] {
607 assert_eq!(source.time_skips(), None);
608 assert_eq!(source.format_version(), Some(3));
609 source.validate().unwrap();
610 assert!(
611 serde_json::to_value(source)
612 .unwrap()
613 .get("time_skips")
614 .is_none()
615 );
616 }
617 let mixed = first
618 .interpolate_events(
619 &second,
620 vec![first.global[0].clone(), second.global[1].clone()],
621 )
622 .unwrap();
623 assert_eq!(mixed.reserved_input(), first.reserved_input());
624 assert_eq!(mixed.format_version(), Some(3));
625 assert_eq!(mixed.time_skips(), None);
626 assert!(
627 serde_json::to_value(&mixed)
628 .unwrap()
629 .get("time_skips")
630 .is_none()
631 );
632 assert_eq!(
633 first
634 .interpolate_events(&second, second.global.clone())
635 .unwrap(),
636 second
637 );
638 let mut invalid = first.clone();
641 invalid.time_skips = Some(vec![]);
642 for (left, right) in [(&invalid, &second), (&second, &invalid)] {
643 for events in [left.global.clone(), right.global.clone()] {
644 assert_eq!(
645 left.interpolate_events(right, events).unwrap_err(),
646 "an empty list of time skips is written as no list"
647 );
648 }
649 }
650 }
651
652 #[test]
653 fn direct_readers_prefixes_and_priority_edits_preserve_provenance() {
654 let original = marked();
655 let directory = tempfile::tempdir().unwrap();
656 let path = directory.path().join("record.json");
657 original.write_to_disk(&path).unwrap();
658 for record in [
659 PreemptionReader::new(&path).load_all(),
660 PreemptionReader::try_new(&path).unwrap().into_inner(),
661 original.schedule_prefix(original.global.len()),
662 original.clone_preemptions_only(),
663 ] {
664 assert_eq!(record.reserved_input(), original.reserved_input());
665 }
666 let prefix = original.schedule_prefix(original.global.len());
667 assert_eq!(prefix.format_version(), Some(3));
668 assert_eq!(prefix.time_skips(), original.time_skips());
669 assert_eq!(original.clone_preemptions_only().format_version(), None);
670 }
671
672 #[test]
673 fn writer_never_qualifies_from_configuration_default_or_drop() {
674 let directory = tempfile::tempdir().unwrap();
675 let path = directory.path().join("partial.json");
676 {
677 let _writer = PreemptionWriter::new(Some(path.clone())).with_guest_ids(chosen());
678 }
679 let dropped = PreemptionReader::new(&path).load_all();
680 assert!(dropped.reserved_input().is_missing());
681 for record in [
682 PreemptionRecord::default().with_guest_ids(chosen()),
683 PreemptionRecord::from_sched_events(vec![]).with_guest_ids(chosen()),
684 PreemptionRecord::from_vecs(&Default::default()).with_guest_ids(chosen()),
685 ] {
686 assert!(record.reserved_input().is_missing());
687 }
688 }
689
690 #[test]
694 fn writer_string_flush_and_drop_preserve_the_complete_imported_metadata() {
695 let directory = tempfile::tempdir().unwrap();
696 for boundaries in [false, true] {
697 for metadata in [
698 ReservedInputMetadata::default(),
699 ReservedInputMetadata(Some(serde_json::Value::Null)),
700 ReservedInputMetadata(Some(serde_json::json!({"future": [17, null]}))),
701 ReservedInputLayout::native().unwrap().metadata(),
702 ] {
703 let mut record = marked().with_reserved_input(metadata);
704 if boundaries {
705 record.global.clear();
706 }
707 let make = |path| {
708 let mut writer = PreemptionWriter::new(path);
709 writer.inner = record.clone();
710 if boundaries {
711 writer = writer.with_slice_boundaries().unwrap();
712 }
713 writer
714 };
715 let string = make(None).into_string();
716 let expected = Recording::decode(string.as_bytes()).unwrap();
717 assert_eq!(expected.reserved_input(), record.reserved_input());
718 assert_eq!(expected.format_version(), Some(3));
719 assert_eq!(expected.time_skips(), record.time_skips());
720 for explicit_flush in [false, true] {
721 let path = directory.path().join("writer.json");
722 let writer = make(Some(path.clone()));
723 if explicit_flush {
724 writer.flush().unwrap();
725 } else {
726 drop(writer);
727 }
728 let written = Recording::read(&path).unwrap();
729 assert_eq!(written.encode().unwrap(), expected.encode().unwrap());
730 }
731 }
732 }
733 }
734
735 #[test]
736 fn an_unqualified_target_cannot_be_retroactively_qualified_by_exec_or_flush() {
737 let layout = ReservedInputLayout::native().unwrap();
738 for first in [Some(layout.clone()), None] {
739 let mut qualifier = ProducerQualification::default();
740 qualifier.observe(first);
741 assert!(qualifier.observe(None).is_missing());
742 assert!(qualifier.observe(Some(layout.clone())).is_missing());
743 }
744 let mut qualifier = ProducerQualification::default();
745 assert_eq!(qualifier.observe(Some(layout.clone())), layout.metadata());
746 assert_eq!(qualifier.observe(Some(layout.clone())), layout.metadata());
747 let mut mismatch = layout;
748 mismatch.page_bytes *= 2;
749 assert!(qualifier.observe(Some(mismatch)).is_missing());
750 }
751
752 #[test]
756 fn writer_setup_observer_carries_verified_layout_and_keeps_failure_sticky() {
757 use reverie::reserved_input::InputAbi;
758 use reverie::reserved_input::InputSetupOutcome;
759 use reverie::reserved_input::InputSetupReceipt;
760 use reverie::reserved_input::InputUnavailable;
761 use reverie::reserved_input::ReservedInputControl;
762
763 let native = ReservedInputLayout::native().unwrap();
764 let layout = reverie::reserved_input::ReservedInputLayout {
765 base: 0x7101_0000,
766 page_bytes: native.page_bytes as usize,
767 abi: if native.abi == "x86_64" {
768 InputAbi::X86_64
769 } else {
770 InputAbi::Aarch64
771 },
772 };
773 let control = ReservedInputControl::new_backend().unwrap();
774 let task = control.backend_register_root(17, 17).unwrap().task;
775 let good = InputSetupReceipt {
776 task,
777 outcome: InputSetupOutcome::Qualified(layout),
778 };
779 let skip = TimeSkip {
780 event: 0,
781 target: LogicalTime::from_nanos(1000),
782 reason: TimeSkipReason::IdleGuestPollers,
783 };
784 for failure in [
785 InputSetupOutcome::Unqualified(InputUnavailable::new("test-unqualified-image")),
786 InputSetupOutcome::Qualified(reverie::reserved_input::ReservedInputLayout {
787 base: layout.base + 4096,
788 ..layout
789 }),
790 ] {
791 let mut writer = PreemptionWriter::new(None)
792 .with_guest_ids(chosen())
793 .with_format(Some(3));
794 writer.insert_schedevent(SchedEvent::branches(DetTid::from_raw(5000), 2));
795 writer.insert_time_skip(skip);
796 assert!(writer.inner.reserved_input().is_missing());
797 writer.observe_reserved_input_setup(&good).unwrap();
798 let recorded =
799 Recording::decode(writer.serialized_string().unwrap().as_bytes()).unwrap();
800 assert_eq!(recorded.reserved_input(), &native.metadata());
801 assert_eq!(recorded.format_version(), Some(3));
802 assert_eq!(recorded.time_skips(), Some([skip].as_slice()));
803 recorded
804 .validate_replay_inputs("writer observer", chosen())
805 .unwrap();
806
807 let unsupported_layout = matches!(failure, InputSetupOutcome::Qualified(_));
808 let outcome = writer.observe_reserved_input_setup(&InputSetupReceipt {
809 task,
810 outcome: failure,
811 });
812 assert_eq!(outcome.is_err(), unsupported_layout);
813 assert!(writer.inner.reserved_input().is_missing());
814 writer.observe_reserved_input_setup(&good).unwrap();
815 let partial = Recording::decode(writer.into_string().as_bytes()).unwrap();
816 assert!(partial.reserved_input().is_missing());
817 assert_eq!(partial.format_version(), Some(3));
818 assert_eq!(partial.time_skips(), Some([skip].as_slice()));
819 assert!(
820 partial
821 .validate_replay_inputs("unqualified earlier image", chosen())
822 .unwrap_err()
823 .to_string()
824 .contains("missing reserved_input provenance")
825 );
826 }
827 }
828}