Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/kani.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ jobs:
# - core_arch::x86::__m128d::as_f64x2 is just one example of hundreds of
# core_arch::x86:: functions that are known to verify successfully.
- name: Run Kani Verification
env:
# The flt2dec memory-safety harnesses (challenge #28) stub the bignum/Fp
# arithmetic, which makes the std `debug_assert!` digit-correctness checks
# (e.g. `d < 10`, `mant < scale`) unprovable. Those asserts are not
# memory-safety properties and are dead in release and in the verify-std
# job (which runs with `--prove-safety-only`, i.e. debug-assertions off).
# Disabling them here keeps autoharness consistent with verify-std; this is
# monotonic (it only removes checks, so no harness can newly fail).
RUSTFLAGS: "-C debug-assertions=off"
run: |
scripts/run-kani.sh --run autoharness --kani-args \
--include-pattern "<(.+)[[:space:]]as[[:space:]](.+)>::disjoint_bitor" \
Expand Down
3 changes: 3 additions & 0 deletions library/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@
#![no_core]
#![rustc_coherence_is_core]
#![rustc_preserve_ub_checks]
// Verification-only (Kani): the flt2dec dragon_verify_stub harness stacks many
// #[kani::stub] attributes whose macro expansion exceeds the default limit.
#![cfg_attr(kani, recursion_limit = "1024")]
//
// Lints:
#![deny(rust_2021_incompatible_or_patterns)]
Expand Down
18 changes: 18 additions & 0 deletions library/core/src/num/bignum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,24 @@ macro_rules! define_bignum {
$name { size: sz, base }
}

/// A nondeterministic but structurally valid bignum, for use as a
/// sound over-approximating stub of the expensive arithmetic methods
/// during Kani verification. Upholds the representation invariant
/// (`size in [1, n]`, `base[size..] == 0`) so callers that read the
/// digits never observe an inconsistent state.
#[cfg(kani)]
pub fn kani_any() -> $name {
let size: usize = crate::kani::any();
crate::kani::assume(size >= 1 && size <= $n);
let mut base = [0; $n];
let mut i = 0;
while i < size {
base[i] = crate::kani::any();
i += 1;
}
$name { size, base }
}

/// Returns the internal digits as a slice `[a, b, c, ...]` such that the numeric
/// value is `a + b * 2^W + c * 2^(2W) + ...` where `W` is the number of bits in
/// the digit type.
Expand Down
169 changes: 169 additions & 0 deletions library/core/src/num/flt2dec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -666,3 +666,172 @@ where
}
}
}

