Skip to content

Commit 7c36edd

Browse files
committed
Verify Boolean and NonZeroUsize candidate predicates
Use extracted value predicates in the production TryFromBytes validators for bool and NonZeroUsize. Keep Boolean candidates as bytes until validation, so invalid encodings remain expressible in the model. Prove scalar and pair conversions and a terminating Boolean-slice validator, including rejected inputs. Reuse the production predicates and compare the concrete candidates with public conversions, exhaustively for Boolean bytes and pairs. Register a narrow, source-bound byte read that preserves bounds failure. Check its complete interpretation and reject an out-of-range-read mutant. Reading through the generic Maybe pointer abstraction remains a separate Rust safety premise. gherrit-pr-id: G7q2xz2bs5ln4dfgckfspk2us4dhjperw Agent-authored-by: AI agent acting on joshlf's behalf
1 parent 12e57ce commit 7c36edd

15 files changed

Lines changed: 605 additions & 15 deletions

File tree

‎verification/aeneas/ADMISSION_DESIGN.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ The local offset copy avoids admitting its interpretation, without modifying the
6565
pinned toolchain. A future builtin replacement needs an independent law for the
6666
reconstructed destination, including unchanged length and prefix preservation.
6767

68+
`util::validity::read_byte` reads one initialized byte with a bounds check. It
69+
returns `.fail .panic` out of bounds; it never constructs the type whose bit
70+
validity is being tested. Its exact source body and complete model are checked.
71+
This narrow boundary avoids importing the generic indexing dictionary's unused
72+
pointer operations. It supplies no premise about arbitrary raw-pointer reads,
73+
uninitialized storage, or padding. Boolean and nonzero-word predicates are
74+
extracted from the helpers used by the production `TryFromBytes` implementations.
75+
The checked scalar/pair conversions and slice validator retain all raw integer
76+
and byte inputs, including rejected encodings. Native regression tests compare
77+
them with the public conversions; the generic `Maybe` pointer path still relies
78+
on its existing Rust safety arguments.
79+
6880
The report lives in the existing `bindings.json`. It includes both AST digests,
6981
root coverage, visited executions, call boundaries, registry digest, source
7082
identity, Cargo configuration hashes, and patched-runtime provenance. Compiler

‎verification/aeneas/external.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
{"rust": "zerocopy::util::transmute_unchecked", "signature": {"unsafe": true, "inputs": ["T0"], "output": "T1", "types": 2, "consts": 0}, "lean": "util.transmute_unchecked", "opaque": true, "source_body_sha256": "58ee51c915af14bac8c12d66d5ac65feadf59742212c869c7f45f57f847d48b6"},
2525
{"rust": "zerocopy::util::copy_unchecked", "signature": {"unsafe": true, "inputs": ["&[u8]", "&mut [u8]"], "output": "tuple0", "types": 0, "consts": 0}, "lean": "util.copy_unchecked", "opaque": true, "source_body_sha256": "e3529644308d0a3ce1a407d163f98676faa36a286ba4a85221b894930d54fa20"},
2626
{"rust": "zerocopy::util::copy_unchecked_at", "signature": {"unsafe": true, "inputs": ["&[u8]", "&mut [u8]", "usize"], "output": "tuple0", "types": 0, "consts": 0}, "lean": "util.copy_unchecked_at", "opaque": true, "source_body_sha256": "968dea5ce0ba53c6d5922cb87b936d5ed58e7e81dce4bc36459d7a42ae251569"},
27+
{"rust": "zerocopy::util::validity::read_byte", "signature": {"unsafe": false, "inputs": ["&[u8]", "usize"], "output": "u8", "types": 0, "consts": 0}, "lean": "util.validity.read_byte", "opaque": true, "source_body_sha256": "35941f898b8ec4684e6f36fefab11d0804ad0c9baa4f151becfaa20ad862976b"},
2728
{"rust": "core::num::{u16}::from_be_bytes", "signature": {"unsafe": false, "inputs": ["[u8;2]"], "output": "u16", "types": 0, "consts": 0}, "lean": "core.num.U16.from_be_bytes"},
2829
{"rust": "core::num::{u16}::from_le_bytes", "signature": {"unsafe": false, "inputs": ["[u8;2]"], "output": "u16", "types": 0, "consts": 0}, "lean": "core.num.U16.from_le_bytes"},
2930
{"rust": "core::num::{u16}::to_be_bytes", "signature": {"unsafe": false, "inputs": ["u16"], "output": "[u8;2]", "types": 0, "consts": 0}, "lean": "core.num.U16.to_be_bytes"},

