GENERAL BREAK: get rid of signed pointers

This commit is contained in:
zseri 2022-09-30 12:35:08 +02:00
parent 18a7c524e6
commit e101c9fd9f
7 changed files with 26 additions and 181 deletions

7
Cargo.lock generated
View file

@ -93,7 +93,6 @@ version = "0.0.0"
dependencies = [
"int-enum",
"proptest",
"siphasher",
]
[[package]]
@ -328,12 +327,6 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "siphasher"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
[[package]]
name = "smallvec"
version = "1.9.0"

View file

@ -11,10 +11,6 @@ license = "LGPL-2.1+"
version = "0.4"
default-features = false
[dependencies.siphasher]
version = "0.3"
default-features = false
[dev-dependencies.proptest]
version = "1.0"
default-features = false

View file

@ -32,7 +32,7 @@ pub enum Instr {
// basic stack operations
/// pushes the given value to the stack
Push(u128),
Push(u64),
/// pops the top $0+1 values from the stack
Pop(u16),
@ -105,7 +105,7 @@ impl<'a> crate::Parse<'a> for Instr {
OpType::CallLocal => u64::parse(inp).map(|(inp, val)| (inp, Instr::CallLocal(val))),
OpType::CallLDefer => u64::parse(inp).map(|(inp, val)| (inp, Instr::CallLDefer(val))),
OpType::JumpCond => u64::parse(inp).map(|(inp, val)| (inp, Instr::JumpCond(val))),
OpType::Push => u128::parse(inp).map(|(inp, val)| (inp, Instr::Push(val))),
OpType::Push => u64::parse(inp).map(|(inp, val)| (inp, Instr::Push(val))),
OpType::Pop => u16::parse(inp).map(|(inp, val)| (inp, Instr::Pop(val))),
OpType::Dup => u16::parse(inp).map(|(inp, val)| (inp, Instr::Dup(val))),
OpType::Swap => u16::parse(inp).map(|(inp, val)| (inp, Instr::Swap(val))),

View file

@ -8,5 +8,3 @@ mod instr;
pub use instr::{Instr, ParseError as InstrParseError};
mod parse;
pub use parse::Parse;
mod pointer;
pub use pointer::Pointer;

View file

@ -1,150 +0,0 @@
use siphasher::sip::SipHasher24 as SipHasher;
/// A signed pointer, the `payload` should never by dereferenced
/// without verifying the pointer first
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C, align(16))]
pub struct Pointer(pub [u8; 16]);
impl From<u128> for Pointer {
#[inline]
fn from(x: u128) -> Self {
Self(x.to_be_bytes())
}
}
impl From<(u64, u64)> for Pointer {
#[inline]
fn from((a, b): (u64, u64)) -> Self {
let mut this = Self([0; 16]);
{
let (a_, b_) = this.0.split_at_mut(8);
a_.copy_from_slice(&a.to_be_bytes());
b_.copy_from_slice(&b.to_be_bytes());
}
this
}
}
impl From<Pointer> for u128 {
#[inline]
fn from(Pointer(x): Pointer) -> u128 {
u128::from_be_bytes(x)
}
}
impl From<Pointer> for (u64, u64) {
#[inline]
fn from(Pointer(x): Pointer) -> (u64, u64) {
let (a, b) = x.split_at(8);
(
u64::from_be_bytes(a.try_into().unwrap()),
u64::from_be_bytes(b.try_into().unwrap()),
)
}
}
impl Pointer {
fn calculate_hmac(payload: u64, key: &u128) -> u64 {
use core::hash::Hasher;
let mut h = SipHasher::new_with_key(&key.to_be_bytes());
h.write_u64(payload & ((1 << 48) - 1));
h.finish()
}
/// SECURITY NOTE: the upper two bytes of `payload` (`origin`) aren't taken
/// into account when calculating the HMAC because they're node-specific
pub fn new_with_key(payload: u64, key: &u128) -> Pointer {
let hmac = Self::calculate_hmac(payload, key);
Self::from((hmac, payload))
}
#[inline]
pub fn verify(&self, key: &u128) -> Option<u64> {
let (hmac, payload) = (*self).into();
if hmac == Self::calculate_hmac(payload, key) {
Some(payload)
} else {
None
}
}
#[inline]
pub fn set_origin(&mut self, origin: u16) {
self.0[8..10].copy_from_slice(&origin.to_be_bytes());
}
#[inline]
pub fn origin(&self) -> u16 {
u16::from_be_bytes(self.0[8..10].try_into().unwrap())
}
}
impl crate::Parse<'_> for Pointer {
type Err = ();
fn parse(inp: &[u8]) -> Result<(&[u8], Self), ()> {
if inp.len() < 16 {
Err(())
} else {
let (this, next) = inp.split_at(16);
Ok((next, Self(this.try_into().unwrap())))
}
}
}
#[cfg(any(test, feature = "std"))]
impl Pointer {
#[inline]
pub fn write_to<W: std::io::Write>(&self, mut writer: W) -> std::io::Result<()> {
writer.write_all(&self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
proptest::proptest! {
#[test]
fn u128_roundtrip(x in 0..u128::MAX) {
let ptr = Pointer::from(x);
assert_eq!(u128::from(ptr), x);
}
}
#[test]
fn pointer_repr() {
use core::mem;
assert_eq!(mem::size_of::<Pointer>(), 16);
assert_eq!(mem::align_of::<Pointer>(), 16);
}
#[test]
fn pointer_usage() {
let k: u128 = 0x000000000000dead000000000000beef;
let p = Pointer::new_with_key(0xfefe, &k);
// verify that this is the same value on all systems
assert_eq!(p.0[0..8], [42, 115, 131, 215, 127, 235, 147, 241]);
assert_eq!(p.verify(&k), Some(0xfefe));
assert_eq!(p.origin(), 0);
}
#[test]
fn pointer_usage2() {
let k: u128 = 0x000000000000dead000000000000beef;
let p = Pointer::new_with_key(0x0508deadbeeffefe, &k);
// verify that this is the same value on all systems
assert_eq!(p.0[0..8], [191, 23, 107, 0, 61, 74, 249, 219]);
assert_eq!(p.verify(&k), Some(0x0508deadbeeffefe));
assert_eq!(p.origin(), 0x0508);
}
#[test]
fn pointer_usage3() {
let k: u128 = 0x000000000000dead000000000000beef;
let p = Pointer::new_with_key(0xf7d8deadbeeffefe, &k);
// verify that this is the same value on all systems
assert_eq!(p.0[0..8], [191, 23, 107, 0, 61, 74, 249, 219]);
assert_eq!(p.verify(&k), Some(0xf7d8deadbeeffefe));
assert_eq!(p.origin(), 0xf7d8);
}
}

View file

@ -5,7 +5,7 @@ extern crate alloc;
use alloc::{sync::Arc, vec::Vec};
use core::fmt;
use fogtix_bytecode::{consts, Instr, Parse, Pointer};
use fogtix_bytecode::{consts, Instr, Parse};
pub type Module = Arc<dyn ModuleKind>;
@ -61,7 +61,7 @@ impl InstrPtr {
}
}
pub type StackEntValue = u128;
pub type StackEntValue = u64;
#[derive(Clone, Debug)]
pub enum Error {
@ -75,7 +75,7 @@ pub enum Error {
OutOfFuel,
NotEnoughStacked,
DivisionByZero,
RemoteCall(Pointer),
RemoteCall(u64),
}
impl fmt::Display for Error {
@ -97,7 +97,7 @@ impl fmt::Display for Error {
Self::OutOfFuel => write!(f, "out of fuel"),
Self::NotEnoughStacked => write!(f, "not enough operands on stack"),
Self::DivisionByZero => write!(f, "tried to divide by zero"),
Self::RemoteCall(ptr) => write!(f, "tried to call remote @ {:x?}", ptr.0),
Self::RemoteCall(ptr) => write!(f, "tried to call remote @ {:x?}", ptr),
}
}
}
@ -157,7 +157,7 @@ impl Process {
if !self.instrp.is_call2jump() {
self.callstack.push(self.instrp.clone());
}
return Err(Error::RemoteCall(Pointer::from(self.stpop()?)));
return Err(Error::RemoteCall(self.stpop()?));
}
Instr::CallLocal(x) => {
if !self.instrp.is_call2jump() {
@ -282,8 +282,8 @@ impl Process {
Some(x) => x,
None => return Err(Error::DivisionByZero),
},
B::Srem => match (a as i128).checked_rem(b as i128) {
Some(x) => x as u128,
B::Srem => match (a as i64).checked_rem(b as i64) {
Some(x) => x as u64,
None => return Err(Error::DivisionByZero),
},
});
@ -299,7 +299,7 @@ mod tests {
#[test]
fn stack_item_size() {
assert_eq!(core::mem::size_of::<StackEntValue>(), 16);
assert_eq!(core::mem::size_of::<StackEntValue>(), 8);
}
proptest::proptest! {

View file

@ -1,15 +1,8 @@
# basic ideas
* use tagged, signed pointers for memory addresses
* use a separate call stack to prevent ROP
* implement message passing
# values
* atoms (u128): atomic values, also used as pointer keys to sign pointers
* bytes ([]u8): arbitrary byte strings
* int (u64): the default integer type
* pointer (u128): signed pointer (can only be accessed with matching key)
* integers as only internal data type
# entry points and libraries
@ -17,3 +10,18 @@ each module starts with the entry point, this avoids the need for a separate "en
the entry point of the main module is what's initially called by the VM to start execution.
the entry point of library modules are called to build the library descriptor table,
which should leave an additional pointer on the stack which can be passed to `call-r`.
# historic
## signed pointers
Initially, this project tried to implement an idea of signed pointers, so that a
party can verify that it authored a pointer (via HMAC, e.g. SipHash); but some
problems remain, which make this impractical
* either it is insecure because the pointer is too small to store a resistant-enough hash
* thus, the pointer is so large that it gets unwieldly (e.g. `u128`)
* how to find out which node or process owns a pointer if you aren't that node
(the hash only indicates if you own it or not, not who owns it)
* how to find out if the pointer is still valid (you need some hashmap or so to store that),
but then you have basically reinvented file descriptors.