Skip to main content

runmat_runtime/native/
validation.rs

1use super::{
2    NativeCall, NativeCancellationReason, NativeDeoptReason, NativeExit, NativeExitKind,
3    NativeFrame, NativeResumeKind, NativeRootSet, NativeSafepoint, NativeSourceMapView,
4    NATIVE_ABI_VERSION,
5};
6
7#[derive(Clone, Debug, Eq, PartialEq)]
8pub struct NativeAbiError {
9    pub field: &'static str,
10    pub message: &'static str,
11}
12
13impl NativeAbiError {
14    pub(super) const fn new(field: &'static str, message: &'static str) -> Self {
15        Self { field, message }
16    }
17}
18
19impl std::fmt::Display for NativeAbiError {
20    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        write!(formatter, "invalid {}: {}", self.field, self.message)
22    }
23}
24
25impl std::error::Error for NativeAbiError {}
26
27impl NativeRootSet {
28    pub fn validate(self) -> Result<(), NativeAbiError> {
29        validate_slice("native.roots", self.roots, self.count)
30    }
31}
32
33impl NativeSourceMapView {
34    pub fn validate(self) -> Result<(), NativeAbiError> {
35        validate_slice("native.source_map", self.entries, self.count)
36    }
37}
38
39impl NativeFrame {
40    pub fn validate(&self) -> Result<(), NativeAbiError> {
41        validate_version(self.abi_version)?;
42        if !self.kind.is_known() {
43            return Err(NativeAbiError::new(
44                "native.frame.kind",
45                "frame kind is unknown",
46            ));
47        }
48        validate_zero("native.frame.flags", self.flags)?;
49        validate_zero("native.frame.reserved", self.reserved)?;
50        validate_slice("native.frame.locals", self.locals, self.local_count)?;
51        validate_slice(
52            "native.frame.operands",
53            self.operands,
54            self.operand_capacity,
55        )?;
56        self.roots.validate()?;
57        if self.resume.is_null() {
58            return Err(NativeAbiError::new(
59                "native.frame.resume",
60                "exact resume state is required",
61            ));
62        }
63        Ok(())
64    }
65}
66
67impl NativeCall {
68    pub fn validate(&self) -> Result<(), NativeAbiError> {
69        validate_version(self.abi_version)?;
70        if !self.kind.is_known() {
71            return Err(NativeAbiError::new(
72                "native.call.kind",
73                "call kind is unknown",
74            ));
75        }
76        validate_zero("native.call.flags", self.flags)?;
77        if self.host.is_null() {
78            return Err(NativeAbiError::new(
79                "native.call.host",
80                "host service table must not be null",
81            ));
82        }
83        if self.frame.is_null() {
84            return Err(NativeAbiError::new(
85                "native.call.frame",
86                "frame must not be null",
87            ));
88        }
89        validate_slice("native.call.arguments", self.arguments, self.argument_count)?;
90        validate_slice("native.call.results", self.results, self.result_capacity)?;
91        if self.requested_outputs as usize > self.result_capacity {
92            return Err(NativeAbiError::new(
93                "native.call.requested_outputs",
94                "requested outputs exceed result capacity",
95            ));
96        }
97        Ok(())
98    }
99}
100
101impl NativeExit {
102    pub fn validate(&self) -> Result<(), NativeAbiError> {
103        validate_version(self.abi_version)?;
104        validate_zero("native.exit.flags", self.flags)?;
105        match self.kind {
106            NativeExitKind::COMPLETED => Ok(()),
107            NativeExitKind::EXCEPTION
108                if self.exception.handle != 0 && self.exception.generation != 0 =>
109            {
110                self.exception.source.validate()
111            }
112            NativeExitKind::CANCELLED
113                if self.cancellation.reason.is_known()
114                    && self.cancellation.reserved == 0
115                    && self.cancellation.generation != 0 =>
116            {
117                Ok(())
118            }
119            NativeExitKind::SUSPENDED => {
120                if self.suspension.continuation == 0
121                    || self.suspension.generation == 0
122                    || self.suspension.resume.is_null()
123                {
124                    return Err(NativeAbiError::new(
125                        "native.exit.suspension",
126                        "continuation and exact resume state are required",
127                    ));
128                }
129                self.suspension.roots.validate()
130            }
131            NativeExitKind::DEOPTIMIZED
132                if self.deoptimization.reason.is_known()
133                    && self.deoptimization.target.is_known()
134                    && !self.deoptimization.resume.is_null() =>
135            {
136                Ok(())
137            }
138            NativeExitKind::EXCEPTION => Err(NativeAbiError::new(
139                "native.exit.exception",
140                "exception handle, generation, and source must be valid",
141            )),
142            NativeExitKind::CANCELLED => Err(NativeAbiError::new(
143                "native.exit.cancellation",
144                "cancellation reason, generation, and reserved fields must be valid",
145            )),
146            NativeExitKind::DEOPTIMIZED => Err(NativeAbiError::new(
147                "native.exit.deoptimization",
148                "reason, target, and resume state must be valid",
149            )),
150            _ => Err(NativeAbiError::new(
151                "native.exit.kind",
152                "exit kind is unknown",
153            )),
154        }
155    }
156}
157
158impl NativeSafepoint {
159    pub fn validate(&self) -> Result<(), NativeAbiError> {
160        if self.resume.is_null() {
161            return Err(NativeAbiError::new(
162                "native.safepoint.resume",
163                "exact resume state is required",
164            ));
165        }
166        self.roots.validate()
167    }
168}
169
170impl super::NativeResumeState {
171    pub fn validate(&self) -> Result<(), NativeAbiError> {
172        validate_zero("native.resume.flags", self.flags)?;
173        validate_zero("native.resume.reserved", self.reserved)?;
174        if !super::NativeSitePhase(self.phase).is_known() {
175            return Err(NativeAbiError::new(
176                "native.resume.phase",
177                "resume site phase is unknown",
178            ));
179        }
180        self.source.validate()
181    }
182}
183
184impl NativeCancellationReason {
185    pub const fn is_known(self) -> bool {
186        self.0 <= Self::SHUTDOWN.0
187    }
188}
189
190impl NativeResumeKind {
191    pub const fn is_known(self) -> bool {
192        self.0 <= Self::OPTIMIZED_NATIVE.0
193    }
194}
195
196impl NativeDeoptReason {
197    pub const fn is_known(self) -> bool {
198        self.0 <= Self::EXPLICIT_SLOW_PATH.0
199    }
200}
201
202fn validate_version(version: u32) -> Result<(), NativeAbiError> {
203    if version != NATIVE_ABI_VERSION.encoded() {
204        return Err(NativeAbiError::new(
205            "native.abi_version",
206            "unsupported native ABI version",
207        ));
208    }
209    Ok(())
210}
211
212fn validate_zero(field: &'static str, value: u32) -> Result<(), NativeAbiError> {
213    if value != 0 {
214        return Err(NativeAbiError::new(
215            field,
216            "reserved bits must be zero for this ABI revision",
217        ));
218    }
219    Ok(())
220}
221
222pub(super) fn validate_slice<T>(
223    field: &'static str,
224    pointer: *const T,
225    count: usize,
226) -> Result<(), NativeAbiError> {
227    if pointer.is_null() != (count == 0) {
228        return Err(NativeAbiError::new(
229            field,
230            "pointer must be null exactly when count is zero",
231        ));
232    }
233    Ok(())
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use crate::native::{
240        NativeCallKind, NativeCancellation, NativeException, NativeFrameKind, NativeResumeState,
241        NativeRoot, NativeSourceLocation, NativeValueRef,
242    };
243
244    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
245    #[cfg_attr(not(target_arch = "wasm32"), test)]
246    fn call_and_frame_require_exact_borrowed_state() {
247        let mut resume = NativeResumeState::default();
248        let mut local = NativeValueRef::NULL;
249        let root = NativeRoot {
250            value: NativeValueRef::NULL,
251            kind: crate::native::NativeRootKind::LOCAL,
252            slot: 0,
253        };
254        let mut frame = NativeFrame {
255            kind: NativeFrameKind::FUNCTION,
256            locals: &mut local,
257            local_count: 1,
258            operands: std::ptr::null_mut(),
259            operand_capacity: 0,
260            roots: NativeRootSet {
261                roots: &root,
262                count: 1,
263            },
264            resume: &mut resume,
265            ..NativeFrame::default()
266        };
267        assert_eq!(frame.validate(), Ok(()));
268
269        let host = std::ptr::dangling::<crate::native::NativeHostVTable>();
270        let call = NativeCall {
271            kind: NativeCallKind::DIRECT,
272            host,
273            frame: &mut frame,
274            ..NativeCall::default()
275        };
276        assert_eq!(call.validate(), Ok(()));
277
278        let invalid = NativeCall {
279            requested_outputs: 1,
280            ..call
281        };
282        assert_eq!(
283            invalid.validate().unwrap_err().field,
284            "native.call.requested_outputs"
285        );
286
287        let invalid_kind = NativeCall {
288            kind: NativeCallKind(u32::MAX),
289            ..call
290        };
291        assert_eq!(
292            invalid_kind.validate().unwrap_err().field,
293            "native.call.kind"
294        );
295    }
296
297    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
298    #[cfg_attr(not(target_arch = "wasm32"), test)]
299    fn unknown_control_codes_fail_closed_without_invalid_rust_enums() {
300        assert!(!NativeCancellationReason(u32::MAX).is_known());
301        assert!(!NativeResumeKind(u32::MAX).is_known());
302        assert!(!NativeDeoptReason(u32::MAX).is_known());
303        assert!(!NativeValueRef {
304            handle: 1,
305            generation: 0
306        }
307        .is_valid());
308
309        let invalid_resume = NativeResumeState {
310            flags: 1,
311            ..NativeResumeState::default()
312        };
313        assert_eq!(
314            invalid_resume.validate().unwrap_err().field,
315            "native.resume.flags"
316        );
317
318        let invalid_source = NativeSourceLocation {
319            start: 2,
320            end: 1,
321            ..NativeSourceLocation::default()
322        };
323        assert_eq!(
324            invalid_source.validate().unwrap_err().field,
325            "native.source_location"
326        );
327    }
328
329    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
330    #[cfg_attr(not(target_arch = "wasm32"), test)]
331    fn transactional_exit_variants_validate_their_selected_payload() {
332        assert_eq!(NativeExit::completed(2).validate(), Ok(()));
333        assert_eq!(
334            NativeExit::exception(NativeException {
335                handle: 7,
336                generation: 1,
337                source: NativeSourceLocation::default(),
338            })
339            .validate(),
340            Ok(())
341        );
342        assert_eq!(
343            NativeExit::cancelled(NativeCancellation {
344                reason: NativeCancellationReason::DEADLINE,
345                reserved: 0,
346                generation: 2,
347            })
348            .validate(),
349            Ok(())
350        );
351        assert!(NativeExit::exception(NativeException::default())
352            .validate()
353            .is_err());
354
355        let mut resume = NativeResumeState::default();
356        let mut frame = NativeFrame {
357            resume: &mut resume,
358            ..NativeFrame::default()
359        };
360        let mut result = NativeValueRef::NULL;
361        let call = NativeCall {
362            requested_outputs: 1,
363            host: std::ptr::dangling::<crate::native::NativeHostVTable>(),
364            frame: &mut frame,
365            results: &mut result,
366            result_capacity: 1,
367            ..NativeCall::default()
368        };
369        assert_eq!(call.validate_exit(&NativeExit::completed(1)), Ok(()));
370        assert_eq!(
371            call.validate_exit(&NativeExit::completed(2))
372                .unwrap_err()
373                .field,
374            "native.exit.produced_outputs"
375        );
376
377        let mut non_transactional = NativeExit::cancelled(NativeCancellation {
378            reason: NativeCancellationReason::REQUESTED,
379            reserved: 0,
380            generation: 1,
381        });
382        non_transactional.produced_outputs = 1;
383        assert_eq!(
384            call.validate_exit(&non_transactional).unwrap_err().field,
385            "native.exit.produced_outputs"
386        );
387    }
388}