‎verification/aeneas/golden/Funs.lean‎

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1808,7 +1808,7 @@ def core.cell.UnsafeCell.Insts.ZerocopyIntoBytes {T : Type} (IntoBytesInst :
18081808
}
18091809
18101810
/-- [zerocopy::util::padding_needed_for]:
1811-
Source: 'src/util/mod.rs', lines 167:0-227:1 -/
1811+
Source: 'src/util/mod.rs', lines 168:0-228:1 -/
18121812
def util.padding_needed_for
18131813
(len : Std.Usize)
18141814
(align : core.num.nonzero.NonZero Std.Usize
@@ -1824,7 +1824,7 @@ def util.padding_needed_for
18241824
ok (i2 &&& mask)
18251825
18261826
/-- [zerocopy::util::round_down_to_next_multiple_of_alignment]:
1827-
Source: 'src/util/mod.rs', lines 254:0-272:1 -/
1827+
Source: 'src/util/mod.rs', lines 255:0-273:1 -/
18281828
def util.round_down_to_next_multiple_of_alignment
18291829
(n : Std.Usize)
18301830
(align : core.num.nonzero.NonZero Std.Usize
@@ -1841,7 +1841,7 @@ def util.round_down_to_next_multiple_of_alignment
18411841
ok (n &&& mask)
18421842
18431843
/-- [zerocopy::util::max]:
1844-
Source: 'src/util/mod.rs', lines 281:0-287:1 -/
1844+
Source: 'src/util/mod.rs', lines 282:0-288:1 -/
18451845
def util.max
18461846
(a : core.num.nonzero.NonZero Std.Usize
18471847
core.num.niche_types.NonZeroUsizeInner)
@@ -1861,7 +1861,7 @@ def util.max
18611861
else ok a
18621862
18631863
/-- [zerocopy::util::min]:
1864-
Source: 'src/util/mod.rs', lines 296:0-302:1 -/
1864+
Source: 'src/util/mod.rs', lines 297:0-303:1 -/
18651865
def util.min
18661866
(a : core.num.nonzero.NonZero Std.Usize
18671867
core.num.niche_types.NonZeroUsizeInner)
@@ -1915,13 +1915,19 @@ def util.owned_transmute.round_trip_checks
19151915
let restored ← util.owned_transmute.from_bytes bytes
19161916
massert (restored = value)
19171917
1918+
/-- [zerocopy::util::validity::bool_encoding]:
1919+
Source: 'src/util/validity/mod.rs', lines 26:0-28:1 -/
1920+
def util.validity.bool_encoding (byte : Std.U8) : Result Bool := do
1921+
ok (byte < 2#u8)
1922+
19181923
/-- [zerocopy::util::safety_checks::checked_bool]:
19191924
Source: 'src/util/safety_checks/mod.rs', lines 28:0-38:1 -/
19201925
def util.safety_checks.checked_bool
19211926
(byte : Std.U8) : Result (Option Bool) := do
1922-
if byte < 2#u8
1923-
then let b ← util.transmute_unchecked Bool byte
1924-
ok (some b)
1927+
let b ← util.validity.bool_encoding byte
1928+
if b
1929+
then let b1 ← util.transmute_unchecked Bool byte
1930+
ok (some b1)
19251931
else ok none
19261932
19271933
/-- [zerocopy::util::safety_checks::checked_copy]:
@@ -1937,4 +1943,74 @@ def util.safety_checks.checked_copy
19371943
ok (true, dst1)
19381944
else ok (false, dst)
19391945
1946+
/-- [zerocopy::util::validity::nonzero_encoding]:
1947+
Source: 'src/util/validity/mod.rs', lines 37:0-39:1 -/
1948+
def util.validity.nonzero_encoding (n : Std.Usize) : Result Bool := do
1949+
ok (n != 0#usize)
1950+
1951+
/-- [zerocopy::util::safety_checks::checked_nonzero]:
1952+
Source: 'src/util/safety_checks/mod.rs', lines 72:0-78:1 -/
1953+
def util.safety_checks.checked_nonzero
1954+
(n : Std.Usize) :
1955+
Result (Option (core.num.nonzero.NonZero Std.Usize
1956+
core.num.niche_types.NonZeroUsizeInner))
1957+
:= do
1958+
let b ← util.validity.nonzero_encoding n
1959+
if b
1960+
then
1961+
core.num.nonzero.NonZero.new
1962+
Usize.Insts.CoreNumNonzeroZeroablePrimitiveNonZeroUsizeInner n
1963+
else ok none
1964+
1965+
/-- [zerocopy::util::safety_checks::checked_bool_pair]:
1966+
Source: 'src/util/safety_checks/mod.rs', lines 91:0-97:1 -/
1967+
def util.safety_checks.checked_bool_pair
1968+
(bytes : Array Std.U8 2#usize) : Result (Option (Array Bool 2#usize)) := do
1969+
let s ← lift (Array.to_slice bytes)
1970+
let left ← util.validity.read_byte s 0#usize
1971+
let s1 ← lift (Array.to_slice bytes)
1972+
let right ← util.validity.read_byte s1 1#usize
1973+
let o ← util.safety_checks.checked_bool left
1974+
match o with
1975+
| none => ok none
1976+
| some left1 =>
1977+
let o1 ← util.safety_checks.checked_bool right
1978+
match o1 with
1979+
| none => ok none
1980+
| some right1 => ok (some (Array.make 2#usize [ left1, right1 ]))
1981+
1982+
/-- [zerocopy::util::safety_checks::bool_slice_valid]: loop body 0:
1983+
Source: 'src/util/safety_checks/mod.rs', lines 108:4-115:1 -/
1984+
@[rust_loop_body]
1985+
def util.safety_checks.bool_slice_valid_loop.body
1986+
(bytes : Slice Std.U8) (i : Std.Usize) :
1987+
Result (ControlFlow Std.Usize Bool)
1988+
:= do
1989+
let i1 := Slice.len bytes
1990+
if i < i1
1991+
then
1992+
let i2 ← util.validity.read_byte bytes i
1993+
let b ← util.validity.bool_encoding i2
1994+
if b
1995+
then let i3 ← i + 1#usize
1996+
ok (cont i3)
1997+
else ok (done false)
1998+
else ok (done true)
1999+
2000+
/-- [zerocopy::util::safety_checks::bool_slice_valid]: loop 0:
2001+
Source: 'src/util/safety_checks/mod.rs', lines 108:4-115:1 -/
2002+
@[rust_loop]
2003+
def util.safety_checks.bool_slice_valid_loop
2004+
(bytes : Slice Std.U8) (i : Std.Usize) : Result Bool := do
2005+
loop
2006+
(fun i1 => util.safety_checks.bool_slice_valid_loop.body bytes i1)
2007+
i
2008+
2009+
/-- [zerocopy::util::safety_checks::bool_slice_valid]:
2010+
Source: 'src/util/safety_checks/mod.rs', lines 106:0-115:1 -/
2011+
@[reducible]
2012+
def util.safety_checks.bool_slice_valid
2013+
(bytes : Slice Std.U8) : Result Bool := do
2014+
util.safety_checks.bool_slice_valid_loop bytes 0#usize
2015+
19402016
end Zerocopy

‎verification/aeneas/golden/FunsExternal_Template.lean‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,17 @@ axiom core.num.niche_types.NonZeroUsizeInner.Insts.CoreCloneClone.clone
4646
core.num.niche_types.NonZeroUsizeInner → Result
4747
core.num.niche_types.NonZeroUsizeInner
4848

49+
/-- [core::num::nonzero::{core::num::nonzero::NonZero<T, Clause0_NonZeroInner>}::new]:
50+
Source: '/rustc/library/core/src/num/nonzero.rs', lines 388:4-388:42
51+
Name pattern: [core::num::nonzero::{core::num::nonzero::NonZero<@T, @Clause0_NonZeroInner>}::new]
52+
Visibility: public -/
53+
@[rust_fun
54+
"core::num::nonzero::{core::num::nonzero::NonZero<@T, @Clause0_NonZeroInner>}::new"]
55+
axiom core.num.nonzero.NonZero.new
56+
{T : Type} {Clause0_NonZeroInner : Type} (ZeroablePrimitiveInst :
57+
core.num.nonzero.ZeroablePrimitive T Clause0_NonZeroInner) :
58+
T → Result (Option (core.num.nonzero.NonZero T Clause0_NonZeroInner))
59+
4960
/-- [core::num::nonzero::{core::num::nonzero::NonZero<T, Clause0_NonZeroInner>}::get]:
5061
Source: '/rustc/library/core/src/num/nonzero.rs', lines 467:4-467:31
5162
Name pattern: [core::num::nonzero::{core::num::nonzero::NonZero<@T, @Clause0_NonZeroInner>}::get]
@@ -58,16 +69,20 @@ axiom core.num.nonzero.NonZero.get
5869
core.num.nonzero.NonZero T Clause0_NonZeroInner → Result T
5970

6071
/-- [zerocopy::util::copy_unchecked]:
61-
Source: 'src/util/mod.rs', lines 314:0-329:1 -/
72+
Source: 'src/util/mod.rs', lines 315:0-330:1 -/
6273
axiom util.copy_unchecked
6374
: Slice Std.U8 → Slice Std.U8 → Result (Slice Std.U8)
6475

6576
/-- [zerocopy::util::copy_unchecked_at]:
66-
Source: 'src/util/mod.rs', lines 340:0-351:1 -/
77+
Source: 'src/util/mod.rs', lines 341:0-352:1 -/
6778
axiom util.copy_unchecked_at
6879
: Slice Std.U8 → Slice Std.U8 → Std.Usize → Result (Slice Std.U8)
6980

7081
/-- [zerocopy::util::transmute_unchecked]:
71-
Source: 'src/util/mod.rs', lines 366:0-395:1 -/
82+
Source: 'src/util/mod.rs', lines 367:0-396:1 -/
7283
axiom util.transmute_unchecked {Src : Type} (Dst : Type) : Src → Result Dst
7384

85+
/-- [zerocopy::util::validity::read_byte]:
86+
Source: 'src/util/validity/mod.rs', lines 49:0-51:1 -/
87+
axiom util.validity.read_byte : Slice Std.U8 → Std.Usize → Result Std.U8
88+
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/- Copyright 2026 The Fuchsia Authors
2+
3+
Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
4+
<LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
5+
license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
6+
This file may not be copied, modified, or distributed except according to
7+
those terms. -/
8+
9+
module
10+
public import Zerocopy
11+
@[expose] public section
12+
open Aeneas Aeneas.Std
13+
namespace Zerocopy.Obligations
14+
15+
-- These expectations retain all raw candidate values. A decoder rejecting bad
16+
-- candidates at the INPUT would conceal precisely the cases being validated.
17+
def bool_encoding_spec_contract (byte : U8) (run : Result Bool) : Prop :=
18+
run ⦃ out => out = decide (byte.val = 0 ∨ byte.val = 1) ⦄
19+
def bool_encoding_spec : Prop :=
20+
∀ byte, bool_encoding_spec_contract byte (util.validity.bool_encoding byte)
21+
22+
def nonzero_encoding_spec_contract (n : Usize) (run : Result Bool) : Prop :=
23+
run ⦃ out => out = decide (n.val ≠ 0) ⦄
24+
def nonzero_encoding_spec : Prop :=
25+
∀ n, nonzero_encoding_spec_contract n (util.validity.nonzero_encoding n)
26+
27+
def checked_nonzero_spec_contract (n : Usize)
28+
(run : Result (Option (core.num.nonzero.NonZero Usize
29+
core.num.niche_types.NonZeroUsizeInner))) : Prop :=
30+
run ⦃ out => match out with
31+
| none => n.val = 0
32+
| some value => value.val = n ∧ 0 < value.val.val ⦄
33+
def checked_nonzero_spec : Prop :=
34+
∀ n, checked_nonzero_spec_contract n (util.safety_checks.checked_nonzero n)
35+
36+
def checked_bool_pair_spec_contract (bytes : Aeneas.Std.Array U8 2#usize)
37+
(run : Result (Option (Aeneas.Std.Array Bool 2#usize))) : Prop :=
38+
run ⦃ out => match out with
39+
| none => ∃ byte ∈ bytes.val, 2 ≤ byte.val
40+
| some values => values.val = bytes.val.map (fun byte => decide (byte.val = 1)) ∧
41+
∀ byte ∈ bytes.val, byte.val < 2 ⦄
42+
def checked_bool_pair_spec : Prop :=
43+
∀ bytes, checked_bool_pair_spec_contract bytes (util.safety_checks.checked_bool_pair bytes)
44+
45+
def bool_slice_valid_spec_contract (bytes : Slice U8) (run : Result Bool) : Prop :=
46+
run ⦃ out => out = decide (∀ byte ∈ bytes.val, byte.val < 2) ⦄
47+
def bool_slice_valid_spec : Prop :=
48+
∀ bytes, bool_slice_valid_spec_contract bytes (util.safety_checks.bool_slice_valid bytes)
49+
50+
end Zerocopy.Obligations

‎verification/aeneas/lean/Proofs/SafetyChecks.lean‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ theorem checked_bool_raw (byte : U8) :
4949
util.safety_checks.checked_bool byte ⦃ out =>
5050
out = if byte.val < 2 then some (decide (byte.val = 1)) else none ⦄ := by
5151
by_cases valid : byte.val < 2
52-
· simp [util.safety_checks.checked_bool, UScalar.lt_equiv,
52+
· simp [util.safety_checks.checked_bool, util.validity.bool_encoding, UScalar.lt_equiv,
5353
UScalar.ofNatCore_val_eq, util.transmute_unchecked, valid, bind_ok]
54-
· simp [util.safety_checks.checked_bool, UScalar.lt_equiv,
54+
· simp [util.safety_checks.checked_bool, util.validity.bool_encoding, UScalar.lt_equiv,
5555
UScalar.ofNatCore_val_eq, valid]
5656

5757
theorem checked_bool_spec : Specs.checked_bool_spec := by

0 commit comments

Comments
 (0)