santh-writ 0.1.1

CPU symbolic execution + exploit witness construction. Takes a weir-produced source→sink path and returns a concrete input that drives execution to the sink. Z3-backed.
// Tests for `sink_payloads`. Wired via
// `#[path = "sink_payloads_tests.rs"] mod tests;`.

use super::*;
use crate::{PathLocation, ShapeParams, SinkConstraintKind, WitnessRequest};

fn dummy_request(
    sink_kind: SinkConstraintKind,
    shape_params: Option<ShapeParams>,
) -> WitnessRequest {
    WitnessRequest {
        source: PathLocation {
            file: "src/foo.c".to_string(),
            byte_start: 0,
            byte_end: 16,
        },
        sink: PathLocation {
            file: "src/foo.c".to_string(),
            byte_start: 100,
            byte_end: 116,
        },
        statements: Vec::new(),
        sink_kind,
        shape_params,
    }
}

// ── stack_overflow_sprintf_unbounded ──────────────────────────────

#[test]
fn sprintf_unbounded_witness_overflows_dst_with_canary_padding() {
    let dst_capacity = 64;
    let w = sprintf_unbounded_witness(dst_capacity).expect("64-byte capacity witnesses");
    // Total length = capacity + 16-byte canary padding.
    assert_eq!(w.bytes.len(), dst_capacity + 16);
    // Every byte is the printable filler used by the proof.
    assert!(w.bytes.iter().all(|b| *b == b'A'));
    assert!(matches!(w.entrypoint_kind, EntrypointKind::Cli));
}

/// Test-gap fix: the render command must report the exact byte count the
/// `bytes` field carries, across boundary capacities. Previously no test tied
/// the render string to the payload length, so a drift (e.g. a hardcoded pad
/// that disagreed with CANARY_PADDING) would go unnoticed.
#[test]
fn sprintf_unbounded_render_byte_count_matches_bytes() {
    for dst_capacity in [0usize, 1, 64, 4096] {
        let w = sprintf_unbounded_witness(dst_capacity).expect("small capacity witnesses");
        let total = dst_capacity + CANARY_PADDING;
        assert_eq!(
            w.bytes.len(),
            total,
            "byte length must equal dst_capacity + CANARY_PADDING"
        );
        assert!(
            w.render.contains(&format!("\"A\" * {total}")),
            "render must state the exact byte count {total}, got: {}",
            w.render
        );
    }
}

/// The gets witness reuses stack_overflow_witness's bytes; its render must
/// report the SAME count those bytes carry (dedup fix: both derive from the
/// single CANARY_PADDING owner, so they cannot disagree).
#[test]
fn gets_witness_render_matches_stack_overflow_bytes() {
    for dst_capacity in [0usize, 1, 64] {
        let w = gets_witness(dst_capacity).expect("small capacity witnesses");
        let total = dst_capacity + CANARY_PADDING;
        assert_eq!(w.bytes.len(), total);
        assert!(
            w.render.contains(&format!("\"A\" * {total}")),
            "gets render must state {total}, got: {}",
            w.render
        );
    }
}

/// Overflow guard (audit): an implausibly large or wrapping capacity must
/// refuse rather than OOM or emit a silently-short payload.
#[test]
fn stack_overflow_witness_refuses_capacity_that_overflows_or_exceeds_cap() {
    // usize::MAX + CANARY_PADDING wraps -> refuse.
    assert!(stack_overflow_witness(usize::MAX).is_none());
    // Just under the wrap point but still absurd -> exceeds MAX_WITNESS_BYTES.
    assert!(stack_overflow_witness(usize::MAX - CANARY_PADDING).is_none());
    // Beyond the 64 MiB cap -> refuse (no plausible stack buffer this large).
    assert!(stack_overflow_witness(MAX_WITNESS_BYTES).is_none());
    assert!(sprintf_unbounded_witness(usize::MAX).is_none());
    assert!(gets_witness(usize::MAX).is_none());
    // Exactly at the cap minus padding is the largest accepted witness.
    let max_ok = MAX_WITNESS_BYTES - CANARY_PADDING;
    let w = stack_overflow_witness(max_ok).expect("capacity at the cap boundary witnesses");
    assert_eq!(w.bytes.len(), MAX_WITNESS_BYTES);
}

