#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StackDepthBound {
Bytes(u64),
Unbounded,
Unknown,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BudgetSource {
ProvenStackDepth,
AssertedFallback,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BudgetDecision {
Use { bytes: u32, source: BudgetSource },
Refuse(String),
}
const STACK_ALIGN_BYTES: u64 = 16;
pub fn budget_from_bound(
bound: StackDepthBound,
sp_init: u32,
fallback: Option<u32>,
) -> BudgetDecision {
match bound {
StackDepthBound::Bytes(n) => {
if n > u64::from(sp_init) {
return BudgetDecision::Refuse(format!(
"proven shadow-stack depth {n} B exceeds the original reservation top \
sp_init={sp_init} B — the original image UNDER-provisioned its stack \
(this is a latent bug in the source, not a shrink target). Refusing. \
VCR-MEM-001 layer-2/#383."
));
}
let aligned = n.next_multiple_of(STACK_ALIGN_BYTES);
let bytes = aligned.min(u64::from(sp_init)) as u32;
BudgetDecision::Use {
bytes,
source: BudgetSource::ProvenStackDepth,
}
}
StackDepthBound::Unbounded | StackDepthBound::Unknown => match fallback {
Some(b) => BudgetDecision::Use {
bytes: b,
source: BudgetSource::AssertedFallback,
},
None => BudgetDecision::Refuse(format!(
"no finite shadow-stack bound was proven ({}) and no asserted \
--shadow-stack-size fallback was given; refusing to invent a budget. \
VCR-MEM-001 layer-2/#383.",
match bound {
StackDepthBound::Unbounded => "recursion / call-graph cycle ⇒ unbounded",
_ =>
"analysis incomplete ⇒ unknown (dynamic alloca, \
unresolved call_indirect, or ambiguous stack pointer)",
}
)),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn proven_depth_rounds_to_16_and_is_proven() {
let d = budget_from_bound(StackDepthBound::Bytes(32), 1_048_576, Some(4096));
assert_eq!(
d,
BudgetDecision::Use {
bytes: 32,
source: BudgetSource::ProvenStackDepth
}
);
}
#[test]
fn proven_depth_unaligned_rounds_up() {
let d = budget_from_bound(StackDepthBound::Bytes(33), 65_536, None);
assert_eq!(
d,
BudgetDecision::Use {
bytes: 48,
source: BudgetSource::ProvenStackDepth
}
);
}
#[test]
fn zero_depth_is_proven_zero() {
let d = budget_from_bound(StackDepthBound::Bytes(0), 4096, None);
assert_eq!(
d,
BudgetDecision::Use {
bytes: 0,
source: BudgetSource::ProvenStackDepth
}
);
}
#[test]
fn proven_depth_above_sp_init_is_under_provision_refusal() {
let d = budget_from_bound(StackDepthBound::Bytes(8192), 4096, Some(2048));
match d {
BudgetDecision::Refuse(msg) => {
assert!(msg.contains("UNDER-provisioned"), "got: {msg}");
assert!(msg.contains("8192"));
}
other => panic!("expected under-provision refusal, got {other:?}"),
}
}
#[test]
fn proven_depth_exactly_at_top_is_accepted() {
let d = budget_from_bound(StackDepthBound::Bytes(4096), 4096, None);
assert_eq!(
d,
BudgetDecision::Use {
bytes: 4096,
source: BudgetSource::ProvenStackDepth
}
);
}
#[test]
fn unbounded_with_fallback_is_asserted() {
let d = budget_from_bound(StackDepthBound::Unbounded, 65_536, Some(8192));
assert_eq!(
d,
BudgetDecision::Use {
bytes: 8192,
source: BudgetSource::AssertedFallback
}
);
}
#[test]
fn unbounded_without_fallback_refuses() {
let d = budget_from_bound(StackDepthBound::Unbounded, 65_536, None);
match d {
BudgetDecision::Refuse(msg) => {
assert!(msg.contains("unbounded"), "got: {msg}");
assert!(msg.contains("recursion"));
}
other => panic!("expected unbounded refusal, got {other:?}"),
}
}
#[test]
fn unknown_with_fallback_is_asserted_not_proven() {
let d = budget_from_bound(StackDepthBound::Unknown, 65_536, Some(1024));
assert_eq!(
d,
BudgetDecision::Use {
bytes: 1024,
source: BudgetSource::AssertedFallback
}
);
}
#[test]
fn unknown_without_fallback_refuses_with_cause() {
let d = budget_from_bound(StackDepthBound::Unknown, 65_536, None);
match d {
BudgetDecision::Refuse(msg) => {
assert!(msg.contains("unknown"), "got: {msg}");
assert!(msg.contains("call_indirect") || msg.contains("alloca"));
}
other => panic!("expected unknown refusal, got {other:?}"),
}
}
}