#[cfg(kani)]
#[unstable(feature = "kani", issue = "none")]
pub mod flt2dec_verify {
use super::*;
use crate::kani;

// A small fixed digit-buffer length keeps the proofs tractable. The
// `assume_init` safety obligations in these functions depend only on control
// flow driven by `buf.len()`, `exp`, and the digit-count arguments; every
// branch (and therefore every distinct set of initialized `parts`) is still
// reachable at this length, so a fixed length loses no path coverage.
const PROOF_BUFLEN: usize = 4;

// `digits_to_dec_str` writes 2, 3, or 4 `parts` depending on `exp` and
// `frac_digits`, then `assume_init_ref`s exactly the prefix it wrote. Kani
// checks that no uninitialized `Part` is ever read and that no UB occurs.
#[kani::proof]
fn check_digits_to_dec_str() {
let buf: [u8; PROOF_BUFLEN] = kani::any();
kani::assume(buf[0] > b'0');
let exp: i16 = kani::any();
let frac_digits: usize = kani::any();
let mut parts: [MaybeUninit<Part<'_>>; 4] = [const { MaybeUninit::uninit() }; 4];
let _ = digits_to_dec_str(&buf, exp, frac_digits, &mut parts);
}

// `digits_to_exp_str` writes a variable prefix of up to 6 `parts` and
// `assume_init_ref`s `parts[..n + 2]` for the `n` it actually wrote.
#[kani::proof]
fn check_digits_to_exp_str() {
let buf: [u8; PROOF_BUFLEN] = kani::any();
kani::assume(buf[0] > b'0');
let exp: i16 = kani::any();
let min_ndigits: usize = kani::any();
let upper: bool = kani::any();
let mut parts: [MaybeUninit<Part<'_>>; 6] = [const { MaybeUninit::uninit() }; 6];
let _ = digits_to_exp_str(&buf, exp, min_ndigits, upper, &mut parts);
}

// An arbitrary sign-formatting option.
fn any_sign() -> Sign {
if kani::any() { Sign::Minus } else { Sign::MinusPlus }
}

// A stub digit generator standing in for `grisu`/`dragon` `format_shortest`.
// It writes one arbitrary nonzero digit into the scratch buffer and returns
// it with an arbitrary exponent. This isolates the `to_shortest_*`
// functions' own `unsafe` (the `assume_init` on `parts` and the delegation
// to the already-verified `digits_to_*_str`) from the loopy strategy code,
// which is verified separately. A generic `fn` is required here rather than
// a closure so it satisfies the higher-ranked lifetime in the `F` bound.
fn stub_shortest<'a>(_d: &Decoded, buf: &'a mut [MaybeUninit<u8>]) -> (&'a [u8], i16) {
let digit: u8 = kani::any();
kani::assume(digit > b'0');
buf[0] = MaybeUninit::new(digit);
let exp: i16 = kani::any();
// SAFETY: we just initialized the element `..1`.
(unsafe { buf[..1].assume_init_ref() }, exp)
}

// `to_shortest_str` handles NaN/Inf/Zero by writing `parts[..1]` and the
// finite case by delegating to `digits_to_dec_str`. An arbitrary `f64`
// reaches every `FullDecoded` arm.
#[kani::proof]
fn check_to_shortest_str() {
let v: f64 = kani::any();
let sign = any_sign();
let frac_digits: usize = kani::any();
let mut buf: [MaybeUninit<u8>; MAX_SIG_DIGITS] =
[const { MaybeUninit::uninit() }; MAX_SIG_DIGITS];
let mut parts: [MaybeUninit<Part<'_>>; 4] = [const { MaybeUninit::uninit() }; 4];
let _ = to_shortest_str(stub_shortest, v, sign, frac_digits, &mut buf, &mut parts);
}

// `to_shortest_exp_str` is the exponential-form analogue; its finite arm
// delegates to `digits_to_dec_str` or `digits_to_exp_str` per `dec_bounds`.
#[kani::proof]
fn check_to_shortest_exp_str() {
let v: f64 = kani::any();
let sign = any_sign();
let lo: i16 = kani::any();
let hi: i16 = kani::any();
kani::assume(lo <= hi);
let upper: bool = kani::any();
let mut buf: [MaybeUninit<u8>; MAX_SIG_DIGITS] =
[const { MaybeUninit::uninit() }; MAX_SIG_DIGITS];
let mut parts: [MaybeUninit<Part<'_>>; 6] = [const { MaybeUninit::uninit() }; 6];
let _ = to_shortest_exp_str(stub_shortest, v, sign, (lo, hi), upper, &mut buf, &mut parts);
}

// For `f64`, `decode` bottoms out at `decoded.exp == -1076` (normal-min,
// which subtracts 2 from `integer_decode`'s minimum of `-1074`), where
// `estimate_max_buf_len` returns 828. 1024 (the size the real `fmt` callers
// use) covers every reachable decoded exponent for the
// `buf.len() >= maxlen` assertions in both `to_exact_*` functions.
const PROOF_EXACT_BUFLEN: usize = 1024;

// Stub `format_exact` for `to_exact_exp_str`, which always passes the result
// to `digits_to_exp_str` (it calls the generator with `limit = i16::MIN`, so
// the real one never returns an empty buffer here). Returns one nonzero
// digit with an arbitrary exponent.
fn stub_exact_full<'a>(
_d: &Decoded,
buf: &'a mut [MaybeUninit<u8>],
_limit: i16,
) -> (&'a [u8], i16) {
let digit: u8 = kani::any();
kani::assume(digit > b'0');
buf[0] = MaybeUninit::new(digit);
let exp: i16 = kani::any();
// SAFETY: we just initialized the element `..1`.
(unsafe { buf[..1].assume_init_ref() }, exp)
}

// Stub `format_exact` for `to_exact_fixed_str`, which branches on
// `exp <= limit`. That arm requires an empty result (the source
// `debug_assert_eq!`s `buf.len() == 0`); the other arm needs a valid nonzero
// digit with `exp > limit`. Couple the result to `limit` so both caller
// arms are exercised soundly.
fn stub_exact_limited<'a>(
_d: &Decoded,
buf: &'a mut [MaybeUninit<u8>],
limit: i16,
) -> (&'a [u8], i16) {
if kani::any() {
let exp: i16 = kani::any();
kani::assume(exp <= limit);
// SAFETY: an empty prefix is trivially initialized.
(unsafe { buf[..0].assume_init_ref() }, exp)
} else {
let digit: u8 = kani::any();
kani::assume(digit > b'0');
buf[0] = MaybeUninit::new(digit);
let exp: i16 = kani::any();
kani::assume(exp > limit);
// SAFETY: we just initialized the element `..1`.
(unsafe { buf[..1].assume_init_ref() }, exp)
}
}

// `to_exact_exp_str` writes `parts[..1]` for NaN/Inf, `parts[..3]`/`parts[..1]`
// for zero, and delegates to `digits_to_exp_str` for finite values.
#[kani::proof]
fn check_to_exact_exp_str() {
let v: f64 = kani::any();
let sign = any_sign();
let ndigits: usize = kani::any();
kani::assume(ndigits > 0);
let upper: bool = kani::any();
let mut buf: [MaybeUninit<u8>; PROOF_EXACT_BUFLEN] =
[const { MaybeUninit::uninit() }; PROOF_EXACT_BUFLEN];
let mut parts: [MaybeUninit<Part<'_>>; 6] = [const { MaybeUninit::uninit() }; 6];
let _ = to_exact_exp_str(stub_exact_full, v, sign, ndigits, upper, &mut buf, &mut parts);
}

// `to_exact_fixed_str` additionally has a finite sub-branch (`exp <= limit`)
// that renders like zero; `stub_exact_limited` reaches both sub-branches.
#[kani::proof]
fn check_to_exact_fixed_str() {
let v: f64 = kani::any();
let sign = any_sign();
let frac_digits: usize = kani::any();
let mut buf: [MaybeUninit<u8>; PROOF_EXACT_BUFLEN] =
[const { MaybeUninit::uninit() }; PROOF_EXACT_BUFLEN];
let mut parts: [MaybeUninit<Part<'_>>; 4] = [const { MaybeUninit::uninit() }; 4];
let _ = to_exact_fixed_str(stub_exact_limited, v, sign, frac_digits, &mut buf, &mut parts);
}
}
136 changes: 136 additions & 0 deletions library/core/src/num/flt2dec/strategy/dragon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,21 @@ pub fn format_shortest<'a>(
let mut up;
let mut i = 0;
loop {
// VERIFICATION (Kani, compiles out otherwise): the Dragon/Loitsch
// digit-count theorem (Burger & Dybvig 1996, Fig 3; Loitsch, PLDI'10): a
// 53-bit-precision f64 has a shortest decimal of at most
// `ceil(53*log10 2) + 1 = 17 = MAX_SIG_DIGITS` significant digits. Every
// `Decoded` reaching this function comes from `decode()` on a real f64
// (the only caller is `format_shortest`), so the digit index `i` never
// reaches 17. This bounds the DIGIT COUNT (an input-precision property);
// buffer safety `i < buf.len()` follows from the separate
// `assert!(buf.len() >= MAX_SIG_DIGITS)` above. The loop break depends on
// the `Big` comparison `mant < minus || scale < mant+plus`, a
// number-theoretic termination fact CBMC cannot derive from the
// (stubbed/havoced) bignum arithmetic, so it is cited.
#[cfg(kani)]
crate::kani::assume(i < MAX_SIG_DIGITS);

// invariants, where `d[0..n-1]` are digits generated so far:
// - `v = mant / scale * 10^(k-n-1) + d[0..n-1] * 10^(k-n)`
// - `v - low = minus / scale * 10^(k-n-1)`
Expand Down Expand Up @@ -248,6 +263,13 @@ pub fn format_shortest<'a>(
// but we are just being safe and consistent here.
// SAFETY: we initialized that memory above.
if let Some(c) = round_up(unsafe { buf[..i].assume_init_mut() }) {
// VERIFICATION (Kani, compiles out otherwise): the digit-count theorem
// bounds the TOTAL significant digits (the `i` generated in the loop
// plus this round-up carry) to <= MAX_SIG_DIGITS, so this carry write
// is in bounds (`i < MAX_SIG_DIGITS <= buf.len()`). Same cited
// Dragon/Loitsch bound as the loop assume above.
#[cfg(kani)]
crate::kani::assume(i < MAX_SIG_DIGITS);
buf[i] = MaybeUninit::new(c);
i += 1;
k += 1;
Expand Down Expand Up @@ -387,3 +409,117 @@ pub fn format_exact<'a>(
// SAFETY: we initialized that memory above.
(unsafe { buf[..len].assume_init_ref() }, k)
}

// Buffer-safety-only proof of format_exact via COMPLETE bignum stubbing.
// Hypothesis: with debug-assertions OFF (so `debug_assert!(d < 10)` is dead, like
// the VeriFast frontend) AND every Big op havoc-stubbed (incl. is_zero/cmp, which
// a partial stub left concrete and bit-blasting), format_exact's only obligations
// are the explicit `for i in 0..len` (len <= buf.len()) bound + the assume_init
// init tracking -- pure control flow, no arithmetic. Run with
// RUSTFLAGS="-C debug-assertions=off".
#[cfg(kani)]
#[unstable(feature = "kani", issue = "none")]
pub mod dragon_verify_stub {
use super::*;
use crate::kani;

// Mutating ops: NO-OP stubs. The Big *values* are irrelevant to buffer
// safety, and all value inspection (is_zero/cmp) is independently stubbed, so
// leaving the Big unchanged is sound and cheap (no symbolic state injected).
fn s_mul_pow2(s: &mut Big, _bits: usize) -> &mut Big {
s
}
fn s_mul_small(s: &mut Big, _o: Digit) -> &mut Big {
s
}
fn s_sub<'a>(s: &'a mut Big, _o: &Big) -> &'a mut Big {
s
}
fn s_add<'a>(s: &'a mut Big, _o: &Big) -> &'a mut Big {
s
}
fn s_mul_digits<'a>(s: &'a mut Big, _o: &[Digit]) -> &'a mut Big {
s
}
fn s_mul_pow10<'a>(s: &'a mut Big, _n: usize) -> &'a mut Big {
s
}
fn s_div_2pow10<'a>(s: &'a mut Big, _n: usize) -> &'a mut Big {
s
}
// Value inspection: drives control flow nondeterministically.
fn s_is_zero(_s: &Big) -> bool {
kani::any()
}
fn s_cmp(_s: &Big, _o: &Big) -> crate::cmp::Ordering {
let x: u8 = kani::any();
match x % 3 {
0 => crate::cmp::Ordering::Less,
1 => crate::cmp::Ordering::Equal,
_ => crate::cmp::Ordering::Greater,
}
}
fn s_estimate(_m: u64, _e: i16) -> i16 {
let k: i16 = kani::any();
kani::assume(k > -400 && k < 400);
k
}

