From 77aa4f2b7bbc470fe6ae2b0ab8f894818d72edd2 Mon Sep 17 00:00:00 2001 From: Ryan Zmuda Date: Wed, 10 Jun 2026 11:53:09 -0400 Subject: [PATCH 01/14] wip: shaping stack sobjs --- resolve-cveassert/libresolve/src/remediate.rs | 44 ++++++++++--- .../libresolve/src/shadowobjs.rs | 65 +++++++++++++++++++ 2 files changed, 100 insertions(+), 9 deletions(-) diff --git a/resolve-cveassert/libresolve/src/remediate.rs b/resolve-cveassert/libresolve/src/remediate.rs index bd17bfa5..68d04ff1 100644 --- a/resolve-cveassert/libresolve/src/remediate.rs +++ b/resolve-cveassert/libresolve/src/remediate.rs @@ -4,7 +4,7 @@ use libc::{ c_char, c_void, calloc, free, malloc, realloc, strdup, strlen, strndup, strnlen, }; -use crate::shadowobjs::{ALIVE_OBJ_LIST, AllocType, FREED_OBJ_LIST, Vaddr}; +use crate::shadowobjs::{SHADOW_STACK, ALIVE_OBJ_LIST, AllocType, FREED_OBJ_LIST, Vaddr}; use log::{info, warn}; @@ -16,10 +16,11 @@ use log::{info, warn}; #[unsafe(no_mangle)] pub extern "C" fn __resolve_alloca(ptr: *mut c_void, size: usize) -> () { let base = ptr as Vaddr; - { - let mut obj_list = ALIVE_OBJ_LIST.lock(); - obj_list.add_shadow_object(AllocType::Stack, base, size); - } + + SHADOW_STACK.with_borrow_mut( + |ss| + ss.add_shadow_object(base, size) + ); info!("[STACK] Object allocated with size: {size}, address: 0x{base:x}"); } @@ -28,10 +29,10 @@ pub extern "C" fn __resolve_alloca(ptr: *mut c_void, size: usize) -> () { pub extern "C" fn __resolve_invalidate_stack(base: *mut c_void) { let base = base as Vaddr; - { - let mut obj_list = ALIVE_OBJ_LIST.lock(); - obj_list.invalidate_at(base); - } + SHADOW_STACK.with_borrow_mut( + |ss| + ss.invalidate_at(base) + ); info!("[STACK] Free addr 0x{base:x}"); } @@ -261,6 +262,31 @@ pub struct ShadowObjBounds { * shadow object as pointers */ #[unsafe(no_mangle)] +pub extern "C" fn __resolve_get_bounds_stack(ptr: *mut c_void) -> ShadowObjBounds { + SHADOW_STACK.with_borrow_mut( + |ss| { + match ss.search_intersection(ptr as Vaddr) { + Some(sobj) => { return ShadowObjBounds { base: sobj.base as *mut c_void, limit: sobj.limit as *mut c_void }; } + None => { return ShadowObjBounds { base: std::ptr::null_mut(), limit: std::ptr::null_mut() }; } + } + } + ); + + // This would only be reached if .with_borrow_mut failed, right? + // TODO: what to do in this case? + return ShadowObjBounds { base: std::ptr::null_mut(), limit: std::ptr::null_mut() }; +} + +/** + * @brief - Helper function that queries stack shadow obj list + * to find a shadow obj where the ptr fits within + * its bounds of allocation + * @input + * - ptr: ptr to allocation + * @return struct containing the base and limit of the + * shadow object as pointers + */ +#[unsafe(no_mangle)] pub extern "C" fn __resolve_get_bounds(ptr: *mut c_void) -> ShadowObjBounds { let sobj_table = ALIVE_OBJ_LIST.lock(); let Some(sobj) = sobj_table.search_intersection(ptr as Vaddr) else { diff --git a/resolve-cveassert/libresolve/src/shadowobjs.rs b/resolve-cveassert/libresolve/src/shadowobjs.rs index 0fa2cf1e..7d034621 100644 --- a/resolve-cveassert/libresolve/src/shadowobjs.rs +++ b/resolve-cveassert/libresolve/src/shadowobjs.rs @@ -2,6 +2,8 @@ // LGPL-3; See LICENSE.txt in the repo root for details. use crate::MutexWrap; +use crate::shadowobjs::ShadowStackObject::FrameFrontPtr; +use std::cell::RefCell; use std::collections::BTreeMap; use std::ops::RangeInclusive; use std::ops::Bound::Included; @@ -117,6 +119,69 @@ impl ShadowObjectTable { pub static ALIVE_OBJ_LIST: MutexWrap = MutexWrap::new(ShadowObjectTable::new()); pub static FREED_OBJ_LIST: MutexWrap = MutexWrap::new(ShadowObjectTable::new()); +/* + Stack shadow object shape: + [frame 0] [ frame 1 ] + [ | | | ][*][ | | | | | ][*] -> (growth direction) + <---------| <-------------| + + We grow by appending ShadowObjects to the end of the Vector, + and inserting new FrameFrontPtrs when we push new frames. + These FrameFrontPtrs reference the beginning of the pervious + frame, letting us backwards traverse in order to search +*/ +enum ShadowStackObject { + ShadowObject(ShadowObject), + FrameFrontPtr(u64) +} + +pub struct ShadowStack { + data: Vec, + tip: u64 +} + +impl ShadowStack { + pub fn new() -> Self { + Self { + data: vec![ShadowStackObject::FrameFrontPtr(0)], + tip: 1, + } + } + + pub fn add_shadow_object(&mut self, base: Vaddr, size: usize) { + let sobj = ShadowObject { + alloc_type: AllocType::Stack, + base, + limit: ShadowObject::limit(base, size), + size, + }; + self.data.push(ShadowStackObject::ShadowObject(sobj)); + + self.tip += 1; + } + + pub fn invalidate_at(&self, base: Vaddr) { + // TODO + } + + /* + Perform a backwards search from tip by jumping + to current frame start and comparing Vaddr. If + Vaddr < FrameVaddr we jump back until the condition + is met, then incrementally search the found frame to + resolve the shadowobject. + */ + pub fn search_intersection(&self, addr: Vaddr) -> Option<&ShadowObject> { + // TODO + + None + } +} + +thread_local! { + pub static SHADOW_STACK: RefCell = RefCell::new(ShadowStack::new()); +} + #[cfg(test)] mod tests { use crate::shadowobjs::{AllocType, ShadowObjectTable}; From eaf437d2fb2ae3feacfd1831b8f6a05d8031b9a3 Mon Sep 17 00:00:00 2001 From: Ryan Zmuda Date: Thu, 11 Jun 2026 08:37:45 -0400 Subject: [PATCH 02/14] backing up buggy locate() --- .../libresolve/src/shadowobjs.rs | 77 ++++++++++++++++++- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/resolve-cveassert/libresolve/src/shadowobjs.rs b/resolve-cveassert/libresolve/src/shadowobjs.rs index 7d034621..de672ea4 100644 --- a/resolve-cveassert/libresolve/src/shadowobjs.rs +++ b/resolve-cveassert/libresolve/src/shadowobjs.rs @@ -2,6 +2,7 @@ // LGPL-3; See LICENSE.txt in the repo root for details. use crate::MutexWrap; +use crate::shadowobjs::LookupError::{AddrOOB, ObjectNotFound}; use crate::shadowobjs::ShadowStackObject::FrameFrontPtr; use std::cell::RefCell; use std::collections::BTreeMap; @@ -132,12 +133,17 @@ pub static FREED_OBJ_LIST: MutexWrap = MutexWrap::new(ShadowO */ enum ShadowStackObject { ShadowObject(ShadowObject), - FrameFrontPtr(u64) + FrameFrontPtr(usize) } pub struct ShadowStack { data: Vec, - tip: u64 + tip: usize +} + +enum LookupError { + AddrOOB, + ObjectNotFound, } impl ShadowStack { @@ -155,11 +161,77 @@ impl ShadowStack { limit: ShadowObject::limit(base, size), size, }; + let ffp = self.data.pop().expect("Missing frame front pointer"); self.data.push(ShadowStackObject::ShadowObject(sobj)); + self.data.push(ffp); self.tip += 1; } + /* + The search algorithm to walk the table and return an index + and object reference if found + + Error here? + */ + fn locate(&self, addr: Vaddr) -> Result<(&ShadowObject, usize), LookupError> { + if self.tip <= 1 { + return Err(ObjectNotFound) + } + + let mut itr = self.tip - 1; // -1 because tip is the write head + let mut last = itr; + + while itr > 0 { + // get the beginning of the current frame from the cap pointer + let ShadowStackObject::FrameFrontPtr(frame_start_idx) = + self.data.get(itr).expect("Malformed shadow stack head") + else { + panic!("Malformed shadow stack head!") + }; + + // get the first object in the current frame + let ShadowStackObject::ShadowObject(first_frame_obj) = + self.data.get(*frame_start_idx).expect("Malformed shadow stack") + else { + panic!("Malformed shadow stack ordering!") + }; + + if first_frame_obj.base > addr { + last = itr; + itr = *frame_start_idx - 1; + + // check if we have traversed entire stack + if itr <= 0 { + return Err(ObjectNotFound) + } + + continue; + } + + // addr allegedly now exists in our current frame; iterate it + while itr < last { + // get current object + let ShadowStackObject::ShadowObject(sobj) = + self.data.get(itr).expect("Malformed shadow stack") + else { + panic!("Malformed shadow stack ordering!") + }; + + if sobj.contains(addr) { + return Ok((sobj,itr)) + } + + itr+=1; + } + + return Err(ObjectNotFound); + } + + // I don't think this is ever reached? + Err(ObjectNotFound) + } + pub fn invalidate_at(&self, base: Vaddr) { // TODO } @@ -172,7 +244,6 @@ impl ShadowStack { resolve the shadowobject. */ pub fn search_intersection(&self, addr: Vaddr) -> Option<&ShadowObject> { - // TODO None } From 626ed78f2c2e108d7ba8cb665ebfc17f3eace14b Mon Sep 17 00:00:00 2001 From: Ryan Zmuda Date: Thu, 11 Jun 2026 08:51:39 -0400 Subject: [PATCH 03/14] updates to locate() --- .../libresolve/src/shadowobjs.rs | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/resolve-cveassert/libresolve/src/shadowobjs.rs b/resolve-cveassert/libresolve/src/shadowobjs.rs index de672ea4..7718fb5c 100644 --- a/resolve-cveassert/libresolve/src/shadowobjs.rs +++ b/resolve-cveassert/libresolve/src/shadowobjs.rs @@ -169,67 +169,67 @@ impl ShadowStack { } /* - The search algorithm to walk the table and return an index + Walk the stack and return an index and object reference if found - - Error here? */ fn locate(&self, addr: Vaddr) -> Result<(&ShadowObject, usize), LookupError> { - if self.tip <= 1 { - return Err(ObjectNotFound) + use LookupError::*; + + if self.data.len() <= 1 { + return Err(ObjectNotFound); // only sentinel ffp; empty } - let mut itr = self.tip - 1; // -1 because tip is the write head - let mut last = itr; + // current frame's trailing ffp + let mut ffp_idx = self.tip - 1; - while itr > 0 { - // get the beginning of the current frame from the cap pointer + loop { + // grab the frame start idx value in trailing ffp let ShadowStackObject::FrameFrontPtr(frame_start_idx) = - self.data.get(itr).expect("Malformed shadow stack head") + self.data.get(ffp_idx).expect("Malformed shadow stack head!") else { panic!("Malformed shadow stack head!") }; - - // get the first object in the current frame - let ShadowStackObject::ShadowObject(first_frame_obj) = - self.data.get(*frame_start_idx).expect("Malformed shadow stack") - else { - panic!("Malformed shadow stack ordering!") + let frame_start_idx = *frame_start_idx; + + // grab the first sobj in frame + let first = match self.data.get(frame_start_idx).expect("Malformed shadow stack") { + ShadowStackObject::ShadowObject(obj) => obj, + ShadowStackObject::FrameFrontPtr(_) => { + // edge case: empty frame should skip to older frame + if frame_start_idx == 0 { + return Err(LookupError::ObjectNotFound); + } + ffp_idx = frame_start_idx - 1; + continue; + } }; - if first_frame_obj.base > addr { - last = itr; - itr = *frame_start_idx - 1; - - // check if we have traversed entire stack - if itr <= 0 { - return Err(ObjectNotFound) + // keep jumping frames until + // first sobj addr < desired addr + if first.base > addr { + if frame_start_idx == 0 { + return Err(ObjectNotFound); // checked everything } - + // previous frames trailing FFP + ffp_idx = frame_start_idx - 1; continue; } - // addr allegedly now exists in our current frame; iterate it - while itr < last { - // get current object + // addr is in (or above) this frame: scan [frame_start_idx, ffp_idx) + let mut scan = frame_start_idx; + while scan < ffp_idx { let ShadowStackObject::ShadowObject(sobj) = - self.data.get(itr).expect("Malformed shadow stack") + self.data.get(scan).expect("Malformed shadow stack!") else { panic!("Malformed shadow stack ordering!") }; - if sobj.contains(addr) { - return Ok((sobj,itr)) + return Ok((sobj, scan)); } - - itr+=1; + scan += 1; } - return Err(ObjectNotFound); } - - // I don't think this is ever reached? - Err(ObjectNotFound) } pub fn invalidate_at(&self, base: Vaddr) { From c9f0b1ad2b818d76749328de5288f319f9297506 Mon Sep 17 00:00:00 2001 From: Ryan Zmuda Date: Thu, 11 Jun 2026 11:41:23 -0400 Subject: [PATCH 04/14] progress (likely unsound) --- .../libresolve/src/shadowobjs.rs | 152 +++++++++++++++--- 1 file changed, 128 insertions(+), 24 deletions(-) diff --git a/resolve-cveassert/libresolve/src/shadowobjs.rs b/resolve-cveassert/libresolve/src/shadowobjs.rs index 7718fb5c..2a1d6bd4 100644 --- a/resolve-cveassert/libresolve/src/shadowobjs.rs +++ b/resolve-cveassert/libresolve/src/shadowobjs.rs @@ -3,7 +3,7 @@ use crate::MutexWrap; use crate::shadowobjs::LookupError::{AddrOOB, ObjectNotFound}; -use crate::shadowobjs::ShadowStackObject::FrameFrontPtr; +use crate::shadowobjs::ShadowStackEntry::FFP; use std::cell::RefCell; use std::collections::BTreeMap; use std::ops::RangeInclusive; @@ -131,14 +131,18 @@ pub static FREED_OBJ_LIST: MutexWrap = MutexWrap::new(ShadowO These FrameFrontPtrs reference the beginning of the pervious frame, letting us backwards traverse in order to search */ -enum ShadowStackObject { +enum ShadowStackValue { ShadowObject(ShadowObject), - FrameFrontPtr(usize) + InvalidatedObject(Vaddr) +} + +enum ShadowStackEntry { + Value(ShadowStackValue), + FFP(usize), } pub struct ShadowStack { - data: Vec, - tip: usize + data: Vec } enum LookupError { @@ -149,8 +153,7 @@ enum LookupError { impl ShadowStack { pub fn new() -> Self { Self { - data: vec![ShadowStackObject::FrameFrontPtr(0)], - tip: 1, + data: vec![ShadowStackEntry::FFP(0)] } } @@ -161,18 +164,51 @@ impl ShadowStack { limit: ShadowObject::limit(base, size), size, }; + + // if addr < tip, we are re-using stack and must replace invalidated sobj + if self.data.len() > 1 { + let ShadowStackEntry::Value(prev_entry) = self.data.get(self.data.len() - 2).unwrap() else { + panic!("Corrupt shadow stack!") + }; + let addr: usize; + match prev_entry { + ShadowStackValue::ShadowObject(sobj) => { + addr = sobj.base; + }, + ShadowStackValue::InvalidatedObject(baddr) => { + addr = *baddr; + } + } + if base < addr { + match self.locate(base) { + Ok((ssobj, idx)) => { + match ssobj { + ShadowStackValue::ShadowObject(_sobj) => { + panic!("Trying to allocate a non-invalidated sobj"); + }, + ShadowStackValue::InvalidatedObject(_iobj) => { + self.data[idx] = ShadowStackEntry::Value(ShadowStackValue::ShadowObject(sobj)); + return; + } + } + }, + Err(_e) => { + panic!("This probably shouldn't happen?"); + } + } + } + } + let ffp = self.data.pop().expect("Missing frame front pointer"); - self.data.push(ShadowStackObject::ShadowObject(sobj)); + self.data.push(ShadowStackEntry::Value(ShadowStackValue::ShadowObject(sobj))); self.data.push(ffp); - - self.tip += 1; } /* Walk the stack and return an index and object reference if found */ - fn locate(&self, addr: Vaddr) -> Result<(&ShadowObject, usize), LookupError> { + fn locate(&self, addr: Vaddr) -> Result<(&ShadowStackValue, usize), LookupError> { use LookupError::*; if self.data.len() <= 1 { @@ -180,11 +216,11 @@ impl ShadowStack { } // current frame's trailing ffp - let mut ffp_idx = self.tip - 1; + let mut ffp_idx = self.data.len() - 1; loop { // grab the frame start idx value in trailing ffp - let ShadowStackObject::FrameFrontPtr(frame_start_idx) = + let ShadowStackEntry::FFP(frame_start_idx) = self.data.get(ffp_idx).expect("Malformed shadow stack head!") else { panic!("Malformed shadow stack head!") @@ -192,9 +228,16 @@ impl ShadowStack { let frame_start_idx = *frame_start_idx; // grab the first sobj in frame - let first = match self.data.get(frame_start_idx).expect("Malformed shadow stack") { - ShadowStackObject::ShadowObject(obj) => obj, - ShadowStackObject::FrameFrontPtr(_) => { + let first_base = match self.data.get(frame_start_idx).expect("Malformed shadow stack") { + ShadowStackEntry::Value( + ShadowStackValue::ShadowObject(obj) + ) => obj.base, + + ShadowStackEntry::Value( + ShadowStackValue::InvalidatedObject(obj) + ) => *obj, + + ShadowStackEntry::FFP(_) => { // edge case: empty frame should skip to older frame if frame_start_idx == 0 { return Err(LookupError::ObjectNotFound); @@ -206,7 +249,7 @@ impl ShadowStack { // keep jumping frames until // first sobj addr < desired addr - if first.base > addr { + if first_base > addr { if frame_start_idx == 0 { return Err(ObjectNotFound); // checked everything } @@ -218,13 +261,23 @@ impl ShadowStack { // addr is in (or above) this frame: scan [frame_start_idx, ffp_idx) let mut scan = frame_start_idx; while scan < ffp_idx { - let ShadowStackObject::ShadowObject(sobj) = + let ShadowStackEntry::Value(ssv) = self.data.get(scan).expect("Malformed shadow stack!") else { panic!("Malformed shadow stack ordering!") }; - if sobj.contains(addr) { - return Ok((sobj, scan)); + + match ssv { + ShadowStackValue::ShadowObject(sobj) => { + if sobj.contains(addr) { + return Ok((ssv, scan)); + } + }, + ShadowStackValue::InvalidatedObject(adr) => { + if *adr == addr { + return Ok((ssv, scan)); + } + } } scan += 1; } @@ -232,8 +285,46 @@ impl ShadowStack { } } - pub fn invalidate_at(&self, base: Vaddr) { - // TODO + /* + 3 cases: + 1. idx(addr) - 1 is an ffp + - we are invalidating an entire frame + 2. addr is last object + - pop last object + 3. addr has object following it + - replace with Invalidated ssv + */ + pub fn invalidate_at(&mut self, base: Vaddr) { + match self.locate(base) { + Ok((_ssv, idx)) => { + // CASE 1 + // if we are at the beginning of the first frame, + // or the beginning of another frame + // TODO: do we need to ensure we are in specifically the latest frame? + if idx == 0 || matches!(self.data.get(idx - 1).expect("Malformed invalidation call"), + ShadowStackEntry::FFP(_)) { + self.data.truncate(idx); + } + + // CASE 2 + // if sobj followed by ffp, replace + // sobj with said ffp + if idx == self.data.len() - 1 { + let ffp = self.data.pop().expect("Malformed shadow stack!"); + self.data.pop(); + self.data.push(ffp); + } + + // CASE 3 + // if sobj has following sobj, + // invalidate in-place + if idx == 0 || matches!(self.data.get(idx + 1).expect("Malformed invalidation call"), + ShadowStackEntry::FFP(_)) { + self.data[idx] = ShadowStackEntry::Value(ShadowStackValue::InvalidatedObject(base)); + } + }, + Err(_e) => {} + } } /* @@ -244,8 +335,21 @@ impl ShadowStack { resolve the shadowobject. */ pub fn search_intersection(&self, addr: Vaddr) -> Option<&ShadowObject> { - - None + match self.locate(addr) { + Ok((sobj, _idx)) => { + match sobj { + ShadowStackValue::ShadowObject(sobj) => { + return Some(sobj); + }, + ShadowStackValue::InvalidatedObject(_iobj) => { + return None; + } + } + }, + Err(e) => { + return None; + } + } } } From 0241accb217cb240cebbba6f22dd4e0e2feaa531 Mon Sep 17 00:00:00 2001 From: Ryan Zmuda Date: Thu, 11 Jun 2026 12:15:01 -0400 Subject: [PATCH 05/14] fix logic errors --- resolve-cveassert/libresolve/src/remediate.rs | 10 ++ .../libresolve/src/shadowobjs.rs | 134 +++++++----------- 2 files changed, 58 insertions(+), 86 deletions(-) diff --git a/resolve-cveassert/libresolve/src/remediate.rs b/resolve-cveassert/libresolve/src/remediate.rs index 68d04ff1..dbd7f8d7 100644 --- a/resolve-cveassert/libresolve/src/remediate.rs +++ b/resolve-cveassert/libresolve/src/remediate.rs @@ -37,6 +37,16 @@ pub extern "C" fn __resolve_invalidate_stack(base: *mut c_void) { info!("[STACK] Free addr 0x{base:x}"); } +#[unsafe(no_mangle)] +pub extern "C" fn __resolve_push_frame() { + SHADOW_STACK.with_borrow_mut( + |ss| + ss.push_frame() + ); + + info!("[STACK] Pushed new frame."); +} + /** * @brief - Allocator logging interface for malloc * @input - size of the allocation in bytes diff --git a/resolve-cveassert/libresolve/src/shadowobjs.rs b/resolve-cveassert/libresolve/src/shadowobjs.rs index 2a1d6bd4..2c0fc7ac 100644 --- a/resolve-cveassert/libresolve/src/shadowobjs.rs +++ b/resolve-cveassert/libresolve/src/shadowobjs.rs @@ -128,8 +128,8 @@ pub static FREED_OBJ_LIST: MutexWrap = MutexWrap::new(ShadowO We grow by appending ShadowObjects to the end of the Vector, and inserting new FrameFrontPtrs when we push new frames. - These FrameFrontPtrs reference the beginning of the pervious - frame, letting us backwards traverse in order to search + These FrameFrontPtrs reference the beginning of their + frame, letting us backwards traverse in order to search efficiently */ enum ShadowStackValue { ShadowObject(ShadowObject), @@ -167,39 +167,30 @@ impl ShadowStack { // if addr < tip, we are re-using stack and must replace invalidated sobj if self.data.len() > 1 { - let ShadowStackEntry::Value(prev_entry) = self.data.get(self.data.len() - 2).unwrap() else { - panic!("Corrupt shadow stack!") - }; - let addr: usize; - match prev_entry { - ShadowStackValue::ShadowObject(sobj) => { - addr = sobj.base; - }, - ShadowStackValue::InvalidatedObject(baddr) => { - addr = *baddr; - } - } - if base < addr { - match self.locate(base) { - Ok((ssobj, idx)) => { - match ssobj { - ShadowStackValue::ShadowObject(_sobj) => { - panic!("Trying to allocate a non-invalidated sobj"); - }, - ShadowStackValue::InvalidatedObject(_iobj) => { - self.data[idx] = ShadowStackEntry::Value(ShadowStackValue::ShadowObject(sobj)); - return; - } + if let ShadowStackEntry::Value(prev_entry) = &self.data[self.data.len() - 2] { + let prev_base = match prev_entry { + ShadowStackValue::ShadowObject(o) => o.base, + ShadowStackValue::InvalidatedObject(b) => *b, + }; + if base <= prev_base { + match self.locate(base) { + Ok((ShadowStackValue::InvalidatedObject(_), idx)) => { + self.data[idx] = + ShadowStackEntry::Value(ShadowStackValue::ShadowObject(sobj)); + return; + } + Ok((ShadowStackValue::ShadowObject(_), _)) => { + panic!("reallocating a live shadow object"); + } + Err(_) => { + panic!("reuse: base below frame head but no invalidated slot found"); } - }, - Err(_e) => { - panic!("This probably shouldn't happen?"); } } } } - let ffp = self.data.pop().expect("Missing frame front pointer"); + let ffp = self.data.pop().expect("missing frame front pointer"); self.data.push(ShadowStackEntry::Value(ShadowStackValue::ShadowObject(sobj))); self.data.push(ffp); } @@ -285,72 +276,43 @@ impl ShadowStack { } } - /* - 3 cases: - 1. idx(addr) - 1 is an ffp - - we are invalidating an entire frame - 2. addr is last object - - pop last object - 3. addr has object following it - - replace with Invalidated ssv - */ pub fn invalidate_at(&mut self, base: Vaddr) { - match self.locate(base) { - Ok((_ssv, idx)) => { - // CASE 1 - // if we are at the beginning of the first frame, - // or the beginning of another frame - // TODO: do we need to ensure we are in specifically the latest frame? - if idx == 0 || matches!(self.data.get(idx - 1).expect("Malformed invalidation call"), - ShadowStackEntry::FFP(_)) { - self.data.truncate(idx); - } - - // CASE 2 - // if sobj followed by ffp, replace - // sobj with said ffp - if idx == self.data.len() - 1 { - let ffp = self.data.pop().expect("Malformed shadow stack!"); - self.data.pop(); - self.data.push(ffp); - } + let Ok((_, idx)) = self.locate(base) else { + debug_assert!(false, "invalidate_at: untracked addr"); + return; + }; - // CASE 3 - // if sobj has following sobj, - // invalidate in-place - if idx == 0 || matches!(self.data.get(idx + 1).expect("Malformed invalidation call"), - ShadowStackEntry::FFP(_)) { - self.data[idx] = ShadowStackEntry::Value(ShadowStackValue::InvalidatedObject(base)); - } - }, - Err(_e) => {} + // Case: frame start, tear down the frame and everything newer. + if idx == 0 || matches!(self.data[idx - 1], ShadowStackEntry::FFP(_)) { + self.data.truncate(idx); + if self.data.is_empty() { + self.data.push(ShadowStackEntry::FFP(0)); + } + return; } + + // Case: last object on stack + if idx == self.data.len() - 2 { + let ffp = self.data.pop().expect("missing trailing FFP"); + self.data.pop(); + self.data.push(ffp); + return; + } + + // Invalidating mid-frame sobj + self.data[idx] = ShadowStackEntry::Value(ShadowStackValue::InvalidatedObject(base)); } - /* - Perform a backwards search from tip by jumping - to current frame start and comparing Vaddr. If - Vaddr < FrameVaddr we jump back until the condition - is met, then incrementally search the found frame to - resolve the shadowobject. - */ pub fn search_intersection(&self, addr: Vaddr) -> Option<&ShadowObject> { match self.locate(addr) { - Ok((sobj, _idx)) => { - match sobj { - ShadowStackValue::ShadowObject(sobj) => { - return Some(sobj); - }, - ShadowStackValue::InvalidatedObject(_iobj) => { - return None; - } - } - }, - Err(e) => { - return None; - } + Ok((ShadowStackValue::ShadowObject(sobj), _)) => Some(sobj), + _ => None, } } + + pub fn push_frame(&mut self) { + self.data.push(ShadowStackEntry::FFP(self.data.len())); + } } thread_local! { From cdca70a75c527014e770b3ef2d6f524e3c27c6a4 Mon Sep 17 00:00:00 2001 From: Ryan Zmuda Date: Thu, 11 Jun 2026 13:10:51 -0400 Subject: [PATCH 06/14] remove dead code --- resolve-cveassert/libresolve/src/shadowobjs.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/resolve-cveassert/libresolve/src/shadowobjs.rs b/resolve-cveassert/libresolve/src/shadowobjs.rs index 2c0fc7ac..a8e7c382 100644 --- a/resolve-cveassert/libresolve/src/shadowobjs.rs +++ b/resolve-cveassert/libresolve/src/shadowobjs.rs @@ -2,8 +2,6 @@ // LGPL-3; See LICENSE.txt in the repo root for details. use crate::MutexWrap; -use crate::shadowobjs::LookupError::{AddrOOB, ObjectNotFound}; -use crate::shadowobjs::ShadowStackEntry::FFP; use std::cell::RefCell; use std::collections::BTreeMap; use std::ops::RangeInclusive; @@ -146,7 +144,6 @@ pub struct ShadowStack { } enum LookupError { - AddrOOB, ObjectNotFound, } From 1de4afa21c58d2b54e2c1d050d4f0c14cf38af29 Mon Sep 17 00:00:00 2001 From: Ryan Zmuda Date: Fri, 12 Jun 2026 08:35:57 -0400 Subject: [PATCH 07/14] fix: address review comments --- resolve-cveassert/libresolve/src/remediate.rs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/resolve-cveassert/libresolve/src/remediate.rs b/resolve-cveassert/libresolve/src/remediate.rs index dbd7f8d7..6506edbd 100644 --- a/resolve-cveassert/libresolve/src/remediate.rs +++ b/resolve-cveassert/libresolve/src/remediate.rs @@ -262,6 +262,18 @@ pub struct ShadowObjBounds { pub limit: *mut c_void, } +impl ShadowObjBounds { + pub fn null() -> Self { + ShadowObjBounds { base: std::ptr::null_mut(), limit: std::ptr::null_mut() } + } +} + +impl From<&crate::shadowobjs::ShadowObject> for ShadowObjBounds { + fn from(sobj: &crate::shadowobjs::ShadowObject) -> Self { + ShadowObjBounds { base: sobj.base as *mut c_void, limit: sobj.limit as *mut c_void } + } +} + /** * @brief - Helper function that queries shadow obj list * to find a shadow obj where the ptr fits within @@ -273,18 +285,14 @@ pub struct ShadowObjBounds { */ #[unsafe(no_mangle)] pub extern "C" fn __resolve_get_bounds_stack(ptr: *mut c_void) -> ShadowObjBounds { - SHADOW_STACK.with_borrow_mut( + return SHADOW_STACK.with_borrow_mut( |ss| { match ss.search_intersection(ptr as Vaddr) { - Some(sobj) => { return ShadowObjBounds { base: sobj.base as *mut c_void, limit: sobj.limit as *mut c_void }; } - None => { return ShadowObjBounds { base: std::ptr::null_mut(), limit: std::ptr::null_mut() }; } + Some(sobj) => { return sobj.into() } + None => { return ShadowObjBounds::null(); } } } ); - - // This would only be reached if .with_borrow_mut failed, right? - // TODO: what to do in this case? - return ShadowObjBounds { base: std::ptr::null_mut(), limit: std::ptr::null_mut() }; } /** @@ -300,10 +308,10 @@ pub extern "C" fn __resolve_get_bounds_stack(ptr: *mut c_void) -> ShadowObjBound pub extern "C" fn __resolve_get_bounds(ptr: *mut c_void) -> ShadowObjBounds { let sobj_table = ALIVE_OBJ_LIST.lock(); let Some(sobj) = sobj_table.search_intersection(ptr as Vaddr) else { - return ShadowObjBounds { base: std::ptr::null_mut(), limit: std::ptr::null_mut() } + return ShadowObjBounds::null(); }; - return ShadowObjBounds { base: sobj.base as *mut c_void, limit: sobj.limit as *mut c_void } + return sobj.into(); } #[unsafe(no_mangle)] From 6d66df7b15f5629e2a78d5da31e4ec9b3e8d4b94 Mon Sep 17 00:00:00 2001 From: Ryan Zmuda Date: Fri, 12 Jun 2026 14:31:21 -0400 Subject: [PATCH 08/14] wip impl backup --- resolve-cveassert/libresolve/src/remediate.rs | 23 +-- .../libresolve/src/shadowobjs.rs | 142 +++++++----------- 2 files changed, 61 insertions(+), 104 deletions(-) diff --git a/resolve-cveassert/libresolve/src/remediate.rs b/resolve-cveassert/libresolve/src/remediate.rs index 6506edbd..cc28afcf 100644 --- a/resolve-cveassert/libresolve/src/remediate.rs +++ b/resolve-cveassert/libresolve/src/remediate.rs @@ -26,25 +26,16 @@ pub extern "C" fn __resolve_alloca(ptr: *mut c_void, size: usize) -> () { } #[unsafe(no_mangle)] -pub extern "C" fn __resolve_invalidate_stack(base: *mut c_void) { +pub extern "C" fn __resolve_invalidate_stack_range(base: *mut c_void, size: usize) { let base = base as Vaddr; - SHADOW_STACK.with_borrow_mut( - |ss| - ss.invalidate_at(base) - ); - - info!("[STACK] Free addr 0x{base:x}"); -} - -#[unsafe(no_mangle)] -pub extern "C" fn __resolve_push_frame() { - SHADOW_STACK.with_borrow_mut( - |ss| - ss.push_frame() - ); + // TODO: COMPLETE ME + // SHADOW_STACK.with_borrow_mut( + // |ss| + // ss.invalidate_at(base) + // ); - info!("[STACK] Pushed new frame."); + // info!("[STACK] Free addr 0x{base:x}"); } /** diff --git a/resolve-cveassert/libresolve/src/shadowobjs.rs b/resolve-cveassert/libresolve/src/shadowobjs.rs index a8e7c382..2d038fa4 100644 --- a/resolve-cveassert/libresolve/src/shadowobjs.rs +++ b/resolve-cveassert/libresolve/src/shadowobjs.rs @@ -118,29 +118,40 @@ impl ShadowObjectTable { pub static ALIVE_OBJ_LIST: MutexWrap = MutexWrap::new(ShadowObjectTable::new()); pub static FREED_OBJ_LIST: MutexWrap = MutexWrap::new(ShadowObjectTable::new()); -/* - Stack shadow object shape: - [frame 0] [ frame 1 ] - [ | | | ][*][ | | | | | ][*] -> (growth direction) - <---------| <-------------| - - We grow by appending ShadowObjects to the end of the Vector, - and inserting new FrameFrontPtrs when we push new frames. - These FrameFrontPtrs reference the beginning of their - frame, letting us backwards traverse in order to search efficiently -*/ enum ShadowStackValue { ShadowObject(ShadowObject), - InvalidatedObject(Vaddr) + InvalidatedObject(Vaddr,usize) } -enum ShadowStackEntry { - Value(ShadowStackValue), - FFP(usize), +impl ShadowStackValue { + fn base(&self) -> Vaddr { + match self { + ShadowStackValue::ShadowObject(o) => o.base, + ShadowStackValue::InvalidatedObject(b, _s) => *b, + } + } + + fn size(&self) -> usize { + match self { + ShadowStackValue::ShadowObject(o) => o.size, + ShadowStackValue::InvalidatedObject(_b, s) => *s, + } + } + + fn contains(&self, addr: Vaddr) -> bool { + let base = self.base(); + + let Some(end) = base.checked_add(self.size() as Vaddr) else { + return false; + }; + + base <= addr && addr < end + } } +#[derive(Default)] pub struct ShadowStack { - data: Vec + data: Vec } enum LookupError { @@ -149,11 +160,15 @@ enum LookupError { impl ShadowStack { pub fn new() -> Self { - Self { - data: vec![ShadowStackEntry::FFP(0)] - } + Self::default() } + /* + Adds a new shadow object to the vector. + + If this object is not at the end of the vector, and is not overwriting an invalid range, + we can drop the frame(s) following it. + */ pub fn add_shadow_object(&mut self, base: Vaddr, size: usize) { let sobj = ShadowObject { alloc_type: AllocType::Stack, @@ -192,84 +207,35 @@ impl ShadowStack { self.data.push(ffp); } - /* - Walk the stack and return an index - and object reference if found - */ - fn locate(&self, addr: Vaddr) -> Result<(&ShadowStackValue, usize), LookupError> { + fn get_at(&self, addr: Vaddr) -> Result<(ShadowStackValue, usize), LookupError> { use LookupError::*; - if self.data.len() <= 1 { - return Err(ObjectNotFound); // only sentinel ffp; empty - } - - // current frame's trailing ffp - let mut ffp_idx = self.data.len() - 1; - + if self.data.len() == 0 { return Err(ObjectNotFound); } + + let mut idx = self.data.len() - 1; + let mut gallop_factor = 1; loop { - // grab the frame start idx value in trailing ffp - let ShadowStackEntry::FFP(frame_start_idx) = - self.data.get(ffp_idx).expect("Malformed shadow stack head!") - else { - panic!("Malformed shadow stack head!") - }; - let frame_start_idx = *frame_start_idx; - - // grab the first sobj in frame - let first_base = match self.data.get(frame_start_idx).expect("Malformed shadow stack") { - ShadowStackEntry::Value( - ShadowStackValue::ShadowObject(obj) - ) => obj.base, - - ShadowStackEntry::Value( - ShadowStackValue::InvalidatedObject(obj) - ) => *obj, - - ShadowStackEntry::FFP(_) => { - // edge case: empty frame should skip to older frame - if frame_start_idx == 0 { - return Err(LookupError::ObjectNotFound); - } - ffp_idx = frame_start_idx - 1; - continue; - } - }; + let sobj = self.data.get(idx).expect("Malformed shadow stack state!"); - // keep jumping frames until - // first sobj addr < desired addr - if first_base > addr { - if frame_start_idx == 0 { - return Err(ObjectNotFound); // checked everything - } - // previous frames trailing FFP - ffp_idx = frame_start_idx - 1; - continue; + let (base, size) = (sobj.base(), sobj.size()); + + // if in this bucket: + if addr > base && addr < base + size { + // BINARY SEARCH THIS BUCKET } - // addr is in (or above) this frame: scan [frame_start_idx, ffp_idx) - let mut scan = frame_start_idx; - while scan < ffp_idx { - let ShadowStackEntry::Value(ssv) = - self.data.get(scan).expect("Malformed shadow stack!") - else { - panic!("Malformed shadow stack ordering!") - }; + // else: giddyup! + gallop_factor *= 2; + idx = idx.saturating_sub(gallop_factor); - match ssv { - ShadowStackValue::ShadowObject(sobj) => { - if sobj.contains(addr) { - return Ok((ssv, scan)); - } - }, - ShadowStackValue::InvalidatedObject(adr) => { - if *adr == addr { - return Ok((ssv, scan)); - } - } + // hit end: check if addr lies outside last bucket + if idx == 0 { + if self.data.get(idx).expect("Malformed shadow stack state!").base() > addr { + // bottom item in stack is somehow still out of range + return Err(ObjectNotFound); } - scan += 1; + // else: fall through to next iteration which binary searches this bucket } - return Err(ObjectNotFound); } } From f187dd833d21ac95c3e829e8774f907021263b0a Mon Sep 17 00:00:00 2001 From: Ryan Zmuda Date: Fri, 12 Jun 2026 15:15:33 -0400 Subject: [PATCH 09/14] progress (likely broken) --- .../libresolve/src/shadowobjs.rs | 176 ++++++++++-------- 1 file changed, 99 insertions(+), 77 deletions(-) diff --git a/resolve-cveassert/libresolve/src/shadowobjs.rs b/resolve-cveassert/libresolve/src/shadowobjs.rs index 2d038fa4..01ea408b 100644 --- a/resolve-cveassert/libresolve/src/shadowobjs.rs +++ b/resolve-cveassert/libresolve/src/shadowobjs.rs @@ -163,119 +163,141 @@ impl ShadowStack { Self::default() } - /* - Adds a new shadow object to the vector. - - If this object is not at the end of the vector, and is not overwriting an invalid range, - we can drop the frame(s) following it. - */ pub fn add_shadow_object(&mut self, base: Vaddr, size: usize) { - let sobj = ShadowObject { + // check if we are overwriting dead obj span + let (idx, og_size) = match self.get_at(base) { + Ok((og_sobj, i)) => (i, og_sobj.size()), + Err(_) => { + debug_assert!(false, "ShadowStack::add_shadow_object: untracked addr"); + return; + } + }; + + self.data[idx] = ShadowStackValue::ShadowObject(ShadowObject { alloc_type: AllocType::Stack, base, limit: ShadowObject::limit(base, size), size, - }; + }); + + // retain the remaining dead space + // if the new object doesn't fill the full extent + if og_size > size { + self.data.insert(idx + 1, ShadowStackValue::InvalidatedObject(base + size, og_size - size)); + } + // If we didn't write through an invalidated object, we can drop subsequent frames + else { + self.data.truncate(idx + 1); + } + + } + + fn binary_search_window(&self, addr: Vaddr, mut lo: usize, mut hi: usize) -> Result<(&ShadowStackValue, usize), LookupError> { + use LookupError::*; - // if addr < tip, we are re-using stack and must replace invalidated sobj - if self.data.len() > 1 { - if let ShadowStackEntry::Value(prev_entry) = &self.data[self.data.len() - 2] { - let prev_base = match prev_entry { - ShadowStackValue::ShadowObject(o) => o.base, - ShadowStackValue::InvalidatedObject(b) => *b, - }; - if base <= prev_base { - match self.locate(base) { - Ok((ShadowStackValue::InvalidatedObject(_), idx)) => { - self.data[idx] = - ShadowStackEntry::Value(ShadowStackValue::ShadowObject(sobj)); - return; - } - Ok((ShadowStackValue::ShadowObject(_), _)) => { - panic!("reallocating a live shadow object"); - } - Err(_) => { - panic!("reuse: base below frame head but no invalidated slot found"); - } - } - } + // Find the last object in [lo, hi) whose base <= addr. + while lo + 1 < hi { + let mid = lo + (hi - lo) / 2; + + if self.data[mid].base() <= addr { + lo = mid; + } else { + hi = mid; } } - let ffp = self.data.pop().expect("missing frame front pointer"); - self.data.push(ShadowStackEntry::Value(ShadowStackValue::ShadowObject(sobj))); - self.data.push(ffp); + let obj = &self.data[lo]; + + if obj.contains(addr) { + Ok((obj, lo)) + } else { + Err(ObjectNotFound) + } } - fn get_at(&self, addr: Vaddr) -> Result<(ShadowStackValue, usize), LookupError> { + fn get_at(&self, addr: Vaddr) -> Result<(&ShadowStackValue, usize), LookupError> { use LookupError::*; - if self.data.len() == 0 { return Err(ObjectNotFound); } - - let mut idx = self.data.len() - 1; - let mut gallop_factor = 1; - loop { - let sobj = self.data.get(idx).expect("Malformed shadow stack state!"); + let n = self.data.len(); + if n == 0 { return Err(ObjectNotFound); } - let (base, size) = (sobj.base(), sobj.size()); + let last_idx = n - 1; + let last = &self.data[last_idx]; + if last.contains(addr) { + return Ok((last, last_idx)); + } - // if in this bucket: - if addr > base && addr < base + size { - // BINARY SEARCH THIS BUCKET + // If addr is above the last object's base but not inside it, + // then it cannot be in any earlier object, assuming sorted non-overlapping extents. + if last.base() <= addr { + return Err(ObjectNotFound); + } + + // Gallop backwards until we find some object with base <= addr. + let mut hi = last_idx; // upper bound + let mut step = 1; + + loop { + let lo = hi.saturating_sub(step); + + // We found a bucket that should contain the address: + if self.data[lo].base() <= addr { + // must be in [lo, hi). + return self.binary_search_window(addr, lo, hi); } - // else: giddyup! - gallop_factor *= 2; - idx = idx.saturating_sub(gallop_factor); - - // hit end: check if addr lies outside last bucket - if idx == 0 { - if self.data.get(idx).expect("Malformed shadow stack state!").base() > addr { - // bottom item in stack is somehow still out of range - return Err(ObjectNotFound); - } - // else: fall through to next iteration which binary searches this bucket + if lo == 0 { + // Reached the end without finding the bucket + return Err(ObjectNotFound); } + + // Giddyup + hi = lo; + step = step.saturating_mul(2); } } - pub fn invalidate_at(&mut self, base: Vaddr) { - let Ok((_, idx)) = self.locate(base) else { + /* + Invalidate a range of shadowstack. + + If that range spans to the end of the stack, we drop all the frames. + Else, we are prepping to re-using a piece of stack + */ + pub fn invalidate_at(&mut self, base: Vaddr, length: usize) { + let Ok((_, start_idx )) = self.get_at(base) else { debug_assert!(false, "invalidate_at: untracked addr"); return; }; - // Case: frame start, tear down the frame and everything newer. - if idx == 0 || matches!(self.data[idx - 1], ShadowStackEntry::FFP(_)) { - self.data.truncate(idx); - if self.data.is_empty() { - self.data.push(ShadowStackEntry::FFP(0)); - } + let Ok((_, end_idx )) = self.get_at(base + length) else { + debug_assert!(false, "invalidate_at: untracked addr"); return; - } + }; - // Case: last object on stack - if idx == self.data.len() - 2 { - let ffp = self.data.pop().expect("missing trailing FFP"); - self.data.pop(); - self.data.push(ffp); - return; + // Dropping >=1 entire frame(s) + // TODO: does this need to be -1? + if end_idx == self.data.len() { + self.data.truncate(end_idx - start_idx); } - // Invalidating mid-frame sobj - self.data[idx] = ShadowStackEntry::Value(ShadowStackValue::InvalidatedObject(base)); + // Invalidating objects within range + // TODO: do we need to add comprehensive checks in places to make sure sobj lengths don't overlap, + // or is that a latent property of stack objects? What about in cases of re-use? + self.data[start_idx] = ShadowStackValue::InvalidatedObject(base, length); + + if end_idx > start_idx { + self.data.drain(start_idx+1..end_idx); // removes each affected obj at once (faster) + } } + // TODO: Does it make more sense to return something explicit + // when we search and get an invalidated stack object? pub fn search_intersection(&self, addr: Vaddr) -> Option<&ShadowObject> { - match self.locate(addr) { + match self.get_at(addr) { Ok((ShadowStackValue::ShadowObject(sobj), _)) => Some(sobj), _ => None, } } - - pub fn push_frame(&mut self) { - self.data.push(ShadowStackEntry::FFP(self.data.len())); - } } thread_local! { From 394b27550a1d5f1024cea8951ca2ab990be91bda Mon Sep 17 00:00:00 2001 From: Ryan Zmuda Date: Mon, 15 Jun 2026 11:27:51 -0400 Subject: [PATCH 10/14] fix: stack grows down on x86 --- resolve-cveassert/libresolve/src/remediate.rs | 11 +- .../libresolve/src/shadowobjs.rs | 172 +++++++++++------- 2 files changed, 107 insertions(+), 76 deletions(-) diff --git a/resolve-cveassert/libresolve/src/remediate.rs b/resolve-cveassert/libresolve/src/remediate.rs index cc28afcf..5662c53b 100644 --- a/resolve-cveassert/libresolve/src/remediate.rs +++ b/resolve-cveassert/libresolve/src/remediate.rs @@ -29,13 +29,12 @@ pub extern "C" fn __resolve_alloca(ptr: *mut c_void, size: usize) -> () { pub extern "C" fn __resolve_invalidate_stack_range(base: *mut c_void, size: usize) { let base = base as Vaddr; - // TODO: COMPLETE ME - // SHADOW_STACK.with_borrow_mut( - // |ss| - // ss.invalidate_at(base) - // ); + SHADOW_STACK.with_borrow_mut( + |ss| + ss.invalidate_at(base, size) + ); - // info!("[STACK] Free addr 0x{base:x}"); + info!("[STACK] Free addr 0x{base:x} size {size}"); } /** diff --git a/resolve-cveassert/libresolve/src/shadowobjs.rs b/resolve-cveassert/libresolve/src/shadowobjs.rs index 01ea408b..656be860 100644 --- a/resolve-cveassert/libresolve/src/shadowobjs.rs +++ b/resolve-cveassert/libresolve/src/shadowobjs.rs @@ -149,6 +149,8 @@ impl ShadowStackValue { } } +// data must be ordered descending (downward growing stack on x86) +// so push/pop are O(1) at the end. #[derive(Default)] pub struct ShadowStack { data: Vec @@ -183,7 +185,7 @@ impl ShadowStack { // retain the remaining dead space // if the new object doesn't fill the full extent if og_size > size { - self.data.insert(idx + 1, ShadowStackValue::InvalidatedObject(base + size, og_size - size)); + self.data.insert(idx, ShadowStackValue::InvalidatedObject(base + size, og_size - size)); } // If we didn't write through an invalidated object, we can drop subsequent frames else { @@ -192,68 +194,31 @@ impl ShadowStack { } - fn binary_search_window(&self, addr: Vaddr, mut lo: usize, mut hi: usize) -> Result<(&ShadowStackValue, usize), LookupError> { - use LookupError::*; - - // Find the last object in [lo, hi) whose base <= addr. - while lo + 1 < hi { - let mid = lo + (hi - lo) / 2; - - if self.data[mid].base() <= addr { - lo = mid; - } else { - hi = mid; - } - } - - let obj = &self.data[lo]; - - if obj.contains(addr) { - Ok((obj, lo)) - } else { - Err(ObjectNotFound) - } - } - fn get_at(&self, addr: Vaddr) -> Result<(&ShadowStackValue, usize), LookupError> { use LookupError::*; let n = self.data.len(); if n == 0 { return Err(ObjectNotFound); } - let last_idx = n - 1; - let last = &self.data[last_idx]; - if last.contains(addr) { - return Ok((last, last_idx)); + // fast path: the top frame (should be) the most common lookup target. + let top_idx = n - 1; + let top = &self.data[top_idx]; + if top.contains(addr) { + return Ok((top, top_idx)); } - // If addr is above the last object's base but not inside it, - // then it cannot be in any earlier object, assuming sorted non-overlapping extents. - if last.base() <= addr { - return Err(ObjectNotFound); + // easy out: below every tracked object + if addr < top.base() { + return Err(ObjectNotFound); // should we return a seperate ObjectOutOfBounds? } - // Gallop backwards until we find some object with base <= addr. - let mut hi = last_idx; // upper bound - let mut step = 1; - - loop { - let lo = hi.saturating_sub(step); - - // We found a bucket that should contain the address: - if self.data[lo].base() <= addr { - // must be in [lo, hi). - return self.binary_search_window(addr, lo, hi); - } - - if lo == 0 { - // Reached the end without finding the bucket - return Err(ObjectNotFound); - } - - // Giddyup - hi = lo; - step = step.saturating_mul(2); + // binary search the shadow stack for value + let idx = self.data.partition_point(|o| o.base() > addr); + let obj = &self.data[idx]; + if obj.contains(addr) { + Ok((obj, idx)) + } else { + Err(ObjectNotFound) } } @@ -264,30 +229,27 @@ impl ShadowStack { Else, we are prepping to re-using a piece of stack */ pub fn invalidate_at(&mut self, base: Vaddr, length: usize) { - let Ok((_, start_idx )) = self.get_at(base) else { - debug_assert!(false, "invalidate_at: untracked addr"); + if length == 0 { return; } + + let Ok((_, start_idx)) = self.get_at(base) else { + debug_assert!(false, "invalidate_at: untracked base"); return; }; - - let Ok((_, end_idx )) = self.get_at(base + length) else { - debug_assert!(false, "invalidate_at: untracked addr"); + let Ok((_, end_idx)) = self.get_at(base + length - 1) else { + debug_assert!(false, "invalidate_at: untracked limit"); return; }; + debug_assert!(end_idx <= start_idx); - // Dropping >=1 entire frame(s) - // TODO: does this need to be -1? - if end_idx == self.data.len() { - self.data.truncate(end_idx - start_idx); + // pop all frames if range reaches the top of stack + if start_idx == self.data.len() - 1 { + self.data.truncate(end_idx); + return; } - // Invalidating objects within range - // TODO: do we need to add comprehensive checks in places to make sure sobj lengths don't overlap, - // or is that a latent property of stack objects? What about in cases of re-use? - self.data[start_idx] = ShadowStackValue::InvalidatedObject(base, length); - - if end_idx > start_idx { - self.data.drain(start_idx+1..end_idx); // removes each affected obj at once (faster) - } + // collapse the covered frames into one dead marker. + self.data.drain(end_idx..start_idx); + self.data[end_idx] = ShadowStackValue::InvalidatedObject(base, length); } // TODO: Does it make more sense to return something explicit @@ -371,4 +333,74 @@ mod tests { // let table = ShadowObjectTable::new(); //table.bounds(0xDEADBEEF).unwrap(); // should panic since there is no interesection } + + use super::{ShadowObject, ShadowStack, ShadowStackValue, Vaddr}; + + fn stack_obj(base: Vaddr, size: usize) -> ShadowStackValue { + ShadowStackValue::ShadowObject(ShadowObject { + alloc_type: AllocType::Stack, + base, + limit: ShadowObject::limit(base, size), + size, + }) + } + + // NOTE: Generated by Opus 4.8 + // + // Tests to ensure the stack grows descending and + // properly resolves common lookups + #[test] + fn shadowstack_descending_lookup() { + // Frames as the stack grows down: each new frame has a lower base, so + // the vec is descending by base (top of stack = last element). + let mut ss = ShadowStack::new(); + ss.data.push(stack_obj(0x3000, 0x100)); + ss.data.push(stack_obj(0x2000, 0x100)); + ss.data.push(stack_obj(0x1000, 0x100)); // top + + // Non-top frames must be found + assert_eq!(ss.search_intersection(0x3050).map(|o| o.base), Some(0x3000)); + assert_eq!(ss.search_intersection(0x2050).map(|o| o.base), Some(0x2000)); + assert_eq!(ss.search_intersection(0x1050).map(|o| o.base), Some(0x1000)); + + // Boundaries: base inclusive, base + size exclusive. + assert_eq!(ss.search_intersection(0x3000).map(|o| o.base), Some(0x3000)); + assert!(ss.search_intersection(0x3100).is_none()); + + // Below the top frame's base => below everything; gaps are misses. + assert!(ss.search_intersection(0x0fff).is_none()); + assert!(ss.search_intersection(0x2500).is_none()); + } + + // NOTE: Generated by Opus 4.8 + // + // Tests stack re-use where we tombstone invalidate a region + // of shadow stack and then re-allocate a sobj inside of it + // that may or may not fill that region entirely + #[test] + fn shadowstack_reuse() { + // Exact reuse: a dead region reused by an equally + // sized object leaves no dead space behind. + let mut ss = ShadowStack::new(); + ss.data.push(ShadowStackValue::InvalidatedObject(0x1000, 0x100)); + assert!(ss.search_intersection(0x1050).is_none()); // dead before reuse + + ss.add_shadow_object(0x1000, 0x100); + assert_eq!(ss.search_intersection(0x1050).map(|o| o.base), Some(0x1000)); // reused, live + assert_eq!(ss.data.len(), 1); // no leftover dead space + + // Partial reuse: a smaller object reuses the base and leaves the + // higher-base remainder dead. + let mut ss = ShadowStack::new(); + ss.data.push(ShadowStackValue::InvalidatedObject(0x1000, 0x400)); + assert!(ss.search_intersection(0x1050).is_none()); // dead before reuse + + ss.add_shadow_object(0x1000, 0x100); + assert_eq!(ss.search_intersection(0x1050).map(|o| o.base), Some(0x1000)); // reused, live + assert!(ss.search_intersection(0x1200).is_none()); // remainder still dead + + // Descending invariant: dead remainder (higher base) precedes the live object. + let bases: Vec<_> = ss.data.iter().map(|o| o.base()).collect(); + assert_eq!(bases, vec![0x1100, 0x1000]); + } } From 069f36b8a8f438037f6a385d0c5d206b08ee29a0 Mon Sep 17 00:00:00 2001 From: Ryan Zmuda Date: Mon, 15 Jun 2026 13:47:58 -0400 Subject: [PATCH 11/14] ensure proper behavior of shadowstack --- .../libresolve/src/shadowobjs.rs | 177 ++++++++---------- 1 file changed, 73 insertions(+), 104 deletions(-) diff --git a/resolve-cveassert/libresolve/src/shadowobjs.rs b/resolve-cveassert/libresolve/src/shadowobjs.rs index 656be860..5bcff773 100644 --- a/resolve-cveassert/libresolve/src/shadowobjs.rs +++ b/resolve-cveassert/libresolve/src/shadowobjs.rs @@ -33,35 +33,24 @@ pub struct ShadowObject { } impl ShadowObject { - /// Returns the base + limit of this shadow object as RangeInclusive - /// - /// Useful for querying contains pub fn bounds(&self) -> RangeInclusive { self.base..=self.limit } - /// Test if `addr` is within the bounds of this shadow object pub fn contains(&self, addr: Vaddr) -> bool { self.bounds().contains(&addr) } - // pub fn contains_region(&self, base: Vaddr, limit: Vaddr) -> bool { - // self.contains(base) && self.contains(limit) - // } - - /// Computes the size of the shadow object from its base and limit pub fn size(&self) -> usize { self.size } - /// Compute a limit from base and size pub fn limit(base: Vaddr, size: usize) -> Vaddr { - if size == 0 { base } else { base + size - 1 } + if size == 0 { base } else { base.saturating_add(size - 1) } } - /// Compute the sentinel pointer value for this object, 1 past its limit pub fn past_limit(&self) -> Vaddr { - self.limit + 1 + self.limit.saturating_add(1) } } @@ -166,32 +155,50 @@ impl ShadowStack { } pub fn add_shadow_object(&mut self, base: Vaddr, size: usize) { - // check if we are overwriting dead obj span - let (idx, og_size) = match self.get_at(base) { - Ok((og_sobj, i)) => (i, og_sobj.size()), - Err(_) => { - debug_assert!(false, "ShadowStack::add_shadow_object: untracked addr"); - return; - } - }; - - self.data[idx] = ShadowStackValue::ShadowObject(ShadowObject { + let new_end = base + size; // exclusive + let make = || ShadowObject { alloc_type: AllocType::Stack, base, limit: ShadowObject::limit(base, size), size, - }); + }; - // retain the remaining dead space - // if the new object doesn't fill the full extent - if og_size > size { - self.data.insert(idx, ShadowStackValue::InvalidatedObject(base + size, og_size - size)); + let Ok((reused, idx)) = self.get_at(base) else { + // most common: pushing a new obj onto the end of the stack + assert!(self.data.last().map_or(true, |top| new_end <= top.base()), + "ShadowStack::add_shadow_object: new object overlaps the stack top"); + self.data.push(ShadowStackValue::ShadowObject(make())); + return; + }; + + let slot_base = reused.base(); + let slot_end = slot_base + reused.size(); + let reused_live = matches!(reused, ShadowStackValue::ShadowObject(_)); + + // also common: new object is being pushed after program has fallen + // back a few stack frames. Overwrite and truncate. + if reused_live { + assert!(slot_base == base, + "ShadowStack::add_shadow_object: re-push lands inside a live object"); + assert!(idx == 0 || new_end <= self.data[idx - 1].base(), + "ShadowStack::add_shadow_object: object overlaps the frame above"); + self.data[idx] = ShadowStackValue::ShadowObject(make()); + self.data.truncate(idx + 1); // drop everything more recent } - // If we didn't write through an invalidated object, we can drop subsequent frames else { - self.data.truncate(idx + 1); + // least common: stack re-use (new alloca inside previously invalidated region) + assert!(new_end <= slot_end, + "ShadowStack::add_shadow_object: object overflows its slot"); + self.data[idx] = ShadowStackValue::ShadowObject(make()); + + // retain invalidated padding around object + if slot_base < base { + self.data.insert(idx + 1, ShadowStackValue::InvalidatedObject(slot_base, base - slot_base)); + } + if new_end < slot_end { + self.data.insert(idx, ShadowStackValue::InvalidatedObject(new_end, slot_end - new_end)); + } } - } fn get_at(&self, addr: Vaddr) -> Result<(&ShadowStackValue, usize), LookupError> { @@ -223,33 +230,54 @@ impl ShadowStack { } /* - Invalidate a range of shadowstack. - - If that range spans to the end of the stack, we drop all the frames. - Else, we are prepping to re-using a piece of stack + Invalidate the address range [base, base + length). + Entries are dropped and replaced by a dead marker. */ pub fn invalidate_at(&mut self, base: Vaddr, length: usize) { if length == 0 { return; } + let end = base + length; // exclusive let Ok((_, start_idx)) = self.get_at(base) else { debug_assert!(false, "invalidate_at: untracked base"); return; }; - let Ok((_, end_idx)) = self.get_at(base + length - 1) else { + let Ok((_, end_idx)) = self.get_at(end - 1) else { debug_assert!(false, "invalidate_at: untracked limit"); return; }; debug_assert!(end_idx <= start_idx); - // pop all frames if range reaches the top of stack - if start_idx == self.data.len() - 1 { - self.data.truncate(end_idx); - return; - } + // ensure we don't carve into live objects + let eff_base = { + let lo = &self.data[start_idx]; // entry holding `base` (lowest address) + if lo.base() < base { + assert!(matches!(lo, ShadowStackValue::InvalidatedObject(..)), + "invalidate_at: range carves into a live object"); + lo.base() + } else { + base + } + }; + let eff_end = { + let hi = &self.data[end_idx]; // entry holding `end - 1` (highest address) + let hi_end = hi.base() + hi.size(); + if hi_end > end { + assert!(matches!(hi, ShadowStackValue::InvalidatedObject(..)), + "invalidate_at: range carves into a live object"); + hi_end + } else { + end + } + }; - // collapse the covered frames into one dead marker. - self.data.drain(end_idx..start_idx); - self.data[end_idx] = ShadowStackValue::InvalidatedObject(base, length); + let was_top = start_idx == self.data.len() - 1; + + self.data.drain(end_idx..=start_idx); + + // if we didn't reach the top, insert dead marker for invalidated range + if !was_top { + self.data.insert(end_idx, ShadowStackValue::InvalidatedObject(eff_base, eff_end - eff_base)); + } } // TODO: Does it make more sense to return something explicit @@ -344,63 +372,4 @@ mod tests { size, }) } - - // NOTE: Generated by Opus 4.8 - // - // Tests to ensure the stack grows descending and - // properly resolves common lookups - #[test] - fn shadowstack_descending_lookup() { - // Frames as the stack grows down: each new frame has a lower base, so - // the vec is descending by base (top of stack = last element). - let mut ss = ShadowStack::new(); - ss.data.push(stack_obj(0x3000, 0x100)); - ss.data.push(stack_obj(0x2000, 0x100)); - ss.data.push(stack_obj(0x1000, 0x100)); // top - - // Non-top frames must be found - assert_eq!(ss.search_intersection(0x3050).map(|o| o.base), Some(0x3000)); - assert_eq!(ss.search_intersection(0x2050).map(|o| o.base), Some(0x2000)); - assert_eq!(ss.search_intersection(0x1050).map(|o| o.base), Some(0x1000)); - - // Boundaries: base inclusive, base + size exclusive. - assert_eq!(ss.search_intersection(0x3000).map(|o| o.base), Some(0x3000)); - assert!(ss.search_intersection(0x3100).is_none()); - - // Below the top frame's base => below everything; gaps are misses. - assert!(ss.search_intersection(0x0fff).is_none()); - assert!(ss.search_intersection(0x2500).is_none()); - } - - // NOTE: Generated by Opus 4.8 - // - // Tests stack re-use where we tombstone invalidate a region - // of shadow stack and then re-allocate a sobj inside of it - // that may or may not fill that region entirely - #[test] - fn shadowstack_reuse() { - // Exact reuse: a dead region reused by an equally - // sized object leaves no dead space behind. - let mut ss = ShadowStack::new(); - ss.data.push(ShadowStackValue::InvalidatedObject(0x1000, 0x100)); - assert!(ss.search_intersection(0x1050).is_none()); // dead before reuse - - ss.add_shadow_object(0x1000, 0x100); - assert_eq!(ss.search_intersection(0x1050).map(|o| o.base), Some(0x1000)); // reused, live - assert_eq!(ss.data.len(), 1); // no leftover dead space - - // Partial reuse: a smaller object reuses the base and leaves the - // higher-base remainder dead. - let mut ss = ShadowStack::new(); - ss.data.push(ShadowStackValue::InvalidatedObject(0x1000, 0x400)); - assert!(ss.search_intersection(0x1050).is_none()); // dead before reuse - - ss.add_shadow_object(0x1000, 0x100); - assert_eq!(ss.search_intersection(0x1050).map(|o| o.base), Some(0x1000)); // reused, live - assert!(ss.search_intersection(0x1200).is_none()); // remainder still dead - - // Descending invariant: dead remainder (higher base) precedes the live object. - let bases: Vec<_> = ss.data.iter().map(|o| o.base()).collect(); - assert_eq!(bases, vec![0x1100, 0x1000]); - } -} +} \ No newline at end of file From 2616f54c455c64eacff485b731cee2b0c3bf9a07 Mon Sep 17 00:00:00 2001 From: Ryan Zmuda Date: Mon, 15 Jun 2026 14:23:32 -0400 Subject: [PATCH 12/14] fix: improve stack object handling and address overflow issues --- resolve-cveassert/libresolve/src/remediate.rs | 2 +- .../libresolve/src/shadowobjs.rs | 87 ++++++++----------- 2 files changed, 39 insertions(+), 50 deletions(-) diff --git a/resolve-cveassert/libresolve/src/remediate.rs b/resolve-cveassert/libresolve/src/remediate.rs index 5662c53b..1a2c6dab 100644 --- a/resolve-cveassert/libresolve/src/remediate.rs +++ b/resolve-cveassert/libresolve/src/remediate.rs @@ -275,7 +275,7 @@ impl From<&crate::shadowobjs::ShadowObject> for ShadowObjBounds { */ #[unsafe(no_mangle)] pub extern "C" fn __resolve_get_bounds_stack(ptr: *mut c_void) -> ShadowObjBounds { - return SHADOW_STACK.with_borrow_mut( + return SHADOW_STACK.with_borrow( |ss| { match ss.search_intersection(ptr as Vaddr) { Some(sobj) => { return sobj.into() } diff --git a/resolve-cveassert/libresolve/src/shadowobjs.rs b/resolve-cveassert/libresolve/src/shadowobjs.rs index 5bcff773..a14842ec 100644 --- a/resolve-cveassert/libresolve/src/shadowobjs.rs +++ b/resolve-cveassert/libresolve/src/shadowobjs.rs @@ -155,7 +155,8 @@ impl ShadowStack { } pub fn add_shadow_object(&mut self, base: Vaddr, size: usize) { - let new_end = base + size; // exclusive + let new_end = base.checked_add(size) + .expect("add_shadow_object: object overflows the address space"); // exclusive let make = || ShadowObject { alloc_type: AllocType::Stack, base, @@ -231,62 +232,61 @@ impl ShadowStack { /* Invalidate the address range [base, base + length). - Entries are dropped and replaced by a dead marker. + + `base` and `base + length` must each land exactly on a tracked object + boundary. The range may span several whole objects (live or dead), but + it may not bisect one. The spanned entries are dropped and replaced by + a single dead marker. */ pub fn invalidate_at(&mut self, base: Vaddr, length: usize) { if length == 0 { return; } - let end = base + length; // exclusive + let end = base.checked_add(length) + .expect("invalidate_at: range overflows the address space"); // exclusive - let Ok((_, start_idx)) = self.get_at(base) else { - debug_assert!(false, "invalidate_at: untracked base"); - return; - }; - let Ok((_, end_idx)) = self.get_at(end - 1) else { - debug_assert!(false, "invalidate_at: untracked limit"); - return; + // entry holding `base` (lowest address in the range) + let (start_idx, lo_base) = match self.get_at(base) { + Ok((v, idx)) => (idx, v.base()), + Err(_) => { debug_assert!(false, "invalidate_at: untracked base"); return; } }; - debug_assert!(end_idx <= start_idx); + assert!(lo_base == base, + "invalidate_at: range start 0x{base:x} is not an object boundary"); - // ensure we don't carve into live objects - let eff_base = { - let lo = &self.data[start_idx]; // entry holding `base` (lowest address) - if lo.base() < base { - assert!(matches!(lo, ShadowStackValue::InvalidatedObject(..)), - "invalidate_at: range carves into a live object"); - lo.base() - } else { - base - } - }; - let eff_end = { - let hi = &self.data[end_idx]; // entry holding `end - 1` (highest address) - let hi_end = hi.base() + hi.size(); - if hi_end > end { - assert!(matches!(hi, ShadowStackValue::InvalidatedObject(..)), - "invalidate_at: range carves into a live object"); - hi_end - } else { - end - } + // entry holding `end - 1` (highest address in the range) + let (end_idx, hi_end) = match self.get_at(end - 1) { + Ok((v, idx)) => (idx, v.base() + v.size()), + Err(_) => { debug_assert!(false, "invalidate_at: untracked limit"); return; } }; + assert!(hi_end == end, + "invalidate_at: range end 0x{end:x} is not an object boundary"); - let was_top = start_idx == self.data.len() - 1; + debug_assert!(end_idx <= start_idx); + let was_top = start_idx == self.data.len() - 1; self.data.drain(end_idx..=start_idx); - // if we didn't reach the top, insert dead marker for invalidated range + // if we didn't reach the top, leave a dead marker for the invalidated range if !was_top { - self.data.insert(end_idx, ShadowStackValue::InvalidatedObject(eff_base, eff_end - eff_base)); + self.data.insert(end_idx, ShadowStackValue::InvalidatedObject(base, length)); } } // TODO: Does it make more sense to return something explicit // when we search and get an invalidated stack object? pub fn search_intersection(&self, addr: Vaddr) -> Option<&ShadowObject> { - match self.get_at(addr) { - Ok((ShadowStackValue::ShadowObject(sobj), _)) => Some(sobj), - _ => None, + // exact containment in a live object + if let Ok((ShadowStackValue::ShadowObject(sobj), _)) = self.get_at(addr) { + return Some(sobj); } + + // edge case: GEP remediation one-past + if let Some(prev) = addr.checked_sub(1) { + if let Ok((ShadowStackValue::ShadowObject(sobj), _)) = self.get_at(prev) { + if sobj.past_limit() == addr { + return Some(sobj); + } + } + } + None } } @@ -361,15 +361,4 @@ mod tests { // let table = ShadowObjectTable::new(); //table.bounds(0xDEADBEEF).unwrap(); // should panic since there is no interesection } - - use super::{ShadowObject, ShadowStack, ShadowStackValue, Vaddr}; - - fn stack_obj(base: Vaddr, size: usize) -> ShadowStackValue { - ShadowStackValue::ShadowObject(ShadowObject { - alloc_type: AllocType::Stack, - base, - limit: ShadowObject::limit(base, size), - size, - }) - } } \ No newline at end of file From 0b9294592fe7856c1e4c6cd0db670394ab1c4e7a Mon Sep 17 00:00:00 2001 From: Ryan Zmuda Date: Tue, 16 Jun 2026 08:26:22 -0400 Subject: [PATCH 13/14] address simple review comments --- resolve-cveassert/libresolve/src/remediate.rs | 6 +++--- .../libresolve/src/shadowobjs.rs | 19 +++++++++++++------ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/resolve-cveassert/libresolve/src/remediate.rs b/resolve-cveassert/libresolve/src/remediate.rs index 1a2c6dab..c2964644 100644 --- a/resolve-cveassert/libresolve/src/remediate.rs +++ b/resolve-cveassert/libresolve/src/remediate.rs @@ -277,9 +277,9 @@ impl From<&crate::shadowobjs::ShadowObject> for ShadowObjBounds { pub extern "C" fn __resolve_get_bounds_stack(ptr: *mut c_void) -> ShadowObjBounds { return SHADOW_STACK.with_borrow( |ss| { - match ss.search_intersection(ptr as Vaddr) { - Some(sobj) => { return sobj.into() } - None => { return ShadowObjBounds::null(); } + return match ss.search_intersection(ptr as Vaddr) { + Some(sobj) => { sobj.into() } + None => { ShadowObjBounds::null() } } } ); diff --git a/resolve-cveassert/libresolve/src/shadowobjs.rs b/resolve-cveassert/libresolve/src/shadowobjs.rs index a14842ec..682aa420 100644 --- a/resolve-cveassert/libresolve/src/shadowobjs.rs +++ b/resolve-cveassert/libresolve/src/shadowobjs.rs @@ -33,22 +33,29 @@ pub struct ShadowObject { } impl ShadowObject { + /// Returns the base + limit of this shadow object as RangeInclusive + /// + /// Useful for querying contains pub fn bounds(&self) -> RangeInclusive { self.base..=self.limit } + /// Test if `addr` is within the bounds of this shadow object pub fn contains(&self, addr: Vaddr) -> bool { self.bounds().contains(&addr) } + /// Computes the size of the shadow object from its base and limit pub fn size(&self) -> usize { self.size } + /// Compute a limit from base and size pub fn limit(base: Vaddr, size: usize) -> Vaddr { if size == 0 { base } else { base.saturating_add(size - 1) } } + /// Compute the sentinel pointer value for this object, 1 past its limit pub fn past_limit(&self) -> Vaddr { self.limit.saturating_add(1) } @@ -279,13 +286,13 @@ impl ShadowStack { } // edge case: GEP remediation one-past - if let Some(prev) = addr.checked_sub(1) { - if let Ok((ShadowStackValue::ShadowObject(sobj), _)) = self.get_at(prev) { - if sobj.past_limit() == addr { - return Some(sobj); - } - } + if let Some(prev) = addr.checked_sub(1) + && let Ok((ShadowStackValue::ShadowObject(sobj), _)) = self.get_at(prev) + && sobj.past_limit() == addr + { + return Some(sobj); } + None } } From 0b8caa12cbb4572692dd56a663f17ec2fbdf2b3a Mon Sep 17 00:00:00 2001 From: Ryan Zmuda Date: Tue, 16 Jun 2026 10:29:26 -0400 Subject: [PATCH 14/14] convert shadowstack to operate on sobjs directly --- .../libresolve/src/shadowobjs.rs | 93 +++++++------------ 1 file changed, 31 insertions(+), 62 deletions(-) diff --git a/resolve-cveassert/libresolve/src/shadowobjs.rs b/resolve-cveassert/libresolve/src/shadowobjs.rs index 682aa420..2c64a827 100644 --- a/resolve-cveassert/libresolve/src/shadowobjs.rs +++ b/resolve-cveassert/libresolve/src/shadowobjs.rs @@ -33,6 +33,15 @@ pub struct ShadowObject { } impl ShadowObject { + pub fn new(ty: AllocType, base: Vaddr, size: usize) -> Self { + Self { + alloc_type: ty, + base, + limit: ShadowObject::limit(base, size), + size + } + } + /// Returns the base + limit of this shadow object as RangeInclusive /// /// Useful for querying contains @@ -74,13 +83,7 @@ impl ShadowObjectTable { /// Adds a new shadow object to the object list, replacing any existing object at `base` pub fn add_shadow_object(&mut self, alloc_type: AllocType, base: Vaddr, size: usize) { - let sobj = ShadowObject { - alloc_type, - base, - limit: ShadowObject::limit(base, size), - size, - }; - self.table.insert(base, sobj); + self.table.insert(base, ShadowObject::new(alloc_type, base, size)); } /// Removes the shadow object with base address equal to `base`. @@ -114,42 +117,11 @@ impl ShadowObjectTable { pub static ALIVE_OBJ_LIST: MutexWrap = MutexWrap::new(ShadowObjectTable::new()); pub static FREED_OBJ_LIST: MutexWrap = MutexWrap::new(ShadowObjectTable::new()); -enum ShadowStackValue { - ShadowObject(ShadowObject), - InvalidatedObject(Vaddr,usize) -} - -impl ShadowStackValue { - fn base(&self) -> Vaddr { - match self { - ShadowStackValue::ShadowObject(o) => o.base, - ShadowStackValue::InvalidatedObject(b, _s) => *b, - } - } - - fn size(&self) -> usize { - match self { - ShadowStackValue::ShadowObject(o) => o.size, - ShadowStackValue::InvalidatedObject(_b, s) => *s, - } - } - - fn contains(&self, addr: Vaddr) -> bool { - let base = self.base(); - - let Some(end) = base.checked_add(self.size() as Vaddr) else { - return false; - }; - - base <= addr && addr < end - } -} - // data must be ordered descending (downward growing stack on x86) // so push/pop are O(1) at the end. #[derive(Default)] pub struct ShadowStack { - data: Vec + data: Vec } enum LookupError { @@ -164,52 +136,46 @@ impl ShadowStack { pub fn add_shadow_object(&mut self, base: Vaddr, size: usize) { let new_end = base.checked_add(size) .expect("add_shadow_object: object overflows the address space"); // exclusive - let make = || ShadowObject { - alloc_type: AllocType::Stack, - base, - limit: ShadowObject::limit(base, size), - size, - }; let Ok((reused, idx)) = self.get_at(base) else { // most common: pushing a new obj onto the end of the stack - assert!(self.data.last().map_or(true, |top| new_end <= top.base()), + assert!(self.data.last().map_or(true, |top| new_end <= top.base), "ShadowStack::add_shadow_object: new object overlaps the stack top"); - self.data.push(ShadowStackValue::ShadowObject(make())); + self.data.push(ShadowObject::new(AllocType::Stack, base, size)); return; }; - let slot_base = reused.base(); + let slot_base = reused.base; let slot_end = slot_base + reused.size(); - let reused_live = matches!(reused, ShadowStackValue::ShadowObject(_)); + let reused_live = reused.alloc_type != AllocType::Unallocated; // also common: new object is being pushed after program has fallen // back a few stack frames. Overwrite and truncate. if reused_live { assert!(slot_base == base, "ShadowStack::add_shadow_object: re-push lands inside a live object"); - assert!(idx == 0 || new_end <= self.data[idx - 1].base(), + assert!(idx == 0 || new_end <= self.data[idx - 1].base, "ShadowStack::add_shadow_object: object overlaps the frame above"); - self.data[idx] = ShadowStackValue::ShadowObject(make()); + self.data[idx] = ShadowObject::new(AllocType::Stack, base, size); self.data.truncate(idx + 1); // drop everything more recent } else { // least common: stack re-use (new alloca inside previously invalidated region) assert!(new_end <= slot_end, "ShadowStack::add_shadow_object: object overflows its slot"); - self.data[idx] = ShadowStackValue::ShadowObject(make()); + self.data[idx] = ShadowObject::new(AllocType::Stack, base, size); // retain invalidated padding around object if slot_base < base { - self.data.insert(idx + 1, ShadowStackValue::InvalidatedObject(slot_base, base - slot_base)); + self.data.insert(idx + 1, ShadowObject::new(AllocType::Unallocated, slot_base, base - slot_base)); } if new_end < slot_end { - self.data.insert(idx, ShadowStackValue::InvalidatedObject(new_end, slot_end - new_end)); + self.data.insert(idx, ShadowObject::new(AllocType::Unallocated, new_end, slot_end - new_end)); } } } - fn get_at(&self, addr: Vaddr) -> Result<(&ShadowStackValue, usize), LookupError> { + fn get_at(&self, addr: Vaddr) -> Result<(&ShadowObject, usize), LookupError> { use LookupError::*; let n = self.data.len(); @@ -223,12 +189,12 @@ impl ShadowStack { } // easy out: below every tracked object - if addr < top.base() { + if addr < top.base { return Err(ObjectNotFound); // should we return a seperate ObjectOutOfBounds? } // binary search the shadow stack for value - let idx = self.data.partition_point(|o| o.base() > addr); + let idx = self.data.partition_point(|o| o.base > addr); let obj = &self.data[idx]; if obj.contains(addr) { Ok((obj, idx)) @@ -252,7 +218,7 @@ impl ShadowStack { // entry holding `base` (lowest address in the range) let (start_idx, lo_base) = match self.get_at(base) { - Ok((v, idx)) => (idx, v.base()), + Ok((v, idx)) => (idx, v.base), Err(_) => { debug_assert!(false, "invalidate_at: untracked base"); return; } }; assert!(lo_base == base, @@ -260,7 +226,7 @@ impl ShadowStack { // entry holding `end - 1` (highest address in the range) let (end_idx, hi_end) = match self.get_at(end - 1) { - Ok((v, idx)) => (idx, v.base() + v.size()), + Ok((v, idx)) => (idx, v.base + v.size()), Err(_) => { debug_assert!(false, "invalidate_at: untracked limit"); return; } }; assert!(hi_end == end, @@ -273,7 +239,7 @@ impl ShadowStack { // if we didn't reach the top, leave a dead marker for the invalidated range if !was_top { - self.data.insert(end_idx, ShadowStackValue::InvalidatedObject(base, length)); + self.data.insert(end_idx, ShadowObject::new(AllocType::Unallocated, base, length)); } } @@ -281,13 +247,16 @@ impl ShadowStack { // when we search and get an invalidated stack object? pub fn search_intersection(&self, addr: Vaddr) -> Option<&ShadowObject> { // exact containment in a live object - if let Ok((ShadowStackValue::ShadowObject(sobj), _)) = self.get_at(addr) { + if let Ok((sobj, _)) = self.get_at(addr) + && sobj.alloc_type != AllocType::Unallocated + { return Some(sobj); } // edge case: GEP remediation one-past if let Some(prev) = addr.checked_sub(1) - && let Ok((ShadowStackValue::ShadowObject(sobj), _)) = self.get_at(prev) + && let Ok((sobj, _)) = self.get_at(prev) + && sobj.alloc_type != AllocType::Unallocated && sobj.past_limit() == addr { return Some(sobj);