Skip to content

Challenge 29: Verify the safety of Box and ThinBox in alloc::boxed - #669

Open
kasimte wants to merge 1 commit into
model-checking:mainfrom
kasimte:challenge-29
Open

kasimte wants to merge 1 commit into
model-checking:mainfrom
kasimte:challenge-29

Conversation

@kasimte

@kasimte kasimte commented Sep 2, 2026 •

Copy link
Copy Markdown

Towards #526. Solves Challenge 29: Safety of boxed.

Summary

Success criterion Status
Safety contracts and verification on the 9 unsafe functions 9/9. Eight via proof_for_contract; the scalar assume_init by construction (const-fn limit; see Scope).
At least 75% of the 46 safe functions verified 46/46 that still exist. Box::into_unique was removed from std upstream, so its harness is dropped.
Generic T at primitive types, Global allocator Yes; the challenge permits primitive instantiations.

67 Kani harnesses, one mod verify per file, all passing at the current toolchain pin via scripts/run-kani.sh; purely additive across three files (boxed.rs, boxed/convert.rs, boxed/thin.rs). Counts: 10 proof_for_contract, 108 kani::cover, 6 should_panic, 0 kani::assume. Two Kani limitations this work surfaced were addressed upstream and authored here (kani#4778 merged, kani#4985 filed; see Upstream Kani contributions).

Kani's check classes cover the challenge's UB list where a check exists: dangling or misaligned access via the pointer_dereference class, intrinsic misuse via the arithmetic-overflow and pointer-offset checks, invalid-value production via the typed-read checks. "Mutating immutable bytes" has no Kani check class (repo-wide tool scope, surfaced on #582; committee policy pending).

The 9 unsafe functions

The four raw-pointer constructors (from_raw, from_non_null, and their _in variants) verify with proof_for_contract at a sized instantiation, plus an unsized [u8] instantiation for the _in pair. The slice assume_init verifies the same way; its target is spelled through the impl's generic parameters (Box::<[MaybeUninit<T>], A>::assume_init), since a concrete turbofish does not resolve against the impl's structured self type but the generic-parameter form does.

The three downcast_uncheckeds on Box<dyn Any (+ Send)(+ Sync), A> verify with proof_for_contract, the target again spelled through each impl's generic parameters (resolver fix in the Upstream note). Each #[requires] fixes the erased type with is::<T>(); each #[ensures] asserts pointer identity across the cast.

The scalar assume_init verifies by construction. The current toolchain makes it a const fn, and a contract on a const fn with an owned self fails const-evaluation (E0493) at this pin. Its harness check_assume_init_u32 runs the real body on a symbolic payload and asserts the postcondition (value read-back and pointer identity), with an in-code note. It reverts to proof_for_contract once the fix lands and the pin bumps.

The challenge table lists <dyn Error>::downcast_unchecked three times, but no such method exists: impl dyn Error exposes only the safe downcast. The three real downcast_uncheckeds are on Box<dyn Any (+ Send)(+ Sync), A>, contracted here.

Upstream Kani contributions

Both authored here:

  • The resolver could not match a proof_for_contract target whose impl block lives outside the type's module (the three downcast_uncheckeds). Reported in kani#4777, fixed in kani#4778 (merged 2026-09-24, in this pin). This is what lets the three verify as proof_for_contract.
  • A contract on a const fn with an owned self fails const-evaluation (E0493), which blocks proof_for_contract on the scalar assume_init. Reported in kani#4984; the fix is kani#4985 (filed).

The 46 safe functions

Most are heap round-trips (allocate, write, hand out a pointer, reconstruct); the harness checks that the value and its allocation return intact. A few carry a property verified on its own terms, and the ThinBox/WithHeader family is new work with no Box analog.

Group What the harnesses establish
Constructors and ownership handoffs (new_in/try_new_*, write, into_boxed_slice, into_raw/into_non_null/leak, into_pin) the value and its allocation survive the round-trip
Slice constructors the same, at a symbolic length bounded only by what Layout::array accepts
Conversions (into_array, from_slice, From<&str>, From<Box<str>>, both TryFroms) content preserved, and where a length can mismatch both arms are asserted. into_array returns Result; both arms asserted. The spec's TryFrom<Box<T>> row has no impl in the tree, so the two real TryFrom sources stand in for it.
downcast on Box<dyn Any…> and dyn Error… the success and failure arms, with pointer identity on success
Drop, Default, Clone including Box<str> clone: a fresh allocation for a non-empty string, the shared dangling pointer for an empty one
ThinBox/WithHeader the full family; new_unsize_zst proven with no assumptions on a slice-metadata instantiation

Two habits keep the proofs non-vacuous. Each input-bearing harness carries a kani::cover confirming it reaches the operation under test, and both arms are covered where a function has two reachable outcomes. No harness constrains inputs with kani::assume; every restriction is a visible any_where domain. Panics are proven, not assumed away: should_panic harnesses send a panicking-drop sentinel through Box<T> and Box<[T]> drop glue, and drive all four non-try slice constructors past isize::MAX into the capacity-overflow guard.

Scope: what this pin cannot reach

Three behaviors sit outside the model here, each noted in-code. Allocation never fails in Kani, so the Err arm of every try_new* is unreachable; those harnesses verify the success arm and mark the dead branch. new_unsize_zst's dyn Any form fails inside its const-allocated metadata block (a missing drop_in_place::<dyn Any> and pointer-liveness checks on the const pointer), so that function is proven on its slice-metadata route. The last WithHeader layout-overflow guard is reachable only by a near-isize::MAX type, so no harness reaches it.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.

@kasimte

kasimte commented Sep 12, 2026

Copy link
Copy Markdown
Author

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 unsafe fns carry contracts, and the safe functions are well past the challenge's 75% bar. It also surfaced (and I filed a fix for) a Kani resolver limitation that blocks proof_for_contract on these trait-object methods (kani#4778). Happy to rebase to clear the merge conflict whenever that's useful. Thanks!

@feliperodri feliperodri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@kasimte

kasimte commented Sep 15, 2026 •

Copy link
Copy Markdown
Author

Thanks for the detailed review, @feliperodri. On your two notes:

  1. The resolver fix is Resolve multi-candidate methods on impls defined outside the type's module kani#4778, linking it here as you suggested; once it merges, the three downcast_unchecked proofs become real proof_for_contracts instead of by-construction checks.
  2. I'll trim the width-duplicate harnesses to tighten the diff and rebase onto current main so the suite verifies at the current pin, in a follow-up.

@kasimte

kasimte commented Sep 16, 2026 •

Copy link
Copy Markdown
Author

@feliperodri the width-duplicate trim from your second note is in:

  • 74 harnesses down to 68, dropping six per-width variants whose proofs were identical modulo the type token. Coverage is unchanged (9/9 unsafe, 46/46 safe) and the change stays purely additive (+1124, no deletions).
  • I kept both ThinBox new/deref variants, since the u8 and u64 cases drive different header-offset arithmetic (align 1 vs align 8) and so aren't true duplicates. Also rebased onto current main; the suite verifies at the pinned Kani 0.67.0 / CBMC 6.10.0, 68/68 locally.

@kasimte
kasimte requested a review from feliperodri September 16, 2026 00:24
@kasimte

kasimte commented Sep 24, 2026

Copy link
Copy Markdown
Author

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.

Thanks for merging it. Done on the Kani side (model-checking/kani#4778). Converting the three here needs this repo's pinned Kani (tool_config/kani-version.toml) to include it, which rides a shared toolchain bump; I'll flip them to real proof_for_contract and re-verify as soon as the pin picks it up.

The width-duplicate trim from (2) already went in (74→68 harnesses).

feliperodri added a commit to ivmat/kani that referenced this pull request Sep 24, 2026
…odule (model-checking#4778)

Resolves model-checking#4777.

`last_two_items_of_path_match` compares the user's turbofish against
`def_path_str`'s rendering of each candidate. Impls living outside their
type's home module render as `<impl Type<Args>>`, which no
user-spellable path can equal — so multi-candidate methods in that
position (e.g. the three `Box<dyn Any(+Send)(+Sync),
A>::downcast_unchecked` impls in alloc's `boxed/convert.rs`) fail to
resolve under every spelling, while single-candidate methods skip
refinement and work.

This is the out-of-module half of model-checking#3773 (whose fix and
`multiple_inherent_impls.rs` test cover the same-module rendering). It
adds a fallback that unwraps the `<impl SELF_TYPE>` form, extracts
SELF_TYPE's generic arguments, strips the disambiguation parentheses
`def_path_str` adds around trait-object bounds (tuple-type parentheses
are semantic and preserved), and retries the comparison.

Scope: this keeps the existing string-comparison approach and only
unblocks the out-of-module rendering; the milder same-module symptom
(concrete turbofish arguments not matching a structured self-type such
as `MaybeUninit<u32>` vs the impl's `MaybeUninit<T>` — the
generic-parameter spelling works there) is unchanged, and a
semantic-resolution rewrite would subsume both. Two smaller spelling
asymmetries are also known and left to that same follow-up: a
trait-object bound nested inside another generic argument still requires
def_path_str's parenthesized spelling (normalization is not recursive),
and same-module dyn-argument candidates still require it too (the
primary comparison does no paren normalization). Happy to take direction
if the deeper rewrite is preferred.

Tests: eight unit tests in the existing
`simple_last_two_items_of_path_match` module (dyn-args match + mismatch,
dyn in second argument position, tuple parens preserved, non-generic
no-fallback, fn-pointer renderings decline cleanly, arrow-bearing lists
skip normalization, spaced turbofish matches on the primary path) and a
new end-to-end regression test
`tests/kani/FunctionContracts/cross_module_multiple_impls.rs` with tuple
and trait-object candidate pairs (fails to resolve without the fix,
verifies with it; the dyn pair fails if the paren-strip is disabled).
Also validated on the motivating case: all three `Box<dyn
Any…>::downcast_unchecked` contracts in verify-rust-std resolve and
verify as `proof_for_contract` targets under the patched resolver.

Applies cleanly on current `main` (`last_two_items_of_path_match` is
unchanged there). One call-out: candidates whose rendered path contains
`->` (fn-pointer / `Fn`-sugar types) are not unwrapped by the fallback —
the top-level `::` split already leaves such paths unmatchable, and the
helpers additionally decline anything their bracket counting cannot
parse — so these keep the existing failed-to-resolve behavior rather
than matching a different candidate (unit-tested).

Motivating case: model-checking/verify-rust-std#669 — the three `Box<dyn
Any…>::downcast_unchecked` contract targets there.

By submitting this pull request, I confirm that my contribution is made
under the terms of the Apache 2.0 and MIT licenses.

---------

Co-authored-by: Felipe R. Monteiro <felisous@amazon.com>
@kasimte

kasimte commented Oct 7, 2026 •

Copy link
Copy Markdown
Author

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 [...] — please link the kani PR here

@feliperodri: Following up here as requested above.

Done: Our kani#4778 is now in the repo's pin, so the three downcast_unchecked now verify via proof_for_contract rather than by construction. The PR is rebased onto the current pin and the suite verifies there.

One new wrinkle from the toolchain bump: Box::<MaybeUninit<T>>::assume_init is now a const fn, and a contract on a const fn with an owned self fails const-evaluation (E0493). That function is verified by construction for now (real body, symbolic payload, full postcondition asserted).

I reported it as kani#4984 and filed a fix in kani#4985, and it reverts to proof_for_contract once that lands and the pin bumps.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Challenge Used to tag a challenge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants