Challenge 29: Verify the safety of Box and ThinBox in alloc::boxed - #669
Challenge 29: Verify the safety of Box and ThinBox in alloc::boxed#669kasimte wants to merge 1 commit into
Conversation
|
Hi @feliperodri — thanks for working through the Challenge 29 reviews; I was wondering if #669 might have been missed? Would you be open to taking a look when you have a chance? It's a purely additive solution — all 9 |
feliperodri
left a comment
There was a problem hiding this comment.
Thanks @kasimte, and apologies — you were right that this was missed in the Challenge 29 round (it was created after the snapshot our triage list was built from). I've now reviewed it fully.
This is a genuinely strong, sound solution: 9/9 unsafe fns with contracts (6 via #[kani::proof_for_contract], 3 Box<dyn Any>::downcast_unchecked verified by-construction on symbolic payloads — the same Kani trait-object resolver limitation, which you've helpfully filed kani#4778 to fix) and 46/46 safe abstractions (~100%, slightly ahead of the other solution's 45/46). Clean on all our vacuity checks — no cfg body swaps, no trivial/loop invariants, no kani::assume anywhere (all restrictions are visible any_where domains), uncapped symbolic slice lengths, per-harness kani::cover non-vacuity witnesses, should_panic twins for the overflow/drop-glue-panic paths, and you even recorded a real Kani counterexample that corrected an over-strong !addr_eq claim. Excellent verification hygiene.
Requesting changes only on the prioritization: we reviewed all four open Challenge 29 solutions together and approved #639 as the accepted solution — it achieves the same 9/9 (6 proof_for_contract + 3 by-construction) + 45/46 coverage more concisely (+765 vs +1200 here), which matters for upstreamability. This PR is equally sound and marginally more complete, so we're keeping it as the strong alternative rather than displacing an already-approved equivalent. If #639 stalls, this is our fallback.
Two things that would make this the stronger of the two: (1) your kani#4778 fix landing so the 3 downcast_unchecked contracts become real proof_for_contracts (a genuine edge over #639) — please link the kani PR here; (2) trimming the width-duplicate harnesses to tighten the diff. Really appreciate the work and the upstream Kani fix.
Towards #526. Solves Challenge 29: Safety of boxed.
The challenge asks for safety contracts on 9
unsafefunctions and verification of at least 75% of the 46 safe functions; this covers all 9 and all 46, including theThinBox/WithHeaderfamily that has noBoxanalog. Generics are instantiated at primitive types, which the challenge allows, with theGlobalallocator. The work is 74 Kani harnesses in onemod verifyper file, all passing viascripts/run-kani.sh, as a purely additive change to three files. On every one, Kani also checks the challenge's four listed undefined behaviors — access through a dangling or misaligned pointer, misuse of intrinsics, mutation of immutable bytes, and production of an invalid value.The 9 unsafe functions
All nine carry safety contracts, and all nine are verified — six with
proof_for_contract, three by construction. The four raw-pointer constructors —from_raw,from_non_null, and their_invariants — are discharged at a sized instantiation and, for the_inpair, an unsized[u8]one. Bothassume_inits are discharged as well, with the target spelled through the impl's own generic parameters (Box::<MaybeUninit<T>, A>::assume_init): concrete turbofish arguments do not resolve against that impl's structured self-type, but the generic-parameter form does.The remaining three — the
downcast_uncheckeds — are verified by running the real function body on symbolic inputs and asserting the whole postcondition, the returned value and its pointer identity: at this repository's Kani pin,proof_for_contractresolves none of the three same-nameddyn-self impls, across five path spellings including the resolver's own printed implementation forms. Their contracts are annotated and checked in the harness rather than machine-linked, with an in-code note at each site. The root cause is that those impl blocks live in a different module thanBox, so the resolver renders them in an<impl …>path form no spelling can match; a resolver fix developed alongside this work (model-checking/kani#4777, fix in model-checking/kani#4778) verifies all three asproof_for_contracttargets under a patched Kani, and each note marks the mechanical upgrade for a pin that includes it.The challenge's function table also lists
<dyn Error>::downcast_uncheckedthree times, but no such method exists:impl dyn Errorexposes only the safedowncast. The three realdowncast_uncheckeds are onBox<dyn Any (+ Send)(+ Sync), A>, and those are the ones contracted here.The 46 safe functions
Most are heap round-trips — allocate, write, hand out a pointer, reconstruct — where the harness checks that the value and its allocation come back intact. A few carry a property worth verifying on its own terms, and the
ThinBox/WithHeaderfamily is the genuinely new work.new_in/try_new_*,write,into_boxed_slice,into_raw/into_non_null/into_unique/leak,into_pinLayout::arraywill acceptinto_array,from_slice,From<&str>,From<Box<str>>, and bothTryFromsTryFrom<Box<T>>row has no impl in the tree, so the two realTryFromsources stand in for itdowncastonBox<dyn Any…>anddyn Error…Drop,Default,CloneBox<str>clone, which asserts a fresh allocation for a non-empty string and the shared dangling pointer for an empty oneThinBox/WithHeadernew_unsize_zstis proven with no assumptions on a slice-metadata instantiationTwo habits keep the proofs honest. First, covers: each input-bearing harness carries a
kani::coverconfirming it reaches the operation under test rather than passing on a setup that silently failed, and where a function has two reachable outcomes, both arms are covered — so no proof passes while checking a dead or unreachable path. No harness constrains its inputs withkani::assume; every input restriction is a visibleany_wheredomain. Second, panics are proven rather than assumed away:should_panicharnesses send a panicking-drop sentinel throughBox<T>andBox<[T]>drop glue, and drive all four non-tryslice constructors pastisize::MAXinto their capacity-overflow guard.What this Kani pin can't reach
Two behaviors sit outside the model here, both noted in-code where they occur. Allocation never fails in Kani, so the
Errarm of everytry_new*is unreachable — those harnesses verify the success arm and mark the dead branch rather than covering it. Andnew_unsize_zst'sdyn Anyform fails inside itsconst-allocated metadata block, on a missingdrop_in_place::<dyn Any>and pointer-liveness checks on the const pointer, so that function is proven on its slice-metadata route instead. The last overflow guard, inWithHeaderlayout arithmetic, is reachable only by a near-isize::MAXtype and is covered through the slice constructors above.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.