refactor: move verification code from 'vm' to 'bytecode' crate

This commit is contained in:
Alain Zscheile 2022-10-23 01:17:34 +02:00
parent c04860b933
commit 28b60c5dbb
5 changed files with 90 additions and 87 deletions

View file

@ -143,6 +143,19 @@ impl Instr {
}
}
pub fn next_instr(m: &[u8], pos: usize) -> Option<Result<(usize, Instr), &[u8]>> {
use crate::Parse;
m.get(pos..).map(|nxti_arr| match Instr::parse(nxti_arr) {
Err(_) => Err(&nxti_arr[..core::cmp::min(nxti_arr.len(), 20)]),
Ok((ptr, i)) => Ok((nxti_arr.len() - ptr.len(), i)),
})
}
pub fn is_call2jump(m: &[u8], pos: usize) -> bool {
// tail-call optimization (would otherwise require much more opcodes)
matches!(next_instr(m, pos), Some(Ok((_, Instr::Return))))
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -1,10 +1,13 @@
#![cfg_attr(not(any(test, feature = "std")), no_std)]
#![forbid(unsafe_code, unused_variables)]
/// Fogtix bytecode is structured as type-(length?)-value items, which can be nested.
#[cfg(test)]
extern crate alloc;
pub mod consts;
mod instr;
pub use instr::{Instr, ParseError as InstrParseError};
pub use instr::{is_call2jump, next_instr, Instr, ParseError as InstrParseError};
mod parse;
pub use parse::{Parse, ParseExt};
mod verify;
pub use verify::{verify, VerifyError, VerifyErrorKind};

View file

@ -0,0 +1,69 @@
use crate::{next_instr, Instr};
use core::fmt;
#[derive(Clone, Debug)]
pub struct VerifyError<'m> {
pub from: usize,
pub kind: VerifyErrorKind<'m>,
}
#[derive(Clone, Debug)]
pub enum VerifyErrorKind<'m> {
InstrpOutOfBounds,
UnparsableInstruction(&'m [u8]),
}
impl fmt::Display for VerifyError<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use VerifyErrorKind as K;
write!(f, "at instruction {}: ", self.from)?;
match &self.kind {
K::InstrpOutOfBounds => write!(f, "instruction pointer out-of-bounds"),
K::UnparsableInstruction(x) => write!(f, "reached unparsable instruction: {:x?}", x),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for VerifyError<'_> {}
/// this should be called before attempting to run an opcode stream to ensure
/// that jump targets are valid (this makes it possible to avoid unnecessary
/// checks during tight loops)
pub fn verify(m: &[u8]) -> Result<(), VerifyError<'_>> {
let mut instrp = 0;
while let Some(nxti) = next_instr(m, instrp) {
let (nxtidelta, nxti) = nxti.map_err(|code| VerifyError {
from: instrp,
kind: VerifyErrorKind::UnparsableInstruction(code),
})?;
assert_ne!(nxtidelta, 0);
let vfe = VerifyError {
from: instrp,
kind: VerifyErrorKind::InstrpOutOfBounds,
};
instrp += nxtidelta;
match nxti {
Instr::CallLocal(x) | Instr::CallLDefer(x) | Instr::JumpCond(x) => {
match usize::try_from(x) {
Err(_) => return Err(vfe),
Ok(x) if x >= m.len() => return Err(vfe),
Ok(_) => {}
}
}
Instr::CallRemote(_)
| Instr::Return
| Instr::Push(_)
| Instr::Pop(_)
| Instr::Dup(_)
| Instr::Swap(_)
| Instr::Shift(_)
| Instr::DupFrom
| Instr::DoMath1(_)
| Instr::DoMath2(_) => {}
}
}
Ok(())
}

View file

@ -18,7 +18,7 @@ fn main() {
let main_mod = readfilez::read_from_file(std::fs::File::open(main_mod_path))
.expect("unable to open/load main module");
if let Err(e) = fogtix_vm::verify(&main_mod) {
if let Err(e) = fogtix_bytecode::verify(&main_mod) {
eprintln!("ERROR: {}", e);
std::process::exit(1);
}

View file

@ -5,19 +5,7 @@ extern crate alloc;
use alloc::vec::Vec;
use core::fmt;
use fogtix_bytecode::{consts, Instr, Parse};
fn next_instr(m: &[u8], pos: usize) -> Option<Result<(usize, Instr), &[u8]>> {
m.get(pos..).map(|nxti_arr| match Instr::parse(nxti_arr) {
Err(_) => Err(&nxti_arr[..core::cmp::min(nxti_arr.len(), 20)]),
Ok((ptr, i)) => Ok((nxti_arr.len() - ptr.len(), i)),
})
}
fn is_call2jump(m: &[u8], pos: usize) -> bool {
// tail-call optimization (would otherwise require much more opcodes)
matches!(next_instr(m, pos), Some(Ok((_, Instr::Return))))
}
use fogtix_bytecode::{consts, is_call2jump, next_instr, Instr};
pub type StackEntValue = u64;
@ -40,76 +28,6 @@ impl fmt::Display for Error<'_> {
}
}
#[derive(Clone, Debug)]
pub struct VerifyError<'m> {
pub from: usize,
pub kind: VerifyErrorKind<'m>,
}
#[derive(Clone, Debug)]
pub enum VerifyErrorKind<'m> {
InstrpOutOfBounds,
UnparsableInstruction(&'m [u8]),
}
impl fmt::Display for VerifyError<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use VerifyErrorKind as K;
write!(f, "at instruction {}: ", self.from)?;
match &self.kind {
K::InstrpOutOfBounds => write!(f, "instruction pointer out-of-bounds"),
K::UnparsableInstruction(x) => write!(f, "reached unparsable instruction: {:x?}", x),
}
}
}
/// this should be called before attempting to run an opcode stream to ensure
/// that jump targets are valid (this makes it possible to avoid unnecessary
/// checks during tight loops)
pub fn verify(m: &[u8]) -> Result<(), VerifyError<'_>> {
let mut instrp = 0;
while let Some(nxti) = next_instr(m, instrp) {
let (nxtidelta, nxti) = nxti.map_err(|code| VerifyError {
from: instrp,
kind: VerifyErrorKind::UnparsableInstruction(code),
})?;
assert_ne!(nxtidelta, 0);
instrp += nxtidelta;
match nxti {
Instr::CallLocal(x) | Instr::CallLDefer(x) | Instr::JumpCond(x) => {
match usize::try_from(x) {
Err(_) => {
return Err(VerifyError {
from: usize::MAX,
kind: VerifyErrorKind::InstrpOutOfBounds,
})
}
Ok(x) if x >= m.len() => {
return Err(VerifyError {
from: x,
kind: VerifyErrorKind::InstrpOutOfBounds,
})
}
Ok(_) => {}
}
}
Instr::CallRemote(_)
| Instr::Return
| Instr::Push(_)
| Instr::Pop(_)
| Instr::Dup(_)
| Instr::Swap(_)
| Instr::Shift(_)
| Instr::DupFrom
| Instr::DoMath1(_)
| Instr::DoMath2(_) => {}
}
}
Ok(())
}
pub struct Process<'m> {
pub stack: Vec<StackEntValue>,
pub m: &'m [u8],
@ -264,7 +182,7 @@ mod tests {
#[test]
fn doesnt_crash(inp in proptest::collection::vec(0..=u8::MAX, 0..1024)) {
if verify(&inp[..]).is_err() {
if fogtix_bytecode::verify(&inp[..]).is_err() {
return Ok(());
}
let mut p = Process {