#[test]
fn sprintf_unbounded_dispatches_via_try_trivial_witness() {
    let req = dummy_request(
        SinkConstraintKind::StackOverflowSprintfUnbounded,
        Some(ShapeParams::StackOverflow { dst_capacity: 128 }),
    );
    let out = try_trivial_witness(&req).expect("sprintf shape witnesses trivially");
    assert_eq!(out.bytes.len(), 128 + 16);
}

// ── heap_overflow_alloc_arith_wraps ───────────────────────────────

#[test]
fn alloc_arith_wraps_emits_smallest_count_for_u32_arch() {
    // struct_size = 24 → smallest n s.t. n * 24 >= 2^32:
    //   ceil(2^32 / 24) = 178956971
    //   wrapped: 178956971 * 24 mod 2^32 = 8
    let w = alloc_arith_wraps_witness(24, 4).expect("encoder produces witness");
    // First 4 bytes = the count as little-endian u32.
    let count_bytes: [u8; 4] = w.bytes[..4].try_into().unwrap();
    let n = u32::from_le_bytes(count_bytes);
    let wrapped = (n as u64).wrapping_mul(24) & 0xFFFF_FFFF;
    // The wrapped allocation must be smaller than what the caller
    // believes they asked for  -  that's the whole exploit.
    assert!(wrapped < (n as u64) * 24);
    // Payload after the count exceeds the wrapped capacity.
    let write_len = w.bytes.len() - 4;
    assert!(write_len as u64 > wrapped);
}

#[test]
fn alloc_arith_wraps_emits_smallest_count_for_u64_arch() {
    // struct_size = 1024 on a 64-bit pointer arch.
    //   2^64 / 1024 = 18014398509481984
    //   wraps to 0 because 1024 divides 2^64 evenly.
    // We still want an overflow: encoder picks ceil + 1; verify
    // the math holds without overflowing u128 in the test.
    let w = alloc_arith_wraps_witness(1024, 8).expect("encoder produces witness");
    let count_bytes: [u8; 8] = w.bytes[..8].try_into().unwrap();
    let n = u64::from_le_bytes(count_bytes);
    let wrapped = (n as u128).wrapping_mul(1024) % (1u128 << 64);
    // Either wrapped < requested (genuine overflow), or wrapped
    // equals 0 (full ring cycle), which still defeats the alloc.
    assert!(wrapped < (n as u128) * 1024 || wrapped == 0);
}

#[test]
fn alloc_arith_wraps_returns_none_for_unit_struct_size() {
    // struct_size = 1 cannot wrap  -  n * 1 = n always.
    assert!(alloc_arith_wraps_witness(1, 8).is_none());
}

#[test]
fn alloc_arith_wraps_dispatches_via_try_trivial_witness() {
    let req = dummy_request(
        SinkConstraintKind::HeapOverflowAllocArithWraps,
        Some(ShapeParams::AllocArithWraps {
            struct_size: 16,
            ptr_width_bytes: 8,
        }),
    );
    let out = try_trivial_witness(&req).expect("alloc-wrap witnesses trivially");
    assert_eq!(out.bytes.len() - 8 > 0, true);
}

// ── oob_write_unbounded_index ─────────────────────────────────────

#[test]
fn oob_write_index_positive_overflow_emits_capacity_as_first_illegal_index() {
    let capacity = 32;
    let w = oob_write_index_witness(capacity, false, 4).expect("encoder produces witness");
    // First 4 bytes = the illegal index encoded as u32.
    let idx_bytes: [u8; 4] = w.bytes[..4].try_into().unwrap();
    let idx = u32::from_le_bytes(idx_bytes);
    assert_eq!(idx, capacity as u32);
    // Trailing payload byte present.
    assert_eq!(w.bytes.len(), 5);
    assert_eq!(w.bytes[4], b'C');
}

#[test]
fn oob_write_index_signed_variant_emits_negative_one() {
    let w = oob_write_index_witness(32, true, 4).expect("encoder produces witness");
    let idx_bytes: [u8; 4] = w.bytes[..4].try_into().unwrap();
    let idx = i32::from_le_bytes(idx_bytes);
    assert_eq!(idx, -1);
    assert_eq!(w.bytes.len(), 5);
}

