Skip to content

Commit 12e57ce

Browse files
committed
Verify the byte writers used by IntoBytes
The public IntoBytes write methods previously kept their length selection inside pointer-bearing code. Route exact, prefix and suffix writes through extracted byte helpers so Lean checks the production decisions and complete destination updates. Prove acceptance, unchanged rejection and preservation of untouched regions. Compose the public big-endian word constructor and byte accessor with the prefix writer. Keep the typed-to-byte conversion and valid-reference conditions as explicit Rust premises. Add a self-contained offset-copy boundary with a source-bound, guarded interpretation and negative controls. This avoids the pinned backend's unsupported suffix reconstruction, which would otherwise lose the destination prefix. Regression tests exercise the public write methods. gherrit-pr-id: Gkipiolhcatlfs5igrsox32w4yivfp7k3 Agent-authored-by: AI agent acting on joshlf's behalf
1 parent 2594344 commit 12e57ce

14 files changed

Lines changed: 575 additions & 48 deletions

File tree

‎verification/aeneas/ADMISSION_DESIGN.md‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ storage, and general memory observations remain unsupported. Valid incoming
3535
references and Aeneas's reviewed borrow translation are explicit premises.
3636

3737
`external.json` supplies exact reviewed leaves, including standard-library
38-
builtins and two local unsafe helpers. Local opaque body hashes are checked
38+
builtins and narrow local byte operations. Local opaque body hashes are checked
3939
against Rust-parser body boundaries. The only admitted generic transmutation
4040
instantiation is `u8 -> bool`; its interpretation rejects all other bytes and
4141
instantiations. Byte copying has explicit input and updated-output slices and
@@ -46,6 +46,25 @@ normalization must not lose a guarded call. Pinned builtin mapping and backend
4646
semantics remain trusted correspondence premises, rather than a translation
4747
certificate established by the report.
4848

