Skip to content

Commit e4e3297

Browse files
committed
Verify core::num::flt2dec memory safety (challenge #28)
Add Kani proof harnesses establishing the memory safety of all 12 safe-functions-with-unsafe-bodies in core::num::flt2dec: the 6 formatting entry points (flt2dec/mod.rs) and the 6 Grisu/Dragon strategy functions (flt2dec/strategy/{grisu,dragon}.rs). Each unsafe block (MaybeUninit::assume_init_* and slice indexing) is proven to touch only initialized, in-bounds memory. The bignum/Fp arithmetic is abstracted via sound stubbing -- buffer safety is independent of the numeric values, and value inspection (cmp/is_zero) is made nondeterministic so all control-flow paths are explored. The shortest-mode functions (grisu::format_shortest_opt, dragon::format_shortest) have an implicit loop bound; their digit index is bounded by the Grisu/Loitsch digit-count theorem (a 53-bit-precision f64 has <= MAX_SIG_DIGITS = 17 significant decimal digits), cited as a cfg(kani) assume because CBMC cannot derive it from the unwound arithmetic. The harnesses use the tight decode() precondition (the functions are internal and only ever receive a decode() result for a real f64), which is what makes that assume sound. All added annotations are cfg(kani) verification-only and compile out of normal builds. Harnesses require -C debug-assertions=off. Signed-off-by: Onyeka Obi <softwareengineerasaservant@isurvivable.cv>
1 parent e9f0a27 commit e4e3297

5 files changed

Lines changed: 625 additions & 0 deletions

File tree

library/core/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@
7575
#![no_core]
7676
#![rustc_coherence_is_core]
7777
#![rustc_preserve_ub_checks]
78+
// Verification-only (Kani): the flt2dec dragon_verify_stub harness stacks many
79+
// #[kani::stub] attributes whose macro expansion exceeds the default limit.
80+
#![cfg_attr(kani, recursion_limit = "1024")]
7881
//
7982
// Lints:
8083
#![deny(rust_2021_incompatible_or_patterns)]

library/core/src/num/bignum.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,24 @@ macro_rules! define_bignum {
108108
$name { size: sz, base }
109109
}
110110

111+
/// A nondeterministic but structurally valid bignum, for use as a
112+
/// sound over-approximating stub of the expensive arithmetic methods
113+
/// during Kani verification. Upholds the representation invariant
114+
/// (`size in [1, n]`, `base[size..] == 0`) so callers that read the
115+
/// digits never observe an inconsistent state.
116+
#[cfg(kani)]
117+
pub fn kani_any() -> $name {
118+
let size: usize = crate::kani::any();
119+
crate::kani::assume(size >= 1 && size <= $n);
120+
let mut base = [0; $n];
121+
let mut i = 0;
122+
while i < size {
123+
base[i] = crate::kani::any();
124+
i += 1;
125+
}
126+
$name { size, base }
127+
}
128+
111129
/// Returns the internal digits as a slice `[a, b, c, ...]` such that the numeric
112130
/// value is `a + b * 2^W + c * 2^(2W) + ...` where `W` is the number of bits in
113131
/// the digit type.

library/core/src/num/flt2dec/mod.rs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,3 +666,172 @@ where
666666
}
667667
}
668668
}
669+
670+
#[cfg(kani)]
671+
#[unstable(feature = "kani", issue = "none")]
672+
pub mod flt2dec_verify {
673+
use super::*;
674+
use crate::kani;
675+
676+
// A small fixed digit-buffer length keeps the proofs tractable. The
677+
// `assume_init` safety obligations in these functions depend only on control
678+
// flow driven by `buf.len()`, `exp`, and the digit-count arguments; every
679+
// branch (and therefore every distinct set of initialized `parts`) is still
680+
// reachable at this length, so a fixed length loses no path coverage.
681+
const PROOF_BUFLEN: usize = 4;
682+
683+
// `digits_to_dec_str` writes 2, 3, or 4 `parts` depending on `exp` and
684+
// `frac_digits`, then `assume_init_ref`s exactly the prefix it wrote. Kani
685+
// checks that no uninitialized `Part` is ever read and that no UB occurs.
686+
#[kani::proof]
687+
fn check_digits_to_dec_str() {
688+
let buf: [u8; PROOF_BUFLEN] = kani::any();
689+
kani::assume(buf[0] > b'0');
690+
let exp: i16 = kani::any();
691+
let frac_digits: usize = kani::any();
692+
let mut parts: [MaybeUninit<Part<'_>>; 4] = [const { MaybeUninit::uninit() }; 4];
693+
let _ = digits_to_dec_str(&buf, exp, frac_digits, &mut parts);
694+
}
695+
696+
// `digits_to_exp_str` writes a variable prefix of up to 6 `parts` and
697+
// `assume_init_ref`s `parts[..n + 2]` for the `n` it actually wrote.
698+
#[kani::proof]
699+
fn check_digits_to_exp_str() {
700+
let buf: [u8; PROOF_BUFLEN] = kani::any();
701+
kani::assume(buf[0] > b'0');
702+
let exp: i16 = kani::any();
703+
let min_ndigits: usize = kani::any();
704+
let upper: bool = kani::any();
705+
let mut parts: [MaybeUninit<Part<'_>>; 6] = [const { MaybeUninit::uninit() }; 6];
706+
let _ = digits_to_exp_str(&buf, exp, min_ndigits, upper, &mut parts);
707+
}
708+
709+
// An arbitrary sign-formatting option.
710+
fn any_sign() -> Sign {
711+
if kani::any() { Sign::Minus } else { Sign::MinusPlus }
712+
}
713+
714+
// A stub digit generator standing in for `grisu`/`dragon` `format_shortest`.
715+
// It writes one arbitrary nonzero digit into the scratch buffer and returns
716+
// it with an arbitrary exponent. This isolates the `to_shortest_*`
717+
// functions' own `unsafe` (the `assume_init` on `parts` and the delegation
718+
// to the already-verified `digits_to_*_str`) from the loopy strategy code,
719+
// which is verified separately. A generic `fn` is required here rather than
720+
// a closure so it satisfies the higher-ranked lifetime in the `F` bound.
721+
fn stub_shortest<'a>(_d: &Decoded, buf: &'a mut [MaybeUninit<u8>]) -> (&'a [u8], i16) {
722+
let digit: u8 = kani::any();
723+
kani::assume(digit > b'0');
724+
buf[0] = MaybeUninit::new(digit);
725+
let exp: i16 = kani::any();
726+
// SAFETY: we just initialized the element `..1`.
727+
(unsafe { buf[..1].assume_init_ref() }, exp)
728+
}
729+
730+
// `to_shortest_str` handles NaN/Inf/Zero by writing `parts[..1]` and the
731+
// finite case by delegating to `digits_to_dec_str`. An arbitrary `f64`
732+
// reaches every `FullDecoded` arm.
733+
#[kani::proof]
734+
fn check_to_shortest_str() {
735+
let v: f64 = kani::any();
736+
let sign = any_sign();
737+
let frac_digits: usize = kani::any();
738+
let mut buf: [MaybeUninit<u8>; MAX_SIG_DIGITS] =
739+
[const { MaybeUninit::uninit() }; MAX_SIG_DIGITS];
740+
let mut parts: [MaybeUninit<Part<'_>>; 4] = [const { MaybeUninit::uninit() }; 4];
741+
let _ = to_shortest_str(stub_shortest, v, sign, frac_digits, &mut buf, &mut parts);
742+
}
743+
744+
// `to_shortest_exp_str` is the exponential-form analogue; its finite arm
745+
// delegates to `digits_to_dec_str` or `digits_to_exp_str` per `dec_bounds`.
746+
#[kani::proof]
747+
fn check_to_shortest_exp_str() {
748+
let v: f64 = kani::any();
749+
let sign = any_sign();
750+
let lo: i16 = kani::any();
751+
let hi: i16 = kani::any();
752+
kani::assume(lo <= hi);
753+
let upper: bool = kani::any();
754+
let mut buf: [MaybeUninit<u8>; MAX_SIG_DIGITS] =
755+
[const { MaybeUninit::uninit() }; MAX_SIG_DIGITS];
756+
let mut parts: [MaybeUninit<Part<'_>>; 6] = [const { MaybeUninit::uninit() }; 6];
757+
let _ = to_shortest_exp_str(stub_shortest, v, sign, (lo, hi), upper, &mut buf, &mut parts);
758+
}
759+
760+
// For `f64`, `decode` bottoms out at `decoded.exp == -1076` (normal-min,
761+
// which subtracts 2 from `integer_decode`'s minimum of `-1074`), where
762+
// `estimate_max_buf_len` returns 828. 1024 (the size the real `fmt` callers
763+
// use) covers every reachable decoded exponent for the
764+
// `buf.len() >= maxlen` assertions in both `to_exact_*` functions.
765+
const PROOF_EXACT_BUFLEN: usize = 1024;
766+
767+
// Stub `format_exact` for `to_exact_exp_str`, which always passes the result
768+
// to `digits_to_exp_str` (it calls the generator with `limit = i16::MIN`, so
769+
// the real one never returns an empty buffer here). Returns one nonzero
770+
// digit with an arbitrary exponent.
771+
fn stub_exact_full<'a>(
772+
_d: &Decoded,
773+
buf: &'a mut [MaybeUninit<u8>],
774+
_limit: i16,
775+
) -> (&'a [u8], i16) {
776+
let digit: u8 = kani::any();
777+
kani::assume(digit > b'0');
778+
buf[0] = MaybeUninit::new(digit);
779+
let exp: i16 = kani::any();
780+
// SAFETY: we just initialized the element `..1`.
781+
(unsafe { buf[..1].assume_init_ref() }, exp)
782+
}
783+
784+
// Stub `format_exact` for `to_exact_fixed_str`, which branches on
785+
// `exp <= limit`. That arm requires an empty result (the source
786+
// `debug_assert_eq!`s `buf.len() == 0`); the other arm needs a valid nonzero
787+
// digit with `exp > limit`. Couple the result to `limit` so both caller
788+
// arms are exercised soundly.
789+
fn stub_exact_limited<'a>(
790+
_d: &Decoded,
791+
buf: &'a mut [MaybeUninit<u8>],
792+
limit: i16,
793+
) -> (&'a [u8], i16) {
794+
if kani::any() {
795+
let exp: i16 = kani::any();
796+
kani::assume(exp <= limit);
797+
// SAFETY: an empty prefix is trivially initialized.
798+
(unsafe { buf[..0].assume_init_ref() }, exp)
799+
} else {
800+
let digit: u8 = kani::any();
801+
kani::assume(digit > b'0');
802+
buf[0] = MaybeUninit::new(digit);
803+
let exp: i16 = kani::any();
804+
kani::assume(exp > limit);
805+
// SAFETY: we just initialized the element `..1`.
806+
(unsafe { buf[..1].assume_init_ref() }, exp)
807+
}
808+
}
809+
810+
// `to_exact_exp_str` writes `parts[..1]` for NaN/Inf, `parts[..3]`/`parts[..1]`
811+
// for zero, and delegates to `digits_to_exp_str` for finite values.
812+
#[kani::proof]
813+
fn check_to_exact_exp_str() {
814+
let v: f64 = kani::any();
815+
let sign = any_sign();
816+
let ndigits: usize = kani::any();
817+
kani::assume(ndigits > 0);
818+
let upper: bool = kani::any();
819+
let mut buf: [MaybeUninit<u8>; PROOF_EXACT_BUFLEN] =
820+
[const { MaybeUninit::uninit() }; PROOF_EXACT_BUFLEN];
821+
let mut parts: [MaybeUninit<Part<'_>>; 6] = [const { MaybeUninit::uninit() }; 6];
822+
let _ = to_exact_exp_str(stub_exact_full, v, sign, ndigits, upper, &mut buf, &mut parts);
823+
}
824+
825+
// `to_exact_fixed_str` additionally has a finite sub-branch (`exp <= limit`)
826+
// that renders like zero; `stub_exact_limited` reaches both sub-branches.
827+
#[kani::proof]
828+
fn check_to_exact_fixed_str() {
829+
let v: f64 = kani::any();
830+
let sign = any_sign();
831+
let frac_digits: usize = kani::any();
832+
let mut buf: [MaybeUninit<u8>; PROOF_EXACT_BUFLEN] =
833+
[const { MaybeUninit::uninit() }; PROOF_EXACT_BUFLEN];
834+
let mut parts: [MaybeUninit<Part<'_>>; 4] = [const { MaybeUninit::uninit() }; 4];
835+
let _ = to_exact_fixed_str(stub_exact_limited, v, sign, frac_digits, &mut buf, &mut parts);
836+
}
837+
}

library/core/src/num/flt2dec/strategy/dragon.rs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,21 @@ pub fn format_shortest<'a>(
181181
let mut up;
182182
let mut i = 0;
183183
loop {
184+
// VERIFICATION (Kani, compiles out otherwise): the Dragon/Loitsch
185+
// digit-count theorem (Burger & Dybvig 1996, Fig 3; Loitsch, PLDI'10): a
186+
// 53-bit-precision f64 has a shortest decimal of at most
187+
// `ceil(53*log10 2) + 1 = 17 = MAX_SIG_DIGITS` significant digits. Every
188+
// `Decoded` reaching this function comes from `decode()` on a real f64
189+
// (the only caller is `format_shortest`), so the digit index `i` never
190+
// reaches 17. This bounds the DIGIT COUNT (an input-precision property);
191+
// buffer safety `i < buf.len()` follows from the separate
192+
// `assert!(buf.len() >= MAX_SIG_DIGITS)` above. The loop break depends on
193+
// the `Big` comparison `mant < minus || scale < mant+plus`, a
194+
// number-theoretic termination fact CBMC cannot derive from the
195+
// (stubbed/havoced) bignum arithmetic, so it is cited.
196+
#[cfg(kani)]
197+
crate::kani::assume(i < MAX_SIG_DIGITS);
198+
184199
// invariants, where `d[0..n-1]` are digits generated so far:
185200
// - `v = mant / scale * 10^(k-n-1) + d[0..n-1] * 10^(k-n)`
186201
// - `v - low = minus / scale * 10^(k-n-1)`
@@ -248,6 +263,13 @@ pub fn format_shortest<'a>(
248263
// but we are just being safe and consistent here.
249264
// SAFETY: we initialized that memory above.
250265
if let Some(c) = round_up(unsafe { buf[..i].assume_init_mut() }) {
266+
// VERIFICATION (Kani, compiles out otherwise): the digit-count theorem
267+
// bounds the TOTAL significant digits (the `i` generated in the loop
268+
// plus this round-up carry) to <= MAX_SIG_DIGITS, so this carry write
269+
// is in bounds (`i < MAX_SIG_DIGITS <= buf.len()`). Same cited
270+
// Dragon/Loitsch bound as the loop assume above.
271+
#[cfg(kani)]
272+
crate::kani::assume(i < MAX_SIG_DIGITS);
251273
buf[i] = MaybeUninit::new(c);
252274
i += 1;
253275
k += 1;
@@ -387,3 +409,146 @@ pub fn format_exact<'a>(
387409
// SAFETY: we initialized that memory above.
388410
(unsafe { buf[..len].assume_init_ref() }, k)
389411
}
412+
413+
#[cfg(kani)]
414+
#[unstable(feature = "kani", issue = "none")]
415+
pub mod dragon_verify {
416+
use super::*;
417+
use crate::kani;
418+
419+
// Buffer safety holds for any bignum values (the digit loop is `for i in 0..len`,
420+
// `len <= buf.len()`). But `debug_assert!(d < 10)` and the `mant <= scale*10`
421+
// loop invariant depend on the REAL scaling arithmetic: havocing `Big` ops
422+
// breaks `scale8 > scale4 > scale2 > scale` and `mant <= scale*10`, so pure
423+
// stubbing produces spurious `d >= 10` failures (a digit-correctness check, not
424+
// a memory-safety one). So this is verified with FULL concrete arithmetic;
425+
// it is memory-light (~1.3GB) but compute-slow. See the `kani_any` havoc helper
426+
// in `num/bignum.rs` for the abstraction that would work given a loop contract
427+
// that re-establishes `mant <= scale*10`.
428+
#[kani::proof]
429+
#[kani::unwind(50)]
430+
fn check_format_exact() {
431+
let mant: u64 = kani::any();
432+
kani::assume(mant > 0 && mant < (1 << 61));
433+
let exp: i16 = kani::any();
434+
kani::assume(exp >= -1076 && exp <= 971);
435+
let d = Decoded { mant, minus: 1, plus: 1, exp, inclusive: kani::any() };
436+
let limit: i16 = kani::any();
437+
let mut buf: [MaybeUninit<u8>; 4] = [const { MaybeUninit::uninit() }; 4];
438+
let _ = format_exact(&d, &mut buf, limit);
439+
}
440+
}
441+
442+
// Buffer-safety-only proof of format_exact via COMPLETE bignum stubbing.
443+
// Hypothesis: with debug-assertions OFF (so `debug_assert!(d < 10)` is dead, like
444+
// the VeriFast frontend) AND every Big op havoc-stubbed (incl. is_zero/cmp, which
445+
// a partial stub left concrete and bit-blasting), format_exact's only obligations
446+
// are the explicit `for i in 0..len` (len <= buf.len()) bound + the assume_init
447+
// init tracking -- pure control flow, no arithmetic. Run with
448+
// RUSTFLAGS="-C debug-assertions=off".
449+
#[cfg(kani)]
450+
#[unstable(feature = "kani", issue = "none")]
451+
pub mod dragon_verify_stub {
452+
use super::*;
453+
use crate::kani;
454+
455+
// Mutating ops: NO-OP stubs. The Big *values* are irrelevant to buffer
456+
// safety, and all value inspection (is_zero/cmp) is independently stubbed, so
457+
// leaving the Big unchanged is sound and cheap (no symbolic state injected).
458+
fn s_mul_pow2(s: &mut Big, _bits: usize) -> &mut Big {
459+
s
460+
}
461+
fn s_mul_small(s: &mut Big, _o: Digit) -> &mut Big {
462+
s
463+
}
464+
fn s_sub<'a>(s: &'a mut Big, _o: &Big) -> &'a mut Big {
465+
s
466+
}
467+
fn s_add<'a>(s: &'a mut Big, _o: &Big) -> &'a mut Big {
468+
s
469+
}
470+
fn s_mul_digits<'a>(s: &'a mut Big, _o: &[Digit]) -> &'a mut Big {
471+
s
472+
}
473+
fn s_mul_pow10<'a>(s: &'a mut Big, _n: usize) -> &'a mut Big {
474+
s
475+
}
476+
fn s_div_2pow10<'a>(s: &'a mut Big, _n: usize) -> &'a mut Big {
477+
s
478+
}
479+
// Value inspection: drives control flow nondeterministically.
480+
fn s_is_zero(_s: &Big) -> bool {
481+
kani::any()
482+
}
483+
fn s_cmp(_s: &Big, _o: &Big) -> crate::cmp::Ordering {
484+
let x: u8 = kani::any();
485+
match x % 3 {
486+
0 => crate::cmp::Ordering::Less,
487+
1 => crate::cmp::Ordering::Equal,
488+
_ => crate::cmp::Ordering::Greater,
489+
}
490+
}
491+
fn s_estimate(_m: u64, _e: i16) -> i16 {
492+
let k: i16 = kani::any();
493+
kani::assume(k > -400 && k < 400);
494+
k
495+
}
496+
497+
#[kani::proof]
498+
#[kani::unwind(6)]
499+
#[kani::stub(Big::mul_pow2, s_mul_pow2)]
500+
#[kani::stub(Big::mul_small, s_mul_small)]
501+
#[kani::stub(Big::sub, s_sub)]
502+
#[kani::stub(Big::add, s_add)]
503+
#[kani::stub(Big::mul_digits, s_mul_digits)]
504+
#[kani::stub(Big::is_zero, s_is_zero)]
505+
#[kani::stub(Big::cmp, s_cmp)]
506+
#[kani::stub(mul_pow10, s_mul_pow10)]
507+
#[kani::stub(div_2pow10, s_div_2pow10)]
508+
#[kani::stub(estimate_scaling_factor, s_estimate)]
509+
fn check_format_exact_stub() {
510+
let mant: u64 = kani::any();
511+
kani::assume(mant > 0 && mant < (1 << 61));
512+
let exp: i16 = kani::any();
513+
kani::assume(exp >= -1076 && exp <= 971);
514+
let d = Decoded { mant, minus: 1, plus: 1, exp, inclusive: kani::any() };
515+
let limit: i16 = kani::any();
516+
let mut buf: [MaybeUninit<u8>; 4] = [const { MaybeUninit::uninit() }; 4];
517+
let _ = format_exact(&d, &mut buf, limit);
518+
}
519+
520+
// Tight decode() precondition for f64 (decoder.rs: minus is always 1, plus is
521+
// 1 or 2, mant is the shifted f64 mantissa so mant <= 2^54). format_shortest
522+
// is internal and only called on a decode() result, so this is its true
523+
// precondition; under it the Dragon/Loitsch digit-count theorem holds.
524+
fn arbitrary_decoded_tight() -> Decoded {
525+
let mant: u64 = kani::any();
526+
kani::assume(mant >= 2 && mant <= (1u64 << 54));
527+
let plus: u64 = kani::any();
528+
kani::assume(plus == 1 || plus == 2);
529+
let exp: i16 = kani::any();
530+
kani::assume(exp >= -1076 && exp <= 971);
531+
Decoded { mant, minus: 1, plus, exp, inclusive: kani::any() }
532+
}
533+
534+
// Buffer-safety proof of format_shortest: complete bignum no-op stubs (Big
535+
// values are irrelevant to buffer safety; `cmp` is nondeterministic so all
536+
// control-flow paths are explored), the tight decode precondition, and the
537+
// in-loop digit-count assume bound the implicit loop. CBMC unrolls (no loop
538+
// contracts).
539+
#[kani::proof]
540+
#[kani::unwind(19)]
541+
#[kani::stub(Big::mul_pow2, s_mul_pow2)]
542+
#[kani::stub(Big::mul_small, s_mul_small)]
543+
#[kani::stub(Big::sub, s_sub)]
544+
#[kani::stub(Big::add, s_add)]
545+
#[kani::stub(Big::cmp, s_cmp)]
546+
#[kani::stub(mul_pow10, s_mul_pow10)]
547+
#[kani::stub(estimate_scaling_factor, s_estimate)]
548+
fn check_format_shortest_stub() {
549+
let d = arbitrary_decoded_tight();
550+
let mut buf: [MaybeUninit<u8>; MAX_SIG_DIGITS] =
551+
[const { MaybeUninit::uninit() }; MAX_SIG_DIGITS];
552+
let _ = format_shortest(&d, &mut buf);
553+
}
554+
}

0 commit comments

Comments
 (0)