Skip to main content

sim_incremental_core/projection/
admission.rs

1use std::{error::Error, fmt};
2
3use sim_kernel::{ContentId, Datum, NumberLiteral, Symbol};
4
5use super::{
6    DeterministicImportManifest, ProjectorPolicy, ProjectorQualification, QualifiedRuntime,
7    QualifiedSourceClosure,
8};
9
10/// Evidence needed to trust an exact native projector implementation.
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct NativeSourceEvidence {
13    /// Exact implementation identity.
14    pub code: ContentId,
15    /// Exact transitive runtime dependency identity.
16    pub dependencies: ContentId,
17    /// Independent review evidence identity.
18    pub review: ContentId,
19    /// Source and dependencies were both reviewed.
20    pub source_and_dependencies_reviewed: bool,
21    /// Deny-ambient-I/O review found no unexplained path.
22    pub ambient_io_closed: bool,
23    /// FFI, unsafe, globals, mutable caches, and nondeterminism were reviewed.
24    pub hidden_state_reviewed: bool,
25    /// The inspected code identity equals the code that will be loaded.
26    pub loaded_code_matches: bool,
27}
28
29/// Evidence needed to admit a closed wasm projector.
30#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct ClosedWasmEvidence {
32    /// Exact semantic module identity.
33    pub module: ContentId,
34    /// Complete imports discovered from the module.
35    pub imports: DeterministicImportManifest,
36    /// Exact qualified runtime.
37    pub runtime: QualifiedRuntime,
38    /// Admission evidence identity.
39    pub admission: ContentId,
40    /// Import discovery covered the complete transitive module.
41    pub import_manifest_complete: bool,
42    /// Start behavior was checked before instantiation.
43    pub start_behavior_checked: bool,
44    /// Fuel and memory limits are enforced by the runtime.
45    pub budgets_enforced: bool,
46}
47
48/// Distinct projector-admission refusal.
49#[derive(Clone, Debug, Eq, PartialEq)]
50pub enum QualificationError {
51    /// Native code did not pass exact source and dependency review.
52    NativeSourceReviewMissing,
53    /// Native code retained an unexplained ambient or hidden input.
54    NativeAmbientInput,
55    /// Reviewed and loaded native code identities differ.
56    NativeCodeMismatch,
57    /// Wasm import discovery was incomplete.
58    IncompleteImportManifest,
59    /// Actual and policy wasm imports differ.
60    ImportManifestMismatch,
61    /// An ambient or nondeterministic wasm import was requested.
62    ForbiddenImport(String),
63    /// Wasm start behavior was not qualified.
64    StartBehaviorUnchecked,
65    /// Runtime numeric, ordering, or fresh-instance semantics are incomplete.
66    RuntimeSemanticsUnqualified,
67    /// Runtime fuel or memory bounds are absent.
68    RuntimeBudgetsUnenforced,
69    /// Canonical policy identity failed.
70    CanonicalPolicy(String),
71}
72
73impl fmt::Display for QualificationError {
74    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
75        write!(formatter, "{self:?}")
76    }
77}
78
79impl Error for QualificationError {}
80
81/// Verifies the two and only two projector admission routes.
82#[derive(Clone, Copy, Debug, Default)]
83pub struct ProjectorQualificationVerifier;
84
85impl ProjectorQualificationVerifier {
86    /// Qualifies exact trusted native code.
87    pub fn trusted_native(
88        policy: &ProjectorPolicy,
89        evidence: NativeSourceEvidence,
90    ) -> Result<ProjectorQualification, QualificationError> {
91        if !evidence.source_and_dependencies_reviewed {
92            return Err(QualificationError::NativeSourceReviewMissing);
93        }
94        if !evidence.ambient_io_closed || !evidence.hidden_state_reviewed {
95            return Err(QualificationError::NativeAmbientInput);
96        }
97        if !evidence.loaded_code_matches {
98            return Err(QualificationError::NativeCodeMismatch);
99        }
100        Ok(ProjectorQualification::TrustedNative {
101            source: QualifiedSourceClosure {
102                code: evidence.code,
103                dependencies: evidence.dependencies,
104                review: evidence.review,
105            },
106            policy: policy_id(policy)?,
107        })
108    }
109
110    /// Qualifies a closed wasm module and deterministic runtime.
111    pub fn closed_wasm(
112        policy: &ProjectorPolicy,
113        evidence: ClosedWasmEvidence,
114    ) -> Result<ProjectorQualification, QualificationError> {
115        if !evidence.import_manifest_complete {
116            return Err(QualificationError::IncompleteImportManifest);
117        }
118        if evidence.imports != policy.imports {
119            return Err(QualificationError::ImportManifestMismatch);
120        }
121        for import in &evidence.imports.imports {
122            if is_forbidden_import(import) {
123                return Err(QualificationError::ForbiddenImport(import.clone()));
124            }
125        }
126        if !evidence.start_behavior_checked {
127            return Err(QualificationError::StartBehaviorUnchecked);
128        }
129        let semantics = &evidence.runtime.semantics;
130        if !semantics.canonical_nan || !semantics.canonical_collections || !semantics.fresh_instance
131        {
132            return Err(QualificationError::RuntimeSemanticsUnqualified);
133        }
134        if !evidence.budgets_enforced {
135            return Err(QualificationError::RuntimeBudgetsUnenforced);
136        }
137        Ok(ProjectorQualification::ClosedWasm {
138            module: evidence.module,
139            policy: policy_id(policy)?,
140            runtime: evidence.runtime,
141            imports: evidence.imports,
142            admission: evidence.admission,
143        })
144    }
145}
146
147pub(crate) fn policy_id(policy: &ProjectorPolicy) -> Result<ContentId, QualificationError> {
148    let input_facts = policy
149        .reads
150        .facts()
151        .map(|fact| Datum::String(fact.as_str().to_owned()))
152        .collect();
153    let imports = policy
154        .imports
155        .imports
156        .iter()
157        .cloned()
158        .map(Datum::String)
159        .collect();
160    let fields = vec![
161        (
162            Symbol::new("input-shape"),
163            content_id_datum(&policy.input_shape),
164        ),
165        (Symbol::new("reads"), Datum::Vector(input_facts)),
166        (Symbol::new("imports"), Datum::Vector(imports)),
167        (
168            Symbol::new("execution"),
169            Datum::Node {
170                tag: Symbol::qualified("projection", "execution-semantics-v1"),
171                fields: vec![
172                    (
173                        Symbol::new("id"),
174                        Datum::String(policy.execution.id.clone()),
175                    ),
176                    (
177                        Symbol::new("canonical-nan"),
178                        Datum::Bool(policy.execution.canonical_nan),
179                    ),
180                    (
181                        Symbol::new("canonical-collections"),
182                        Datum::Bool(policy.execution.canonical_collections),
183                    ),
184                    (
185                        Symbol::new("fresh-instance"),
186                        Datum::Bool(policy.execution.fresh_instance),
187                    ),
188                ],
189            },
190        ),
191        (
192            Symbol::new("max-inputs"),
193            number_datum(policy.budgets.max_inputs as u64),
194        ),
195        (
196            Symbol::new("max-output-bytes"),
197            number_datum(policy.budgets.max_output_bytes as u64),
198        ),
199        (
200            Symbol::new("max-fuel"),
201            number_datum(policy.budgets.max_fuel),
202        ),
203        (
204            Symbol::new("max-memory-bytes"),
205            number_datum(policy.budgets.max_memory_bytes as u64),
206        ),
207        (
208            Symbol::new("requires-confinement"),
209            Datum::Bool(policy.requires_confinement),
210        ),
211    ];
212    Datum::Node {
213        tag: Symbol::qualified("projection", "projector-policy-v1"),
214        fields,
215    }
216    .content_id()
217    .map_err(|error| QualificationError::CanonicalPolicy(error.to_string()))
218}
219
220pub(crate) fn content_id_datum(id: &ContentId) -> Datum {
221    Datum::Node {
222        tag: Symbol::qualified("core", "content-id-v1"),
223        fields: vec![
224            (
225                Symbol::new("algorithm"),
226                Datum::Symbol(id.algorithm.clone()),
227            ),
228            (Symbol::new("bytes"), Datum::Bytes(id.bytes.to_vec())),
229        ],
230    }
231}
232
233fn number_datum(value: u64) -> Datum {
234    Datum::Number(NumberLiteral {
235        domain: Symbol::qualified("projection", "u64"),
236        canonical: value.to_string(),
237    })
238}
239
240fn is_forbidden_import(import: &str) -> bool {
241    const FORBIDDEN: &[&str] = &[
242        "wasi",
243        "filesystem",
244        "path_",
245        "proc",
246        "environment",
247        "environ",
248        "clock",
249        "time",
250        "random",
251        "network",
252        "socket",
253        "thread",
254        "shared-memory",
255    ];
256    let lower = import.to_ascii_lowercase();
257    FORBIDDEN.iter().any(|needle| lower.contains(needle))
258}