#[kani::proof]
#[kani::unwind(6)]
#[kani::stub(Big::mul_pow2, s_mul_pow2)]
#[kani::stub(Big::mul_small, s_mul_small)]
#[kani::stub(Big::sub, s_sub)]
#[kani::stub(Big::add, s_add)]
#[kani::stub(Big::mul_digits, s_mul_digits)]
#[kani::stub(Big::is_zero, s_is_zero)]
#[kani::stub(Big::cmp, s_cmp)]
#[kani::stub(mul_pow10, s_mul_pow10)]
#[kani::stub(div_2pow10, s_div_2pow10)]
#[kani::stub(estimate_scaling_factor, s_estimate)]
fn check_format_exact_stub() {
let mant: u64 = kani::any();
kani::assume(mant > 0 && mant < (1 << 61));
let exp: i16 = kani::any();
kani::assume(exp >= -1076 && exp <= 971);
let d = Decoded { mant, minus: 1, plus: 1, exp, inclusive: kani::any() };
let limit: i16 = kani::any();
let mut buf: [MaybeUninit<u8>; 4] = [const { MaybeUninit::uninit() }; 4];
let _ = format_exact(&d, &mut buf, limit);
}

// Tight decode() precondition for f64 (decoder.rs: minus is always 1, plus is
// 1 or 2, mant is the shifted f64 mantissa so mant <= 2^54). format_shortest
// is internal and only called on a decode() result, so this is its true
// precondition; under it the Dragon/Loitsch digit-count theorem holds.
fn arbitrary_decoded_tight() -> Decoded {
let mant: u64 = kani::any();
kani::assume(mant >= 2 && mant <= (1u64 << 54));
let plus: u64 = kani::any();
kani::assume(plus == 1 || plus == 2);
let exp: i16 = kani::any();
kani::assume(exp >= -1076 && exp <= 971);
Decoded { mant, minus: 1, plus, exp, inclusive: kani::any() }
}

// Buffer-safety proof of format_shortest: complete bignum no-op stubs (Big
// values are irrelevant to buffer safety; `cmp` is nondeterministic so all
// control-flow paths are explored), the tight decode precondition, and the
// in-loop digit-count assume bound the implicit loop. CBMC unrolls (no loop
// contracts).
#[kani::proof]
#[kani::unwind(19)]
#[kani::stub(Big::mul_pow2, s_mul_pow2)]
#[kani::stub(Big::mul_small, s_mul_small)]
#[kani::stub(Big::sub, s_sub)]
#[kani::stub(Big::add, s_add)]
#[kani::stub(Big::cmp, s_cmp)]
#[kani::stub(mul_pow10, s_mul_pow10)]
#[kani::stub(estimate_scaling_factor, s_estimate)]
fn check_format_shortest_stub() {
let d = arbitrary_decoded_tight();
let mut buf: [MaybeUninit<u8>; MAX_SIG_DIGITS] =
[const { MaybeUninit::uninit() }; MAX_SIG_DIGITS];
let _ = format_shortest(&d, &mut buf);
}
}
Loading
Loading