49+
The production `IntoBytes` methods delegate their write selection to
50+
`util::bytewrite::{exact,prefix,suffix}`. Their contracts prove exact byte
51+
contents, preservation of untouched destination regions, and unchanged
52+
rejection. The offset-copy boundary checks `start <= dst.len()` first, matching
53+
Rust's checked subslicing, then requires `src.len() <= dst.len() - start` for the
54+
unsafe copy. Its successful result is the original prefix, the source, and the
55+
original suffix. Incoming slice references provide initialized bytes, access
56+
permissions, disjointness, and allocation lifetime, just as for `copy_unchecked`.
57+
The boundary's Rust body subslices and directly calls `copy_nonoverlapping`.
58+
Keeping that body self-contained makes its source hash cover the actual pointer
59+
operation; a change to another local helper cannot silently change its meaning.
60+
61+
The pinned backend's `SliceIndexRangeFromUsizeSlice.get_mut` reconstructs a
62+
modified suffix incorrectly: it prepends the modified suffix to the old suffix
63+
instead of retaining the original prefix. That builtin remains unregistered.
64+
The local offset copy avoids admitting its interpretation, without modifying the
65+
pinned toolchain. A future builtin replacement needs an independent law for the
66+
reconstructed destination, including unchanged length and prefix preservation.
67+
4968
The report lives in the existing `bindings.json`. It includes both AST digests,
5069
root coverage, visited executions, call boundaries, registry digest, source
5170
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
@@ -23,6 +23,7 @@
2323
{"rust": "core::option::{core::option::Option<T0>}::unwrap", "signature": {"unsafe": false, "inputs": ["core::option::Option<T0>"], "output": "T0", "types": 1, "consts": 0}, "lean": "core.option.Option.unwrap"},
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"},
26+
{"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"},
2627
{"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"},
2728
{"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"},
2829
{"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: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,13 @@ def byteorder.U32.from_bytes
424424
(O : Type) (bytes : Array Std.U8 4#usize) : Result (byteorder.U32 O) := do
425425
ok { _0 := bytes, _1 := core.marker.PhantomData.mk }
426426
427+
/-- [zerocopy::byteorder::{zerocopy::byteorder::U16<O>}::to_bytes]:
428+
Source: 'src/byteorder.rs', lines 578:12-580:13
429+
Visibility: public -/
430+
def byteorder.U16.to_bytes
431+
{O : Type} (self : byteorder.U16 O) : Result (Array Std.U8 2#usize) := do
432+
ok self._0
433+
427434
/-- [zerocopy::byteorder::{zerocopy::byteorder::U16<O>}::new]:
428435
Source: 'src/util/macros.rs', lines 568:21-568:88
429436
Visibility: public -/
@@ -791,6 +798,57 @@ def trait_checks.check_array_into_bytes : Result Unit := do
791798
trait_checks.require_into_bytes (Array.Insts.ZerocopyIntoBytes 3#usize
792799
(Array.Insts.ZerocopyIntoBytes 2#usize Bool.Insts.ZerocopyIntoBytes))
793800
801+
/-- [zerocopy::util::bytewrite::exact]:
802+
Source: 'src/util/bytewrite/mod.rs', lines 27:0-37:1 -/
803+
def util.bytewrite.exact
804+
(src : Slice Std.U8) (dst : Slice Std.U8) :
805+
Result (Bool × (Slice Std.U8))
806+
:= do
807+
let i := Slice.len dst
808+
let i1 := Slice.len src
809+
if i = i1
810+
then let dst1 ← util.copy_unchecked src dst
811+
ok (true, dst1)
812+
else ok (false, dst)
813+
814+
/-- [zerocopy::util::bytewrite::prefix]:
815+
Source: 'src/util/bytewrite/mod.rs', lines 48:0-57:1 -/
816+
def util.bytewrite.prefix
817+
(src : Slice Std.U8) (dst : Slice Std.U8) :
818+
Result (Bool × (Slice Std.U8))
819+
:= do
820+
let i := Slice.len src
821+
let i1 := Slice.len dst
822+
if i <= i1
823+
then let dst1 ← util.copy_unchecked src dst
824+
ok (true, dst1)
825+
else ok (false, dst)
826+
827+
/-- [zerocopy::util::bytewrite::suffix]:
828+
Source: 'src/util/bytewrite/mod.rs', lines 68:0-79:1 -/
829+
def util.bytewrite.suffix
830+
(src : Slice Std.U8) (dst : Slice Std.U8) :
831+
Result (Bool × (Slice Std.U8))
832+
:= do
833+
let i := Slice.len dst
834+
let i1 := Slice.len src
835+
let o ← lift (Usize.checked_sub i i1)
836+
match o with
837+
| none => ok (false, dst)
838+
| some start =>
839+
let dst1 ← util.copy_unchecked_at src dst start
840+
ok (true, dst1)
841+
842+
/-- [zerocopy::util::bytewrite::be_word_prefix]:
843+
Source: 'src/util/bytewrite/mod.rs', lines 93:0-96:1 -/
844+
def util.bytewrite.be_word_prefix
845+
(n : Std.U16) (dst : Slice Std.U8) : Result (Bool × (Slice Std.U8)) := do
846+
let value ←
847+
byteorder.U16.new byteorder.BigEndian.Insts.ZerocopyByteorderByteOrder n
848+
let a ← byteorder.U16.to_bytes value
849+
let s ← lift (Array.to_slice a)
850+
util.bytewrite.prefix s dst
851+
794852
/-- [zerocopy::impls::_#1::{impl zerocopy::IntoBytes for i8}::only_derive_is_allowed_to_implement_this_trait]:
795853
Source: 'src/util/macros.rs', lines 165:8-165:62
796854
Visibility: public -/
@@ -1750,7 +1808,7 @@ def core.cell.UnsafeCell.Insts.ZerocopyIntoBytes {T : Type} (IntoBytesInst :
17501808
}
17511809
17521810
/-- [zerocopy::util::padding_needed_for]:
1753-
Source: 'src/util/mod.rs', lines 166:0-226:1 -/
1811+
Source: 'src/util/mod.rs', lines 167:0-227:1 -/
17541812
def util.padding_needed_for
17551813
(len : Std.Usize)
17561814
(align : core.num.nonzero.NonZero Std.Usize
@@ -1766,7 +1824,7 @@ def util.padding_needed_for
17661824
ok (i2 &&& mask)
17671825
17681826
/-- [zerocopy::util::round_down_to_next_multiple_of_alignment]:
1769-
Source: 'src/util/mod.rs', lines 253:0-271:1 -/
1827+
Source: 'src/util/mod.rs', lines 254:0-272:1 -/
17701828
def util.round_down_to_next_multiple_of_alignment
17711829
(n : Std.Usize)
17721830
(align : core.num.nonzero.NonZero Std.Usize
@@ -1783,7 +1841,7 @@ def util.round_down_to_next_multiple_of_alignment
17831841
ok (n &&& mask)
17841842
17851843
/-- [zerocopy::util::max]:
1786-
Source: 'src/util/mod.rs', lines 280:0-286:1 -/
1844+
Source: 'src/util/mod.rs', lines 281:0-287:1 -/
17871845
def util.max
17881846
(a : core.num.nonzero.NonZero Std.Usize
17891847
core.num.niche_types.NonZeroUsizeInner)
@@ -1803,7 +1861,7 @@ def util.max
18031861
else ok a
18041862
18051863
/-- [zerocopy::util::min]:
1806-
Source: 'src/util/mod.rs', lines 295:0-301:1 -/
1864+
Source: 'src/util/mod.rs', lines 296:0-302:1 -/
18071865
def util.min
18081866
(a : core.num.nonzero.NonZero Std.Usize
18091867
core.num.niche_types.NonZeroUsizeInner)

‎verification/aeneas/golden/FunsExternal_Template.lean‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,16 @@ axiom core.num.nonzero.NonZero.get
5858
core.num.nonzero.NonZero T Clause0_NonZeroInner → Result T
5959

6060
/-- [zerocopy::util::copy_unchecked]:
61-
Source: 'src/util/mod.rs', lines 313:0-328:1 -/
61+
Source: 'src/util/mod.rs', lines 314:0-329:1 -/
6262
axiom util.copy_unchecked
6363
: Slice Std.U8 → Slice Std.U8 → Result (Slice Std.U8)
6464

65+
/-- [zerocopy::util::copy_unchecked_at]:
66+
Source: 'src/util/mod.rs', lines 340:0-351:1 -/
67+
axiom util.copy_unchecked_at
68+
: Slice Std.U8 → Slice Std.U8 → Std.Usize → Result (Slice Std.U8)
69+
6570
/-- [zerocopy::util::transmute_unchecked]:
66-
Source: 'src/util/mod.rs', lines 340:0-369:1 -/
71+
Source: 'src/util/mod.rs', lines 366:0-395:1 -/
6772
axiom util.transmute_unchecked {Src : Type} (Dst : Type) : Src → Result Dst
6873

‎verification/aeneas/golden/Types.lean‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ structure byteorder.F64 (O : Type) where
178178
_1 : core.marker.PhantomData O
179179
180180
/-- Trait declaration: [zerocopy::IntoBytes]
181-
Source: 'src/lib.rs', lines 5941:0-6446:1
181+
Source: 'src/lib.rs', lines 5941:0-6414:1
182182
Visibility: public -/
183183
structure IntoBytes (Self : Type) where
184184
only_derive_is_allowed_to_implement_this_trait : Result Unit
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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+
public import Rust.Bytes
12+
@[expose] public section
13+
open Aeneas Aeneas.Std
14+
namespace Zerocopy.Obligations
15+
16+
-- Retain all raw inputs, including unsuccessful and empty writes. These
17+
-- expectations are independent of the specification macro and its decoders.
18+
def write_exact_spec_contract (src dst : Slice U8)
19+
(run : Result (Bool × Slice U8)) : Prop :=
20+
run ⦃ out => out.1 = decide (src.val.length = dst.val.length) ∧
21+
out.2.val = if src.val.length = dst.val.length then src.val else dst.val ⦄
22+
23+
def write_prefix_spec_contract (src dst : Slice U8)
24+
(run : Result (Bool × Slice U8)) : Prop :=
25+
run ⦃ out => out.1 = decide (src.val.length ≤ dst.val.length) ∧
26+
out.2.val = if src.val.length ≤ dst.val.length
27+
then src.val ++ dst.val.drop src.val.length else dst.val ⦄
28+
29+
def write_suffix_spec_contract (src dst : Slice U8)
30+
(run : Result (Bool × Slice U8)) : Prop :=
31+
run ⦃ out => out.1 = decide (src.val.length ≤ dst.val.length) ∧
32+
out.2.val = if src.val.length ≤ dst.val.length
33+
then dst.val.take (dst.val.length - src.val.length) ++ src.val else dst.val ⦄
34+
35+
def write_exact_spec : Prop :=
36+
∀ src dst, write_exact_spec_contract src dst (util.bytewrite.exact src dst)
37+
def write_prefix_spec : Prop :=
38+
∀ src dst, write_prefix_spec_contract src dst (util.bytewrite.prefix src dst)
39+
def write_suffix_spec : Prop :=
40+
∀ src dst, write_suffix_spec_contract src dst (util.bytewrite.suffix src dst)
41+
42+
def write_be_word_prefix_spec_contract (n : U16) (dst : Slice U8)
43+
(run : Result (Bool × Slice U8)) : Prop :=
44+
run ⦃ out => out.1 = decide (2 ≤ dst.val.length) ∧
45+
out.2.val.map U8.bv = if 2 ≤ dst.val.length
46+
then Rust.Bytes.encodeBE 2 n.val ++ (dst.val.drop 2).map U8.bv
47+
else dst.val.map U8.bv ⦄
48+
def write_be_word_prefix_spec : Prop :=
49+
∀ n dst, write_be_word_prefix_spec_contract n dst (util.bytewrite.be_word_prefix n dst)
50+
51+
end Zerocopy.Obligations
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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 Specs
11+
public import Proofs.SafetyChecks
12+
public import Proofs.Byteorder
13+
@[expose] public section
14+
15+
/-!
16+
These functions are the size selection and byte-copy implementation called by
17+
IntoBytes's three public write methods. The proofs include unsuccessful writes
18+
and the unaffected destination region, rather than merely checking copied bytes.
19+
The source-bound copy interpretations retain the reference premises documented
20+
in ADMISSION_DESIGN.md; their pointer implementations are not extracted.
21+
-/
22+
open Aeneas Aeneas.Std AeneasSpecs
23+
namespace Zerocopy.Proofs
24+
25+
private theorem decode_write (out : Bool × Slice U8) :
26+
(inferInstance : RustModel (Bool × Slice U8)).decode out =
27+
some (out.1, byteSlice out.2) := by
28+
have decoded := decode_byte_slice out.2
29+
simp only [RustModel.decode] at decoded ⊢
30+
rw [decoded]
31+
32+
theorem write_exact_raw (src dst : Slice U8) :
33+
util.bytewrite.exact src dst ⦃ out =>
34+
out.1 = decide (src.val.length = dst.val.length) ∧
35+
out.2.val = if src.val.length = dst.val.length then src.val else dst.val ⦄ := by
36+
by_cases same : src.val.length = dst.val.length
37+
· simp [util.bytewrite.exact, Slice.len,
38+
util.copy_unchecked, same, bind_ok]
39+
· simp [util.bytewrite.exact, Slice.len, same, Ne.symm same]
40+
41+
theorem write_prefix_raw (src dst : Slice U8) :
42+
util.bytewrite.prefix src dst ⦃ out =>
43+
out.1 = decide (src.val.length ≤ dst.val.length) ∧
44+
out.2.val = if src.val.length ≤ dst.val.length
45+
then src.val ++ dst.val.drop src.val.length else dst.val ⦄ := by
46+
by_cases fits : src.val.length ≤ dst.val.length
47+
· simp [util.bytewrite.prefix, Slice.len, UScalar.le_equiv,
48+
util.copy_unchecked, fits, bind_ok]
49+
· simp [util.bytewrite.prefix, Slice.len, UScalar.le_equiv, fits]
50+
51+
theorem write_suffix_raw (src dst : Slice U8) :
52+
util.bytewrite.suffix src dst ⦃ out =>
53+
out.1 = decide (src.val.length ≤ dst.val.length) ∧
54+
out.2.val = if src.val.length ≤ dst.val.length
55+
then dst.val.take (dst.val.length - src.val.length) ++ src.val else dst.val ⦄ := by
56+
unfold util.bytewrite.suffix
57+
step as ⟨start, facts⟩
58+
cases start with
59+
| none =>
60+
have unfit : ¬ src.val.length ≤ dst.val.length := by simpa [Slice.len] using facts
61+
simp [unfit]
62+
| some start =>
63+
have bounds : start.val = dst.val.length - src.val.length ∧
64+
src.val.length ≤ dst.val.length := by
65+
simp only [Slice.len] at facts
66+
exact ⟨facts.2.1, facts.1⟩
67+
have offset : start.val ≤ dst.val.length := by omega
68+
have fits : src.val.length ≤ dst.val.length - start.val := by omega
69+
have finish : start.val + src.val.length = dst.val.length := by omega
70+
simp only [util.copy_unchecked_at, if_pos offset, dif_pos fits, bind_ok, WP.spec_ok]
71+
simp [bounds.1, bounds.2]
72+
73+
-- The concrete consumer uses the public byteorder operations, then the same
74+
-- prefix writer as IntoBytes. Reuse the proved encoder rather than recalculate
75+
-- its bitvector arithmetic inside this caller proof.
76+
theorem write_be_word_prefix_raw (n : U16) (dst : Slice U8) :
77+
util.bytewrite.be_word_prefix n dst ⦃ out =>
78+
out.1 = decide (2 ≤ dst.val.length) ∧
79+
out.2.val.map U8.bv = if 2 ≤ dst.val.length
80+
then Rust.Bytes.encodeBE 2 n.val ++ (dst.val.drop 2).map U8.bv
81+
else dst.val.map U8.bv ⦄ := by
82+
have encoding_eq : (do
83+
let value ← byteorder.U16.new byteorder.BigEndian.Insts.ZerocopyByteorderByteOrder n
84+
byteorder.U16.to_bytes value) = byteorder.verification.write_u16_be n := by
85+
simp only [byteorder.verification.write_u16_be, core.convert.IntoFrom.into,
86+
ArrayU82.Insts.CoreConvertFromU16.from,
87+
byteorder.U16.to_bytes]
88+
unfold util.bytewrite.be_word_prefix
89+
rw [← Aeneas.Std.bind_assoc, encoding_eq]
90+
step with Raw.write_u16_be n as ⟨bytes, encoded⟩
91+
simp only [lift, bind_ok]
92+
apply WP.spec_mono (write_prefix_raw (Array.to_slice bytes) dst)
93+
intro out facts
94+
have length : bytes.val.length = 2 := by simp
95+
simp only [Array.to_slice, Slice.from_val, length] at facts
96+
refine ⟨facts.1, ?_⟩
97+
by_cases fits : 2 ≤ dst.val.length
98+
· simp only [if_pos fits] at facts ⊢
99+
rw [facts.2, List.map_append, encoded]
100+
· simp only [if_neg fits] at facts ⊢
101+
rw [facts.2]
102+
103+
theorem write_exact_spec : Specs.write_exact_spec := by
104+
intro src dst _ _ _ _
105+
apply WP.spec_mono (write_exact_raw src dst)
106+
intro out facts
107+
exact ⟨(out.1, byteSlice out.2), decode_write out, facts⟩
108+
109+
theorem write_prefix_spec : Specs.write_prefix_spec := by
110+
intro src dst _ _ _ _
111+
apply WP.spec_mono (write_prefix_raw src dst)
112+
intro out facts
113+
exact ⟨(out.1, byteSlice out.2), decode_write out, facts⟩
114+
115+
theorem write_suffix_spec : Specs.write_suffix_spec := by
116+
intro src dst _ _ _ _
117+
apply WP.spec_mono (write_suffix_raw src dst)
118+
intro out facts
119+
exact ⟨(out.1, byteSlice out.2), decode_write out, facts⟩
120+
121+
theorem write_be_word_prefix_spec : Specs.write_be_word_prefix_spec := by
122+
intro n dst _ _ _ _
123+
apply WP.spec_mono (write_be_word_prefix_raw n dst)
124+
intro out facts
125+
exact ⟨(out.1, byteSlice out.2), decode_write out, facts⟩
126+
127+
end Zerocopy.Proofs

0 commit comments

Comments
 (0)