diff --git a/rust/ares/Cargo.lock b/rust/ares/Cargo.lock index d585a78..02fe6dd 100644 --- a/rust/ares/Cargo.lock +++ b/rust/ares/Cargo.lock @@ -59,9 +59,10 @@ name = "ares" version = "0.1.0" dependencies = [ "ares_crypto", + "ares_guard", "ares_macros", "ares_pma", - "assert_no_alloc", + "assert_no_alloc 1.1.2", "autotools", "bitvec", "cc", @@ -86,7 +87,7 @@ version = "0.1.0" dependencies = [ "aes", "aes-siv", - "assert_no_alloc", + "assert_no_alloc 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "curve25519-dalek", "ed25519-dalek", "ibig 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -95,6 +96,14 @@ dependencies = [ "x25519-dalek", ] +[[package]] +name = "ares_guard" +version = "0.1.0" +dependencies = [ + "bindgen", + "cc", +] + [[package]] name = "ares_macros" version = "0.1.0" @@ -111,6 +120,10 @@ dependencies = [ "cc", ] +[[package]] +name = "assert_no_alloc" +version = "1.1.2" + [[package]] name = "assert_no_alloc" version = "1.1.2" @@ -145,11 +158,11 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.69.1" +version = "0.69.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ffcebc3849946a7170a05992aac39da343a90676ab392c51a4280981d6379c2" +checksum = "a4c69fae65a523209d34240b60abe0c42d33d1045d445c0839d8a4894a736e2d" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "cexpr", "clang-sys", "lazy_static", @@ -174,9 +187,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "bitvec" @@ -664,9 +677,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "log" @@ -801,9 +814,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.76" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] @@ -860,9 +873,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.2" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", @@ -872,9 +885,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" dependencies = [ "aho-corasick", "memchr", @@ -904,11 +917,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.28" +version = "0.38.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "errno", "libc", "linux-raw-sys", @@ -991,9 +1004,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" diff --git a/rust/ares/Cargo.toml b/rust/ares/Cargo.toml index 3d7ec25..8928d11 100644 --- a/rust/ares/Cargo.toml +++ b/rust/ares/Cargo.toml @@ -11,14 +11,15 @@ edition = "2018" # Please keep these alphabetized [dependencies] +ares_guard = { path = "../ares_guard" } ares_crypto = { path = "../ares_crypto" } ares_macros = { path = "../ares_macros" } # Use this when debugging requires the debug printfs in the PMA # ares_pma = { path = "../ares_pma", features=["debug_prints"] } ares_pma = { path = "../ares_pma" } -assert_no_alloc = "1.1.2" # use this when debugging requires allocation (e.g. eprintln) -# assert_no_alloc = {version="1.1.2", features=["warn_debug"]} +# assert_no_alloc = { path = "../assert-no-alloc", features=["warn_debug"] } +assert_no_alloc = { path = "../assert-no-alloc" } bitvec = "1.0.0" criterion = "0.4" either = "1.9.0" @@ -35,8 +36,8 @@ signal-hook = "0.3" static_assertions = "1.1.0" [build-dependencies] -autotools = "0.2.6" -cc = "1.0.79" +autotools = "0.2" +cc = "1.0" [[bin]] name = "ares" diff --git a/rust/ares/src/guard.rs b/rust/ares/src/guard.rs new file mode 100644 index 0000000..843a4d8 --- /dev/null +++ b/rust/ares/src/guard.rs @@ -0,0 +1,114 @@ +use crate::interpreter::{Error, Mote, Result}; +use crate::noun::D; +use ares_guard::*; +use assert_no_alloc::permit_alloc; +use std::convert::TryFrom; +use std::ffi::c_void; +use std::marker::PhantomData; + +#[derive(Debug)] +pub enum GuardError { + InvalidSignal, + MemoryProtection, + NullPointer, + OutOfMemory, + Setup, + Unknown, +} + +impl From for GuardError { + fn from(value: u32) -> Self { + match value { + GUARD_NULL => Self::NullPointer, + GUARD_SIGNAL => Self::InvalidSignal, + GUARD_OOM => Self::OutOfMemory, + x if (x & GUARD_MPROTECT) != 0 => Self::MemoryProtection, + x if (x & (GUARD_MALLOC | GUARD_SIGACTION)) != 0 => Self::Setup, + _ => Self::Unknown, + } + } +} + +pub struct CCallback<'closure> { + pub function: unsafe extern "C" fn(*mut c_void) -> *mut c_void, + pub input: *mut c_void, + // without this it's too easy to accidentally drop the closure too soon + _lifetime: PhantomData<&'closure mut c_void>, +} + +impl<'closure> CCallback<'closure> { + pub fn new(closure: &'closure mut F) -> Self + where + F: FnMut() -> Result, + { + let function: unsafe extern "C" fn(*mut c_void) -> *mut c_void = Self::call_closure::; + + // debug_assert_eq!( + // std::mem::size_of::<&'closure mut F>(), + // std::mem::size_of::<*const c_void>() + // ); + // debug_assert_eq!( + // std::mem::size_of_val(&function), + // std::mem::size_of::<*const c_void>() + // ); + + Self { + function, + input: closure as *mut F as *mut c_void, + _lifetime: PhantomData, + } + } + + unsafe extern "C" fn call_closure(input: *mut c_void) -> *mut c_void + where + F: FnMut() -> Result, + { + let cb: &mut F = input.cast::().as_mut().unwrap(); + let v = (*cb)(); + permit_alloc(|| { + let v_box = Box::new(v); + let v_ptr = Box::into_raw(v_box); + v_ptr as *mut c_void + }) + } +} + +pub fn call_with_guard Result>( + stack_pp: *const *const u64, + alloc_pp: *const *const u64, + closure: &mut F, +) -> Result { + let cb = CCallback::new(closure); + let mut ret_p: *mut c_void = std::ptr::null_mut(); + let ret_pp = &mut ret_p as *mut *mut c_void; + + unsafe { + let res = guard( + Some(cb.function as unsafe extern "C" fn(*mut c_void) -> *mut c_void), + cb.input, + stack_pp as *const usize, + alloc_pp as *const usize, + ret_pp, + ); + + // eprintln!("\r BEFORE:"); + // eprintln!("\r ret = {:?}", ret); + // eprintln!("\r ret_p = {:p}, {:?}", ret_p as *mut Result, *(ret_p as *mut Result)); + // eprintln!("\r ret_pp = {:p}, {:p}, {:?}", ret_pp, *ret_pp, **(ret_pp as *mut *mut Result)); + if res == 0 { + // TODO: come back to this + permit_alloc(|| { + let result_box = Box::from_raw(ret_p as *mut Result); + *result_box + }) + } else { + let err = GuardError::from(u32::try_from(res).unwrap()); + match err { + GuardError::OutOfMemory => Err(Error::NonDeterministic(Mote::Meme, D(0))), + _ => { + panic!("serf: guard: unexpected error {:?}", err); + } + } + } + } +} diff --git a/rust/ares/src/lib.rs b/rust/ares/src/lib.rs index 17b7223..d6f6391 100644 --- a/rust/ares/src/lib.rs +++ b/rust/ares/src/lib.rs @@ -3,6 +3,7 @@ extern crate num_derive; extern crate lazy_static; #[macro_use] extern crate static_assertions; +pub mod guard; pub mod hamt; pub mod interpreter; pub mod jets; diff --git a/rust/ares/src/mem.rs b/rust/ares/src/mem.rs index f2c403c..210e897 100644 --- a/rust/ares/src/mem.rs +++ b/rust/ares/src/mem.rs @@ -152,6 +152,16 @@ impl NockStack { self.alloc_pointer } + /** Current stack pointer of this NockStack */ + pub fn get_stack_pointer_pointer(&self) -> *const *mut u64 { + &self.stack_pointer + } + + /** Current alloc pointer of this NockStack */ + pub fn get_alloc_pointer_pointer(&self) -> *const *mut u64 { + &self.alloc_pointer + } + /** Start of the memory range for this NockStack */ pub fn get_start(&self) -> *const u64 { self.start diff --git a/rust/ares/src/serf.rs b/rust/ares/src/serf.rs index 3aa8031..8da4e92 100644 --- a/rust/ares/src/serf.rs +++ b/rust/ares/src/serf.rs @@ -1,3 +1,4 @@ +use crate::guard::call_with_guard; use crate::hamt::Hamt; use crate::interpreter; use crate::interpreter::{inc, interpret, Error, Mote}; @@ -143,7 +144,7 @@ impl Context { snapshot: Option, constant_hot_state: &[HotEntry], ) -> Self { - let mut stack = NockStack::new(4096 << 10 << 10, 0); + let mut stack = NockStack::new(2048 << 10 << 10, 0); let newt = Newt::new(); let cache = Hamt::::new(&mut stack); @@ -401,7 +402,28 @@ fn slam(context: &mut Context, axis: u64, ovo: Noun) -> Result { let sam = T(stack, &[D(6), D(0), D(7)]); let fol = T(stack, &[D(8), pul, D(9), D(2), D(10), sam, D(0), D(2)]); let sub = T(stack, &[arvo, ovo]); - interpret(&mut context.nock_context, sub, fol) + + let frame_p = stack.get_frame_pointer(); + let stack_pp = stack.get_stack_pointer_pointer(); + let alloc_pp = stack.get_alloc_pointer_pointer(); + + let res = call_with_guard( + stack_pp as *const *const u64, + alloc_pp as *const *const u64, + &mut || interpret(&mut context.nock_context, sub, fol), + ); + + if let Err(Error::NonDeterministic(Mote::Meme, _)) = res { + unsafe { + let stack = &mut context.nock_context.stack; + assert_no_alloc::reset_counters(); + while stack.get_frame_pointer() != frame_p { + stack.frame_pop(); + } + } + } + + res } fn peek(context: &mut Context, ovo: Noun) -> Noun { @@ -604,18 +626,14 @@ fn work_swap(context: &mut Context, job: Noun, goof: Noun) { context.work_swap(ovo, fec); } Err(goof_crud) => { - work_bail(context, &[goof_crud, goof]); + eprintln!("\r serf: bail"); + let stack = &mut context.nock_context.stack; + let lud = T(stack, &[goof_crud, goof, D(0)]); + context.work_bail(lud); } } } -fn work_bail(context: &mut Context, goofs: &[Noun]) { - let stack = &mut context.nock_context.stack; - let lest = T(stack, goofs); - let lud = T(stack, &[lest, D(0)]); - context.work_bail(lud); -} - fn work_trace_name(stack: &mut NockStack, wire: Noun, vent: Atom) -> String { let wpc = path_to_cord(stack, wire); let wpc_len = met3_usize(wpc); diff --git a/rust/ares_guard/Cargo.lock b/rust/ares_guard/Cargo.lock new file mode 100644 index 0000000..26d68b9 --- /dev/null +++ b/rust/ares_guard/Cargo.lock @@ -0,0 +1,432 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "ares_guard" +version = "0.1.0" +dependencies = [ + "bindgen", + "cc", +] + +[[package]] +name = "bindgen" +version = "0.69.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c69fae65a523209d34240b60abe0c42d33d1045d445c0839d8a4894a736e2d" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + +[[package]] +name = "libloading" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "prettyplease" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "shlex" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" diff --git a/rust/ares_guard/Cargo.toml b/rust/ares_guard/Cargo.toml new file mode 100644 index 0000000..5a3f718 --- /dev/null +++ b/rust/ares_guard/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "ares_guard" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +[build-dependencies] +bindgen = "0.69" +cc = "1.0" diff --git a/rust/ares_guard/build.rs b/rust/ares_guard/build.rs new file mode 100644 index 0000000..0c11bf3 --- /dev/null +++ b/rust/ares_guard/build.rs @@ -0,0 +1,77 @@ +extern crate bindgen; + +use std::env; +use std::path::PathBuf; + +fn main() { + let opt_level = env::var("OPT_LEVEL").unwrap(); + let define_debug = if env::var("CARGO_FEATURE_DEBUG_PRINTS").is_ok() { + "-DDEBUG" + } else { + "-UDEBUG" + }; + + // This is the directory where the `c` library is located. + let libdir_path = PathBuf::from("c-src") + // Canonicalize the path as `rustc-link-search` requires an absolute + // path. + .canonicalize() + .expect("cannot canonicalize path"); + let libdir_path_str = libdir_path.to_str().expect("Path is not a valid string"); + + // This is the path to the `c` headers file. + let headers_path = libdir_path.join("wrapper.h"); + let headers_path_str = headers_path.to_str().expect("Path is not a valid string"); + + println!("cargo:rerun-if-changed={}", libdir_path_str); + + let res = cc::Build::new() + .file( + libdir_path + .join("guard.c") + .to_str() + .expect("Path is not a valid string"), + ) + .flag(format!("-O{}", opt_level).as_ref()) + .flag(define_debug) + .flag("-g3") + .flag("-Wall") + .flag("-Wextra") + .flag("-Wformat=2") + .flag("-Wmissing-include-dirs") + .flag("-Wnested-externs") + .flag("-Wpedantic") + .flag("-Wredundant-decls") + .flag("-Wshadow") + .flag("-Wwrite-strings") + .flag("-Wno-unused-parameter") + .flag("-Wno-pointer-arith") + .flag("-Wno-strict-prototypes") + .flag("-Wno-unused-function") + .try_compile("guard"); + + if let Err(err) = res { + panic!("{}", err); + } + + // The bindgen::Builder is the main entry point + // to bindgen, and lets you build up options for + // the resulting bindings. + let bindings = bindgen::Builder::default() + // The input header we would like to generate + // bindings for. + .header(headers_path_str) + // Tell cargo to invalidate the built crate whenever any of the + // included header files changed. + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + // Finish the builder and generate the bindings. + .generate() + // Unwrap the Result and panic on failure. + .expect("Unable to generate bindings"); + + // Write the bindings to the $OUT_DIR/bindings.rs file. + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("bindings.rs"); + bindings + .write_to_file(out_path) + .expect("Couldn't write bindings!"); +} diff --git a/rust/ares_guard/c-src/guard.c b/rust/ares_guard/c-src/guard.c new file mode 100644 index 0000000..8aa71f4 --- /dev/null +++ b/rust/ares_guard/c-src/guard.c @@ -0,0 +1,287 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "guard.h" + +#define GD_PAGE_BITS 14ULL +#define GD_PAGE_SIZE (1ULL << GD_PAGE_BITS) // 16 KB +#define GD_PAGE_MASK (GD_PAGE_SIZE - 1) +#define GD_PAGE_ROUND_DOWN(foo) (foo & (~GD_PAGE_MASK)) + +/** + * XX: documentation + */ +typedef struct _gs { + uintptr_t guard_p; + const uintptr_t *stack_pp; + const uintptr_t *alloc_pp; + jmp_buf env_buffer; + struct sigaction prev_sa; +} GuardState; + +static GuardState *_guard_state = NULL; + + +static int32_t +_prot_page(void *address, int prot) +{ + if (mprotect(address, GD_PAGE_SIZE, prot)) { + fprintf(stderr, "guard: prot: mprotect error %d\r\n", errno); + fprintf(stderr, "%s\r\n", strerror(errno)); + return guard_mprotect | errno; + } + + return 0; +} + +static int32_t +_mark_page(void *address) +{ + return _prot_page(address, PROT_NONE); +} + +static int32_t +_unmark_page(void *address) +{ + return _prot_page(address, PROT_READ | PROT_WRITE); +} + +/** + * Center the guard page. + */ +static int32_t +_focus_guard( + uintptr_t *guard_pp, + const uintptr_t stack_p, + const uintptr_t alloc_p +) { + // Check for strange situations. + fprintf(stderr, "guard: focus: stack pointer at %p\r\n", (void *)stack_p); + fprintf(stderr, "guard: focus: alloc pointer at %p\r\n", (void *)alloc_p); + if (stack_p == 0 || alloc_p == 0) { + fprintf(stderr, "guard: focus: stack or alloc pointer is null\r\n"); + return guard_null; + } else if (stack_p == alloc_p) { + fprintf(stderr, "guard: focus: stack and alloc pointers equal\r\n"); + return guard_oom; + } + + uintptr_t old_guard_p = *guard_pp; + uintptr_t new_guard_p; + int32_t err = 0; + + fprintf(stderr, "guard: focus: old guard = %p\r\n", (void *)old_guard_p); + + // Unmark the old guard page (if there is one) + if (old_guard_p) { + if ((err = _unmark_page((void *)old_guard_p))) { + fprintf(stderr, "guard: focus: unmark error\r\n"); + return err; + } + } + + // Compute new guard page + // XX: Should we also check for new_guard_p < min(stack_p, alloc_p)? + new_guard_p = GD_PAGE_ROUND_DOWN((stack_p + alloc_p) / 2); + fprintf(stderr, "guard: focus: new guard = %p\r\n", (void *)new_guard_p); + if (new_guard_p == old_guard_p) { + fprintf(stderr, "guard: focus: OOM\r\n"); + return guard_oom; + } + + // Mark new guard page + if ((err = _mark_page((void *)new_guard_p))) { + fprintf(stderr, "guard: focus: mark error\r\n"); + return err; + } + + // Update guard page tracker + fprintf(stderr, "guard: focus: installed guard page at %p\r\n", (void *)new_guard_p); + *guard_pp = new_guard_p; + + return 0; +} + +void +_signal_handler(int sig, siginfo_t *si, void *unused) +{ + uintptr_t sig_addr; + int32_t err = 0; + + fprintf(stderr, "guard: sig_handle: %d received\r\n", sig); + + if (sig != SIGSEGV) { + fprintf(stderr, "guard: sig_handle: invalid signal\r\n"); + siglongjmp(_guard_state->env_buffer, guard_signal); + } + + sig_addr = (uintptr_t)si->si_addr; + fprintf(stderr, "guard: SIGSEGV address = %p\r\n", (void *)sig_addr); + + fprintf(stderr, "guard: sig_handle: %p \r\n", _guard_state); + if ( + sig_addr >= _guard_state->guard_p && + sig_addr < (_guard_state->guard_p + GD_PAGE_SIZE)) + { + fprintf(stderr, "guard: page at %p hit\r\n", (void *)_guard_state->guard_p); + err = _focus_guard( + &(_guard_state->guard_p), + *(_guard_state->stack_pp), + *(_guard_state->alloc_pp)); + if (err) { + fprintf(stderr, "guard: sig_handle: focus error\r\n"); + siglongjmp(_guard_state->env_buffer, err); + } + } else { + struct sigaction prev_sa = _guard_state->prev_sa; + + fprintf(stderr, "guard: page at %p miss\r\n", (void *)_guard_state->guard_p); + + if (prev_sa.sa_sigaction != NULL) { + prev_sa.sa_sigaction(sig, si, unused); + } else if (prev_sa.sa_handler != NULL) { + prev_sa.sa_handler(sig); + } else { + // There should always be a default SIGSEGV handler + assert(0); + } + } +} + +int32_t +_register_handler(struct sigaction *prev_sa) +{ + struct sigaction sa; + + // Flag to use sa_sigaction + sa.sa_flags = SA_SIGINFO; + // Must use sa_sigaction; sa-handler takes signal handler as its only argument + sa.sa_sigaction = _signal_handler; + // Set mask of signals to ignore while running signal handler + // TODO: By default the signal that triggered the signal handler is automatically added to the + // mask while it's being handled, so unless we plan to add more signals to this then I + // don't think it's necessary. + // sigemptyset(&sa.sa_mask); + // sigaddset(&(sa.sa_mask), SIGSEGV); + + // Set the new SIGSEGV handler, and save the old SIGSEGV handler (if any) + if (sigaction(SIGSEGV, &sa, prev_sa)) { + fprintf(stderr, "guard: register: sigaction error\r\n"); + fprintf(stderr, "%s\r\n", strerror(errno)); + return guard_sigaction | errno; + } + + return 0; +} + +int32_t +_setup( + GuardState **gs_p, + const uintptr_t *const stack_pp, + const uintptr_t *const alloc_pp +) { + GuardState *gs; + int32_t err = 0; + + assert(*gs_p == NULL); + fprintf(stderr, "guard: setup: stack pointer at %p\r\n", (void *)(*stack_pp)); + fprintf(stderr, "guard: setup: alloc pointer at %p\r\n", (void *)(*alloc_pp)); + + // Setup guard page state + *gs_p = (GuardState *)malloc(sizeof(GuardState)); + gs = *gs_p; + if (gs == NULL) { + fprintf(stderr, "guard: malloc error\r\n"); + fprintf(stderr, "%s\r\n", strerror(errno)); + return guard_malloc | errno; + } + fprintf(stderr, "guard: state allocated to %p \r\n", gs); + + gs->guard_p = 0; + gs->stack_pp = stack_pp; + gs->alloc_pp = alloc_pp; + + // Initialize the guard page + if ((err = _focus_guard(&(gs->guard_p), *stack_pp, *alloc_pp))) { + fprintf(stderr, "guard: setup: _focus_guard error\r\n"); + return err; + } + + // Register guard page signal handler + if ((err = _register_handler(&(gs->prev_sa)))) { + fprintf(stderr, "guard: setup: _register_handler error\r\n"); + return err; + } + + return 0; +} + +int32_t +_teardown(GuardState** gs_p) +{ + int32_t err = 0; + + if (*gs_p != NULL) { + GuardState *gs = *gs_p; + + if (gs->guard_p != 0) { + err = _unmark_page((void *)gs->guard_p); + } + + if (sigaction(SIGSEGV, &(gs->prev_sa), NULL)) { + fprintf(stderr, "guard: teardown: sigaction error\r\n"); + fprintf(stderr, "%s\r\n", strerror(errno)); + err = guard_sigaction | errno; + } + + free(gs); + *gs_p = NULL; + } + + return err; +} + +int32_t +guard( + callback f, + void *closure, + const uintptr_t *const stack_pp, + const uintptr_t *const alloc_pp, + void ** ret +) { + int32_t err = 0; + + // Setup the guard page + fprintf(stderr, "guard: setup\r\n"); + if ((err = _setup(&_guard_state, stack_pp, alloc_pp))) { + goto done; + } + + // Run given closure + fprintf(stderr, "guard: run\r\n"); + if (!(err = sigsetjmp(_guard_state->env_buffer, 1))) { + *ret = f(closure); + + // Clean up + fprintf(stderr, "guard: teardown\r\n"); + err = _teardown(&_guard_state); + + fprintf(stderr, "guard: return\r\n"); + return err; + } else { +done: + // Clean up + fprintf(stderr, "guard: teardown\r\n"); + _teardown(&_guard_state); + + fprintf(stderr, "guard: return\r\n"); + return err; + } +} diff --git a/rust/ares_guard/c-src/guard.h b/rust/ares_guard/c-src/guard.h new file mode 100644 index 0000000..fd08343 --- /dev/null +++ b/rust/ares_guard/c-src/guard.h @@ -0,0 +1,59 @@ +#ifndef __GUARD_H__ +#define __GUARD_H__ + +#include + +/** + * Error codes and flags. + * + * The flags are bitwise added to the errno of their respective errors. + */ +typedef enum { + guard_null = 1, // null stack or alloc pointer + guard_signal, // invalid signal + guard_oom, // OOM + guard_malloc = 0x10000000, // malloc error flag + guard_mprotect = 0x20000000, // mprotect error flag + guard_sigaction = 0x40000000, // sigaction error flag +} guard_err; + +typedef void *(*callback)(void *); + +/** + * Execute the given closure `f` within the memory arena between the + * `stack` and `alloc` pointers, with guard page protection. Write either + * `f`'s succesful result or a `guard_err` to the given `ret` pointer. + * + * Memory + * ------ + * The free memory arena between the `stack` and `alloc` pointers is part of a + * NockStack frame, which may either face east or west. If the frame faces + * east, the `stack` pointer will be greater than the `alloc` pointer. If it + * faces west, the `stack` pointer will be less than the `alloc` pointer. + * + * All the pages in the memory arena are marked clean (`PROT_READ | PROT_WRITE`) + * by default, with the exception of a single guard page in the middle of the + * arena, which is marked with `PROT_NONE`. + * + * Guard + * ----- + * This function protects the free memory arena between the `stack` and `alloc` + * pointers with a guard page. A guard page is simply a single page of memory + * which is marked with `PROT_NONE`. Since all other pages are marked clean by + * default, a SIGSEGV will only be raised if the `f` function attempts to write + * to the guard page. When it does, the signal handler will attempt to re-center + * the guard page in the remaining free space left in the arena. If there is no + * more free space, then memory exhaustion has occurred and the `guard_spent` + * error will be written to the `ret` pointer. The caller is then responsible + * for handling this error and aborting with a `bail:meme`. + */ +int32_t +guard( + callback f, + void *closure, + const uintptr_t *const stack_pp, + const uintptr_t *const alloc_pp, + void ** ret +); + +#endif // __GUARD_H__ diff --git a/rust/ares_guard/c-src/wrapper.h b/rust/ares_guard/c-src/wrapper.h new file mode 100644 index 0000000..29d14dd --- /dev/null +++ b/rust/ares_guard/c-src/wrapper.h @@ -0,0 +1 @@ +#include "guard.h" diff --git a/rust/ares_guard/src/lib.rs b/rust/ares_guard/src/lib.rs new file mode 100644 index 0000000..38e6c3c --- /dev/null +++ b/rust/ares_guard/src/lib.rs @@ -0,0 +1,12 @@ +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); + +pub const GUARD_NULL: u32 = guard_err_guard_null; +pub const GUARD_SIGNAL: u32 = guard_err_guard_signal; +pub const GUARD_OOM: u32 = guard_err_guard_oom; +pub const GUARD_MALLOC: u32 = guard_err_guard_malloc; +pub const GUARD_MPROTECT: u32 = guard_err_guard_mprotect; +pub const GUARD_SIGACTION: u32 = guard_err_guard_sigaction; diff --git a/rust/assert-no-alloc/Cargo.toml b/rust/assert-no-alloc/Cargo.toml new file mode 100644 index 0000000..bbe18fb --- /dev/null +++ b/rust/assert-no-alloc/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "assert_no_alloc" +version = "1.1.2" +authors = ["Florian Jung "] +edition = "2018" +license = "BSD-1-Clause" +description = "Custom Rust allocator allowing to temporarily disable memory (de)allocations for a thread. Aborts or prints a warning if allocating although forbidden." +homepage = "https://github.com/Windfisch/rust-assert-no-alloc" +repository = "https://github.com/Windfisch/rust-assert-no-alloc" +readme = "README.md" +keywords = ["allocator", "real-time", "debug", "audio"] +categories = ["development-tools::debugging"] + +[features] +default = ["disable_release"] +warn_debug = [] +warn_release = [] +disable_release = [] + +# Print a backtrace before aborting the program when an allocation failure happens +backtrace = ["dep:backtrace"] +# Use the `log` crate instead of printing to STDERR +# WARNING: If the allocation failure happens during a logger call, then +# depending on the logger's implementation this may block indefinitely +log = ["dep:log"] + +[dependencies] +backtrace = { version = "0.3", optional = true } +log = { version = "0.4", optional = true } + +[package.metadata.docs.rs] +features = ["warn_debug"] diff --git a/rust/assert-no-alloc/LICENSE b/rust/assert-no-alloc/LICENSE new file mode 100644 index 0000000..c108e4a --- /dev/null +++ b/rust/assert-no-alloc/LICENSE @@ -0,0 +1,21 @@ +assert_no_alloc -- A custom Rust allocator allowing to temporarily disable +memory (de)allocations for a thread. + +Copyright (c) 2020 Florian Jung + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/rust/assert-no-alloc/README.md b/rust/assert-no-alloc/README.md new file mode 100644 index 0000000..8411b1c --- /dev/null +++ b/rust/assert-no-alloc/README.md @@ -0,0 +1,143 @@ +assert_no_alloc +=============== + +This crate provides a custom allocator that allows to temporarily disable +memory (de)allocations for a thread. If a (de)allocation is attempted +anyway, the program will abort or print a warning. + +It uses thread local storage for the "disabled-flag/counter", and thus +should be thread safe, if the underlying allocator (currently hard-coded +to `std::alloc::System`) is. + +[documentation @ docs.rs](https://docs.rs/assert_no_alloc/1.1.0/assert_no_alloc/), +[crates.io](https://crates.io/crates/assert_no_alloc) + +Rationale +--------- + +No-allocation-zones are relevant e.g. in real-time scenarios like audio +callbacks. Allocation and deallocation can take unpredictable amounts of +time, and thus can *sometimes* lead to audible glitches because the audio +data is not served in time. + +Debugging such problems can be hard, because it is difficult to reproduce +such problems consistently. Avoiding such problems is also hard, since +allocation/deallocation is a common thing to do and most libraries are not +explicit whether certain functions can allocate or not. Also, this might +even depend on the run-time situation (e.g. a `Vec::push` might allocate, +but it is guaranteed to not allocate *if* enough space has been `reserve()`d +before). + +To aid the developer in tackling these problems, this crate offers an easy +way of detecting all forbidden allocations. + +How to use +---------- + +First, configure the features: `warn_debug` and `warn_release` change the +behaviour from aborting your program into just printing an error message +on `stderr`. Aborting is useful for debugging purposes, as it allows you +to retrieve a stacktrace, while warning is less intrusive. + +Note that you need to disable the (default-enabled) `disable_release` feature +by specify `default-features = false` if you want to use `warn_release`. If +`disable_release` is set (which is the default), then this crate will do +nothing if built in `--release` mode. + +Second, use the allocator provided by this crate. Add this to `main.rs`: + +```rust +use assert_no_alloc::*; + +#[cfg(debug_assertions)] // required when disable_release is set (default) +#[global_allocator] +static A: AllocDisabler = AllocDisabler; +``` + +Third, wrap code sections that may not allocate like this: + +```rust +assert_no_alloc(|| { + println!("This code can not allocate."); +}); +``` + +Advanced use +------------ + +Values can be returned using: + +```rust +let answer = assert_no_alloc(|| { 42 }); +``` + +The effect of `assert_no_alloc` can be overridden using `permit_alloc`: + +```rust +assert_no_alloc(|| { + permit_alloc(|| { + // Allocate some memory here. This will work. + }); +}); +``` + +This is useful for test stubs whose code is executed in an `assert_no_alloc` +context. + +Objects that deallocate upon `Drop` can be wrapped in `PermitDrop`: + +```rust +let foo = PermitDrop::new( + permit_alloc(|| + Box::new(...) + ) +); +``` + +Dropping `foo` will not trigger an assertion (but dropping a `Box` would). + +`assert_no_alloc()` calls can be nested, with proper panic unwinding handling. + +Note that to fully bypass this crate, e.g. when in release mode, you need to +*both* have the `disable_release` feature flag enabled (which it is by default) +and to not register `AllocDisabler` as `global_allocator`. + +Optional features +----------------- + +These compile time features are not enabled by default: + +- `backtrace` causes a backtrace to be printed before the allocation failure. + This backtrace is gathered at runtime, and its accuracy depends on the + platform and the compilation options used. +- `log` uses the `log` crate to write the allocation failure message to the + configured logger. If the `backtrace` feature is also enabled, then the + backtrace will also be written to the logger This can be useful when using a + logger that writes directly to a file or any other place that isn't STDERR. + + The main caveat here is that if the allocation was caused by the logger and if + the logger wraps its entire log function in a regular non-entrant mutex, then + this may result in a deadlock. Make sure your logger doesn't do this before + enabling this feature. + +Examples +-------- + +See [examples/main.rs](https://github.com/Windfisch/rust-assert-no-alloc/blob/master/examples/main.rs) for an example. + +You can try out the different feature flags: + +- `cargo run --example main` -> memory allocation of 4 bytes failed. Aborted (core dumped) +- `cargo run --example main --release --no-default-features` -> same as above. +- `cargo run --example main --features=warn_debug` -> Tried to (de)allocate memory in a thread that forbids allocator calls! This will not be executed if the above allocation has aborted. +- `cargo run --example main --features=warn_release --release --no-default-features` -> same as above. +- `cargo run --example main --release` will not even check for forbidden allocations + +Test suite +---------- + +The tests will fail to compile with the default features. Run them using: + +``` +cargo test --features=warn_debug --tests +``` diff --git a/rust/assert-no-alloc/examples/main.rs b/rust/assert-no-alloc/examples/main.rs new file mode 100644 index 0000000..7f43d57 --- /dev/null +++ b/rust/assert-no-alloc/examples/main.rs @@ -0,0 +1,34 @@ +use assert_no_alloc::*; + +#[cfg(debug_assertions)] +#[global_allocator] +static A: AllocDisabler = AllocDisabler; + +fn main() { + println!("Alloc is allowed. Let's allocate some memory..."); + let mut vec_can_push = Vec::new(); + vec_can_push.push(42); + + println!(); + + let fib5 = assert_no_alloc(|| { + println!("Alloc is forbidden. Let's calculate something without memory allocations..."); + + fn fib(n: u32) -> u32 { + if n<=1 { 1 } + else { fib(n-1) + fib(n-2) } + } + + fib(5) + }); + println!("\tSuccess, the 5th fibonacci number is {}", fib5); + println!(); + + assert_no_alloc(|| { + println!("Alloc is forbidden. Let's allocate some memory..."); + let mut vec_cannot_push = Vec::new(); + vec_cannot_push.push(42); // panics + }); + + println!("This will not be executed if the above allocation has aborted."); +} diff --git a/rust/assert-no-alloc/src/lib.rs b/rust/assert-no-alloc/src/lib.rs new file mode 100644 index 0000000..00f6a92 --- /dev/null +++ b/rust/assert-no-alloc/src/lib.rs @@ -0,0 +1,249 @@ +/* assert_no_alloc -- A custom Rust allocator allowing to temporarily disable + * memory (de)allocations for a thread. + * + * Copyright (c) 2020 Florian Jung + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#![doc = include_str!("../README.md")] + +use std::alloc::{System,GlobalAlloc,Layout}; +use std::cell::Cell; + +// check for mutually exclusive features. +#[cfg(all(feature = "disable_release", feature = "warn_release"))] +compile_error!("disable_release cannot be active at the same time with warn_release"); + + +#[cfg(not(all(feature = "disable_release", not(debug_assertions))))] // if not disabled +thread_local! { + static ALLOC_FORBID_COUNT: Cell = Cell::new(0); + static ALLOC_PERMIT_COUNT: Cell = Cell::new(0); + + #[cfg(any( all(feature="warn_debug", debug_assertions), all(feature="warn_release", not(debug_assertions)) ))] + static ALLOC_VIOLATION_COUNT: Cell = Cell::new(0); +} + +#[cfg(all(feature = "disable_release", not(debug_assertions)))] // if disabled +pub fn assert_no_alloc T> (func: F) -> T { // no-op + func() +} + +#[cfg(all(feature = "disable_release", not(debug_assertions)))] // if disabled +pub fn permit_alloc T> (func: F) -> T { // no-op + func() +} + +#[cfg(not(all(feature = "disable_release", not(debug_assertions))))] // if not disabled +/// Calls the `func` closure, but forbids any (de)allocations. +/// +/// If a call to the allocator is made, the program will abort with an error, +/// print a warning (depending on the `warn_debug` feature flag. Or ignore +/// the situation, when compiled in `--release` mode with the `disable_release` +///feature flag set (which is the default)). +pub fn assert_no_alloc T> (func: F) -> T { + // RAII guard for managing the forbid counter. This is to ensure correct behaviour + // when catch_unwind is used + struct Guard; + impl Guard { + fn new() -> Guard { + ALLOC_FORBID_COUNT.with(|c| c.set(c.get()+1)); + Guard + } + } + impl Drop for Guard { + fn drop(&mut self) { + ALLOC_FORBID_COUNT.with(|c| c.set(c.get()-1)); + } + } + + + #[cfg(any( all(feature="warn_debug", debug_assertions), all(feature="warn_release", not(debug_assertions)) ))] // if warn mode is selected + let old_violation_count = violation_count(); + + + let guard = Guard::new(); // increment the forbid counter + let ret = func(); + std::mem::drop(guard); // decrement the forbid counter + + + #[cfg(any( all(feature="warn_debug", debug_assertions), all(feature="warn_release", not(debug_assertions)) ))] // if warn mode is selected + if violation_count() > old_violation_count { + eprintln!("Tried to (de)allocate memory in a thread that forbids allocator calls!"); + } + + return ret; +} + +#[cfg(not(all(feature = "disable_release", not(debug_assertions))))] // if not disabled +/// Calls the `func` closure. Allocations are temporarily allowed, even if this +/// code runs inside of assert_no_alloc. +pub fn permit_alloc T> (func: F) -> T { + // RAII guard for managing the permit counter + struct Guard; + impl Guard { + fn new() -> Guard { + ALLOC_PERMIT_COUNT.with(|c| c.set(c.get()+1)); + Guard + } + } + impl Drop for Guard { + fn drop(&mut self) { + ALLOC_PERMIT_COUNT.with(|c| c.set(c.get()-1)); + } + } + + let guard = Guard::new(); // increment the forbid counter + let ret = func(); + std::mem::drop(guard); // decrement the forbid counter + + return ret; +} + +#[cfg(any( all(feature="warn_debug", debug_assertions), all(feature="warn_release", not(debug_assertions)) ))] // if warn mode is selected +/// Returns the count of allocation warnings emitted so far. +/// +/// Only available when the `warn_debug` or `warn release` features are enabled. +pub fn violation_count() -> u32 { + ALLOC_VIOLATION_COUNT.with(|c| c.get()) +} + +#[cfg(any( all(feature="warn_debug", debug_assertions), all(feature="warn_release", not(debug_assertions)) ))] // if warn mode is selected +/// Resets the count of allocation warnings to zero. +/// +/// Only available when the `warn_debug` or `warn release` features are enabled. +pub fn reset_violation_count() { + ALLOC_VIOLATION_COUNT.with(|c| c.set(0)); +} + +pub fn reset_counters() { + ALLOC_FORBID_COUNT.with(|c| c.set(0)); + ALLOC_PERMIT_COUNT.with(|c| c.set(0)); + + #[cfg(any( all(feature="warn_debug", debug_assertions), all(feature="warn_release", not(debug_assertions)) ))] + ALLOC_VIOLATION_COUNT.with(|c| c.set(0)); +} + + + +#[cfg(not(all(feature = "disable_release", not(debug_assertions))))] // if not disabled +/// The custom allocator that handles the checking. +/// +/// To use this crate, you must add the following in your `main.rs`: +/// ```rust +/// use assert_no_alloc::*; +/// // ... +/// #[cfg(debug_assertions)] +/// #[global_allocator] +/// static A: AllocDisabler = AllocDisabler; +/// ``` +pub struct AllocDisabler; + +#[cfg(not(all(feature = "disable_release", not(debug_assertions))))] // if not disabled +impl AllocDisabler { + fn check(&self, layout: Layout) { + let forbid_count = ALLOC_FORBID_COUNT.with(|f| f.get()); + let permit_count = ALLOC_PERMIT_COUNT.with(|p| p.get()); + if forbid_count > 0 && permit_count == 0 { + #[cfg(any( all(feature="warn_debug", debug_assertions), all(feature="warn_release", not(debug_assertions)) ))] // if warn mode is selected + ALLOC_VIOLATION_COUNT.with(|c| c.set(c.get()+1)); + + #[cfg(any( all(not(feature="warn_debug"), debug_assertions), all(not(feature="warn_release"), not(debug_assertions)) ))] // if abort mode is selected + { + #[cfg(all(feature = "log", feature = "backtrace"))] + permit_alloc(|| log::error!("Memory allocation of {} bytes failed from:\n{:?}", layout.size(), backtrace::Backtrace::new())); + #[cfg(all(feature = "log", not(feature = "backtrace")))] + permit_alloc(|| log::error!("Memory allocation of {} bytes failed", layout.size())); + + #[cfg(all(not(feature = "log"), feature = "backtrace"))] + permit_alloc(|| eprintln!("Allocation failure from:\n{:?}", backtrace::Backtrace::new())); + + // This handler can be overridden (although as of writing, the API to do so is still + // unstable) so we must always call this even when the log feature is enabled + std::alloc::handle_alloc_error(layout); + } + } + } +} + +#[cfg(not(all(feature = "disable_release", not(debug_assertions))))] // if not disabled +unsafe impl GlobalAlloc for AllocDisabler { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + self.check(layout); + System.alloc(layout) + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + self.check(layout); + System.dealloc(ptr, layout) + } +} + +/// Wrapper for objects whose Drop implementation shall be permitted +/// to (de)allocate. +/// +/// Typical usage: +/// +/// ```rust +/// let foo = PermitDrop::new( +/// permit_alloc(|| +/// Box::new(...) +/// ) +/// ); +/// ``` +/// +/// Here, creation of the Box is guarded by the explicit `permit_alloc` call, +/// and destruction of the Box is guarded by PermitDrop. Neither creation nor +/// destruction will cause an assertion failure from within `assert_no_alloc`. +pub struct PermitDrop(Option); + +impl PermitDrop { + pub fn new(t: T) -> PermitDrop { + permit_alloc(|| { + PermitDrop(Some(t)) + }) + } +} + +impl std::ops::Deref for PermitDrop { + type Target = T; + fn deref(&self) -> &T { self.0.as_ref().unwrap() } +} + +impl std::ops::DerefMut for PermitDrop { + fn deref_mut(&mut self) -> &mut T { self.0.as_mut().unwrap() } +} + +impl Iterator for PermitDrop { + type Item = I::Item; + fn next(&mut self) -> Option { + (**self).next() + } +} + + +impl Drop for PermitDrop { + fn drop(&mut self) { + let mut tmp = None; + std::mem::swap(&mut tmp, &mut self.0); + permit_alloc(|| { + std::mem::drop(tmp); + }); + } +} diff --git a/rust/assert-no-alloc/tests/test.rs b/rust/assert-no-alloc/tests/test.rs new file mode 100644 index 0000000..d5f6c8d --- /dev/null +++ b/rust/assert-no-alloc/tests/test.rs @@ -0,0 +1,148 @@ +use assert_no_alloc::*; +use std::panic::catch_unwind; + +#[global_allocator] +static A: AllocDisabler = AllocDisabler; + +#[cfg(not(feature = "warn_debug"))] +compile_error!("The test suite requires the warn_debug feature to be enabled. Use `cargo test --features warn_debug`"); + +// This is only a kludge; what we actually want to check is "will do_alloc() be optimized out?", e.g. due to +// compiler optimizations turned on in --release mode. We can't do that, the closest we can get is to check +// whether debug_assertions are disabled, which coincidentially also happens in release mode. +#[cfg(not(debug_assertions))] +compile_error!("The test suite only works in debug mode. Use `cargo test --features warn_debug`"); + +#[cfg(feature = "warn_debug")] +fn check_and_reset() -> bool { + let result = violation_count() > 0; + reset_violation_count(); + result +} + +// Provide a stub check_and_reset() function if warn_debug is disabled. This will never be compiled due to the +// compile_error!() above, but this stub ensures that the output will not be cluttered with spurious error +// messages. +#[cfg(not(feature = "warn_debug"))] +fn check_and_reset() -> bool { unreachable!() } + +fn do_alloc() { + let _tmp: Box = Box::new(42); +} + +#[test] +fn ok_noop() { + assert_eq!(check_and_reset(), false); + do_alloc(); + assert_eq!(check_and_reset(), false); +} + +#[test] +fn ok_simple() { + assert_eq!(check_and_reset(), false); + assert_no_alloc(|| { + }); + + do_alloc(); + assert_eq!(check_and_reset(), false); +} + +#[test] +fn ok_nested() { + assert_eq!(check_and_reset(), false); + assert_no_alloc(|| { + assert_no_alloc(|| { + }); + }); + + do_alloc(); + assert_eq!(check_and_reset(), false); +} + +#[test] +fn forbidden_simple() { + assert_eq!(check_and_reset(), false); + assert_no_alloc(|| { + do_alloc(); + }); + assert_eq!(check_and_reset(), true); +} + +#[test] +fn forbidden_in_nested() { + assert_eq!(check_and_reset(), false); + assert_no_alloc(|| { + assert_no_alloc(|| { + do_alloc(); + }); + }); + assert_eq!(check_and_reset(), true); +} + +#[test] +fn forbidden_after_nested() { + assert_eq!(check_and_reset(), false); + assert_no_alloc(|| { + assert_no_alloc(|| { + }); + do_alloc(); + }); + assert_eq!(check_and_reset(), true); +} + +#[test] +fn unwind_ok() { + assert_eq!(check_and_reset(), false); + assert_no_alloc(|| { + let r = catch_unwind(|| { + assert_no_alloc(|| { + panic!(); + }); + }); + assert!(r.is_err()); + }); + check_and_reset(); // unwinding might have allocated memory; we don't care about that. + do_alloc(); + assert_eq!(check_and_reset(), false); +} + +#[test] +fn unwind_nested() { + assert_eq!(check_and_reset(), false); + assert_no_alloc(|| { + let r = catch_unwind(|| { + assert_no_alloc(|| { + panic!(); + }); + }); + assert!(r.is_err()); + + check_and_reset(); // unwinding might have allocated memory; we don't care about that. + do_alloc(); + assert_eq!(check_and_reset(), true); + }); +} + +#[test] +fn unwind_nested2() { + assert_eq!(check_and_reset(), false); + assert_no_alloc(|| { + assert_no_alloc(|| { + let r = catch_unwind(|| { + assert_no_alloc(|| { + assert_no_alloc(|| { + panic!(); + }); + }); + }); + assert!(r.is_err()); + + check_and_reset(); // unwinding might have allocated memory; we don't care about that. + do_alloc(); + assert_eq!(check_and_reset(), true); + }); + }); + check_and_reset(); // unwinding might have allocated memory; we don't care about that. + do_alloc(); + assert_eq!(check_and_reset(), false); +}