Skip to main content

hermit/
verify_run_record.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Producer-owned wire record and raw streams for one retained execution.
10//!
11//! The CLI writes the final record after the output files. Both the CLI
12//! comparator and the manifest runner read this schema; policy stays with
13//! those consumers.
14
15use std::fs;
16use std::path::Path;
17
18use serde::Deserialize;
19use serde::Serialize;
20
21use crate::canonical_verdict::BranchCounterVerdict;
22use crate::canonical_verdict::ContainerDisposition;
23use crate::canonical_verdict::LogCompareStrictness;
24use crate::canonical_verdict::RunFailure;
25
26/// The record of how the run ended, written last: a directory without it is
27/// an execution that did not finish writing its evidence.
28pub const RECORD: &str = "verify-run.json";
29/// The run's verification log, the log `run --verify` compares.
30pub const LOG: &str = "run.log";
31/// The guest's standard output.
32pub const STDOUT: &str = "stdout";
33/// The guest's standard error.
34pub const STDERR: &str = "stderr";
35/// The run's summary (`--summary-json`), when Detcore wrote one.
36pub const SUMMARY: &str = "summary.json";
37/// The host files the run's guest opened (`hermit::host_input_change`).
38pub const HOST_INPUTS: &str = "host-inputs";
39/// The recording a `record start --verify-run` execution made, which a
40/// `--verify-replay-of` execution replays.
41pub const RECORDING: &str = "recording";
42/// The epoch a recording and its replay record: the replay runs at the
43/// recording's own epoch, so the pair agrees on it by construction.
44pub const RECORDING_EPOCH: &str = "recording";
45
46/// The DBT backend's typed whole-process statistics stream, as DynamoRIO's
47/// client wrote it (Reverie decodes it; `backends::require_dbt_stats` applies verification policy).
48pub const DBT_STATS: &str = "dbt-stats.bin";
49
50/// The version of [`VerifyRunRecord`].
51pub const SCHEMA: u32 = 1;
52
53/// How one `--verify-run` execution ended, and the comparison it was run for.
54#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
55#[serde(deny_unknown_fields)]
56pub struct VerifyRunRecord {
57    pub schema: u32,
58    /// The backend, as `--backend` spells it.
59    pub backend: String,
60    pub comparison: RecordedComparison,
61    /// Whether the run used `--passthru-opt`, which decides whether a host
62    /// file change can be named as a divergence's cause.
63    pub passthru_opt: bool,
64    /// The virtual-time epoch the run started its clock at (`--epoch`). `run
65    /// --verify` gives its two runs one epoch; two separate runs agree only
66    /// when the caller gives them the same one.
67    pub epoch: String,
68    /// `hermit host-capabilities`'s `exact_branch_counter` verdict, for a run
69    /// on a backend that reads that counter.
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub exact_branch_counter: Option<BranchCounterVerdict>,
72    /// What the DBT backend's comparison reads besides the log, for a DBT run.
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub dbt: Option<DbtRunRecord>,
75    /// Which half of which verification this is.
76    #[serde(default, skip_serializing_if = "RunRole::is_run")]
77    pub role: RunRole,
78    pub outcome: VerifyRunOutcome,
79}
80
81/// Which half of which verification a `--verify-run` execution is.
82#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
83#[serde(rename_all = "snake_case")]
84pub enum RunRole {
85    /// One of `run --verify`'s two runs.
86    #[default]
87    Run,
88    /// `record start --verify`'s recording.
89    Record,
90    /// `record start --verify`'s replay of a recording.
91    Replay,
92}
93
94impl RunRole {
95    fn is_run(&self) -> bool {
96        *self == Self::Run
97    }
98}
99
100/// What a DBT run's comparison reads besides its log and [`DBT_STATS`].
101#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
102#[serde(deny_unknown_fields)]
103pub struct DbtRunRecord {
104    /// How many process-image initialization records the run's evidence
105    /// held (`DbtEvidence::initialization_records`).
106    pub initialization_records: u64,
107}
108
109/// The comparison the run's flags select, as `run --verify` derives it
110/// (`RunOpts::verification_comparison_options`). Both runs of a comparison
111/// must record the same one.
112#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
113#[serde(deny_unknown_fields)]
114pub struct RecordedComparison {
115    pub strictness: LogCompareStrictness,
116    /// `--verify-verbose`: the all-level diagnostic comparison.
117    pub diagnostic_full_trace: bool,
118    /// Whether the records carry syscall output-buffer content.
119    pub compare_io_buffers: bool,
120    pub virtualize_time: bool,
121}
122
123/// How one execution ended.
124#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
125#[serde(rename_all = "snake_case", tag = "kind", deny_unknown_fields)]
126pub enum VerifyRunOutcome {
127    /// The guest ran to its end; its output is in [`STDOUT`] and [`STDERR`].
128    Completed {
129        status: RecordedStatus,
130        skid_overshoots: u64,
131        /// How many syscall DETLOG records an in-guest Tool forwarded into
132        /// the log (LiteInst, SaBRe); `None` on a backend that forwards none.
133        #[serde(default, skip_serializing_if = "Option::is_none")]
134        forwarded_syscalls: Option<u64>,
135        /// The run ended in a deadlock under Hermit's model
136        /// (`hermit::SchedulerEndedRun`), not a guest result and a native hang
137        /// only if Hermit models every wake the program relies on; `status` is
138        /// then `HERMIT_GUEST_DEADLOCK_EXIT`, which the guest never exited with.
139        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
140        guest_deadlock: bool,
141        /// The run's virtual clock passed `--virtual-timeout`, this bound in
142        /// virtual nanoseconds (`hermit::SchedulerEndedRun`); `status` is then 124,
143        /// which the guest never exited with.
144        #[serde(default, skip_serializing_if = "Option::is_none")]
145        virtual_timeout_ns: Option<u64>,
146    },
147    /// A typed failure of the run stopped it ([`RunFailure`]).
148    RunFailed {
149        failure: RunFailure,
150        /// The failure's message, without the run's position in a pair, which
151        /// the comparison adds where `run --verify` names it.
152        detail: String,
153    },
154    /// The run's container child exited before returning the guest's output.
155    ContainerFailed { disposition: ContainerDisposition },
156    /// The run overshot its PMU skid this many times and returned no output.
157    SkidOvershoot { count: u64 },
158    /// An error that is none of the above stopped the run; `run --verify`
159    /// leaves its pending no-result for such an error.
160    Error { detail: String },
161}
162
163/// A guest's wait status.
164#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
165#[serde(rename_all = "snake_case", tag = "kind", deny_unknown_fields)]
166pub enum RecordedStatus {
167    Exited { code: i32 },
168    Signaled { signal: i32, core_dumped: bool },
169}
170
171impl VerifyRunRecord {
172    /// Read the final record, refusing incomplete or unsupported evidence.
173    pub fn read(directory: &Path) -> Result<Self, String> {
174        let path = directory.join(RECORD);
175        let bytes = fs::read(&path).map_err(|error| {
176            format!(
177                "reading {}: a --verify-run directory without its record did not finish: {error}",
178                path.display()
179            )
180        })?;
181        let record: Self = serde_json::from_slice(&bytes)
182            .map_err(|error| format!("parsing {}: {error}", path.display()))?;
183        if record.schema != SCHEMA {
184            return Err(format!(
185                "{} has schema {}, and this Hermit reads schema {SCHEMA}",
186                path.display(),
187                record.schema
188            ));
189        }
190        Ok(record)
191    }
192}
193
194/// The guest's exact output bytes, separate from the producer's diagnostics.
195pub struct RunOutput {
196    pub stdout: Vec<u8>,
197    pub stderr: Vec<u8>,
198}
199
200/// Read both streams without treating invalid UTF-8 or missing files as empty.
201pub fn read_output(directory: &Path) -> Result<RunOutput, String> {
202    let read = |name: &str| {
203        let path = directory.join(name);
204        fs::read(&path).map_err(|error| format!("reading {}: {error}", path.display()))
205    };
206    Ok(RunOutput {
207        stdout: read(STDOUT)?,
208        stderr: read(STDERR)?,
209    })
210}