/// AUDIT 2026-04-27 finding 4 (writ HIGH): index emitted at the
/// sink's actual ABI width  -  not always 4 bytes. A 64-bit
/// `size_t` sink reads 8 bytes; emitting 4 leaves the upper
/// word zero/undefined and produces the wrong index.
#[test]
fn oob_write_index_emits_8_byte_index_on_64bit_sink() {
    let capacity = 256;
    let w = oob_write_index_witness(capacity, false, 8).expect("encoder produces witness");
    let idx_bytes: [u8; 8] = w.bytes[..8].try_into().unwrap();
    let idx = u64::from_le_bytes(idx_bytes);
    assert_eq!(idx, capacity as u64);
    assert_eq!(w.bytes.len(), 9);
}

#[test]
fn oob_write_index_signed_variant_emits_8_byte_negative_one() {
    let w = oob_write_index_witness(32, true, 8).expect("encoder produces witness");
    let idx_bytes: [u8; 8] = w.bytes[..8].try_into().unwrap();
    let idx = i64::from_le_bytes(idx_bytes);
    assert_eq!(idx, -1);
    assert_eq!(w.bytes.len(), 9);
}

/// AUDIT 2026-04-27 finding 8 (writ LOW): the encoder must
/// refuse to silently truncate `capacity` to `u32::MAX` when
/// the requested 32-bit width cannot represent the value.
#[test]
fn oob_write_index_returns_none_when_capacity_exceeds_index_width() {
    // capacity > u32::MAX encoded into a 4-byte index slot is
    // not representable; encoder must refuse.
    assert!(
        oob_write_index_witness((u32::MAX as usize).wrapping_add(1), false, 4).is_none()
            || (u32::MAX as usize).checked_add(1).is_none()
    );
}

#[test]
fn oob_write_index_returns_none_for_unsupported_width() {
    assert!(oob_write_index_witness(32, false, 2).is_none());
    assert!(oob_write_index_witness(32, true, 16).is_none());
}

#[test]
fn oob_write_index_dispatches_via_try_trivial_witness() {
    let req = dummy_request(
        SinkConstraintKind::OobWriteUnboundedIndex,
        Some(ShapeParams::OobWriteIndex {
            capacity: 256,
            signed: false,
            index_width_bytes: 4,
        }),
    );
    let out = try_trivial_witness(&req).expect("OOB-index witnesses trivially");
    let idx = u32::from_le_bytes(out.bytes[..4].try_into().unwrap());
    assert_eq!(idx, 256);
}

// ── alloc_arith_wraps_witness  -  ZST shape (audit finding 6) ───────

/// AUDIT 2026-04-27 finding 6 (writ MEDIUM): `struct_size == 0`
/// (C flexible-array-member or ZST patterns) must produce a
/// valid Class-1 witness  -  `malloc(n * 0)` always allocates 0
/// bytes, so any subsequent write is unconditionally a heap
/// overflow.
#[test]
fn alloc_arith_wraps_admits_zero_struct_size() {
    let w = alloc_arith_wraps_witness(0, 8).expect("ZST shape produces a witness");
    // First 8 bytes = count (1, encoded as u64 little-endian).
    let count_bytes: [u8; 8] = w.bytes[..8].try_into().unwrap();
    let n = u64::from_le_bytes(count_bytes);
    assert_eq!(n, 1);
    // Remaining bytes are the canary-padding write payload.
    assert!(w.bytes.len() > 8);
}

#[test]
fn alloc_arith_wraps_returns_none_for_struct_size_one() {
    // n * 1 == n always; no wrap possible.
    assert!(alloc_arith_wraps_witness(1, 8).is_none());
}

// ── shape-params mismatch falls through to None ───────────────────

#[test]
fn try_trivial_witness_returns_none_when_shape_params_missing() {
    let req = dummy_request(SinkConstraintKind::StackOverflowSprintfUnbounded, None);
    assert!(try_trivial_witness(&req).is_none());
}

#[test]
fn try_trivial_witness_returns_none_when_shape_params_wrong_variant() {
    // OobWriteIndex shape_params handed to a stack-overflow sink
    // can't mean anything  -  encoder must refuse rather than guess.
    let req = dummy_request(
        SinkConstraintKind::StackOverflowStrcpy,
        Some(ShapeParams::OobWriteIndex {
            capacity: 32,
            signed: false,
            index_width_bytes: 4,
        }),
    );
    assert!(try_trivial_witness(&req).is_none());
}