Skip to content

CBOR, CDDL, COSE: eliminate all SZ.fits_u64 assumptions#296

Merged
tahina-pro merged 22 commits into
project-everest:masterfrom
tahina-pro:_taramana_cbor_sizet
Jun 29, 2026
Merged

CBOR, CDDL, COSE: eliminate all SZ.fits_u64 assumptions#296
tahina-pro merged 22 commits into
project-everest:masterfrom
tahina-pro:_taramana_cbor_sizet

Conversation

@tahina-pro

@tahina-pro tahina-pro commented Jun 26, 2026

Copy link
Copy Markdown
Member

Summary

This PR removes every assume SZ.fits_u64 (and every squash SZ.fits_u64
precondition/parameter) from the EverParse CBOR and CDDL Pulse code — 62 → 0
including the one assume (SZ.fits_u64) that the EverCDDL tool (CDDL.Tool.Gen)
emitted into the preamble of every generated .fst
, so generated parsers,
validators and serializers are now assumption-free too.
These assumptions silently baked in the platform fact "size_t is at least 64
bits", which is not guaranteed by C (size_t is only mandated to be ≥ 16 bits)
nor by Rust (usize is only guaranteed ≥ 16 bits). They were a soundness gap in
the verified-to-C/Rust story: the proofs assumed something the target platform
need not satisfy.

Every site is now discharged by one of a small number of sound, portable
mechanisms (detailed below), so the extracted C and Rust are correct on any
target with a ≥ 16-bit size_t/usize. As downstream cleanup, the now-obsolete
const_assert!(usize::BITS >= 64) guards are removed from the Rust test
harnesses, and the result is validated by a full 32-bit cross-platform
build+test run.

This builds on a prerequisite change in the F* FStar.SizeT interface
(weakening uint64_to_sizet to requires fits_u64 \/ fits (U64.v x) : FStarLang/FStar@a22c993) and adds
one new lemma to the Pulse standard library (Pulse.Lib.ArrayPtr.pts_to_len : FStarLang/pulse@74bb5a4).

Motivation

FStar.SizeT exposes fits_at_least_16 (size_t ≥ 16 bits) but no dual
"size_t ≤ 64 bits" fact and no checked u64 → size_t conversion. Wherever
the CBOR/CDDL Pulse code needed to convert a CBOR header argument (a u64
length/count) or an in-memory length into a size_t, it papered over the gap
with assume SZ.fits_u64. This PR replaces each such assumption with a real
proof or a runtime check, so the size_t-platform assumption is fully discharged.

Key techniques

  • Portable base-2¹⁵ size comparison (new module
    CBOR.Pulse.Raw.EverParse.SizeComparison, mirrored in CDDL.Pulse.Misc for
    the CDDL layer): 32768 = 2¹⁵ is the largest power of two guaranteed to fit
    any ≥16-bit size_t (via FStar.SizeT.fits_at_least_16). Dividing a
    size_t by 32768 four times computes b / 2⁶⁰; if the quotient ≥ 16 then
    b ≥ 2⁶⁴ (so it cannot equal a u64), otherwise b < 2⁶⁴ and
    sizet_to_uint64 narrows exactly (via FStar.Math.Lemmas.small_mod).
    This yields verified, truncation-free u64 ↔ size_t comparisons
    (u64_lte_sizet, sizet_lte_u64, sizet_fits_u64, sizet_eq_u64) with no
    platform axiom. In C/Rust these compile to >>15 / &0x7FFF and are sound at
    16/32/64-bit width.
  • "≥ 1 byte" element-count bound: a valid serialized nlist n of items whose
    parser has parser_kind_low ≥ 1 occupies ≥ n bytes, so n ≤ SZ.v (len payload), hence SZ.fits n. Packaged as a reusable ghost lemma
    nlist_count_fits so only the resulting fits fact enters tight SMT contexts.
  • Representation-invariant recovery: the in-memory array/map/slice length is
    itself a size_t equal to the u64 header count (e.g.
    CBOR.Pulse.Raw.Match.fst: SZ.v (S.len ptr) == U64.v count), so the bound is
    already in hand — no conversion needed.
  • Pulse.Lib.ArrayPtr.pts_to_len (new Pulse-stdlib lemma): an arrayptr is
    backed by an in-memory Pulse.Lib.Array, whose length is SZ.fits by
    construction; therefore pts_to s #p v ⇒ SZ.fits (Seq.length v). This
    discharges the FFI _from_arrayptr boundary sites without any precondition
    change.
  • Exact narrowing for sizet_to_uint64: with FStar.UInt.fits (length) 64
    already in the constructor preconditions, small_mod makes the truncating
    sizet_to_uint64 exact (fits_u64 was the wrong/dual fact, only ever a
    shortcut).

What changed, by area

1. Validator / jumper (Class A/B/C)

  • New CBOR.Pulse.Raw.EverParse.SizeComparison with the base-2¹⁵ primitives and
    nlist_count_fits.
  • Removed the squash SZ.fits_u64 parameter from the entire validator/jumper
    public API (validate_raw_data_item, jump_raw_data_item,
    validate/jump_leaf(_content), impl_remaining_data_items_header,
    jump_recursive_step_count_leaf, impl_holds_on_raw_data_item, …), plus the
    serialized readers, parse entry points, and the nondet streaming validators
    (the two deepest residuals in Nondet.Gen were discharged with
    impl_remaining_data_items_header + nlist_count_fits).

2. Serializer side

  • Refactored the nlist serializer combinators l2r_write_nlist_as_slice and
    compute_remaining_size_nlist_as_slice (in LowParse.Pulse.VCList, used only
    by CBOR) to take the element count as Ghost.erased nat and read the runtime
    loop bound from S.len of the in-memory slice itself. This drops
    uint64_to_sizet from the spec index (now U64.v count, matching
    serialize_content) and cascade-removes the f64 squash parameter from the
    whole ser/siz tree in Format.Serialize.fst, including the two top-level
    entry points and the string case.
  • CDDL/CBOR length-vs-2^64-1 guards reworked to the portable comparisons.
    Because the CDDL C-extraction include set excludes the CBOR raw layer, the
    comparison helpers are provided in CDDL.Pulse.Misc as real (non-inline) C
    functions
    so they compose under &&/|| without tripping KaRaMeL's Low*
    short-circuit check.

3. C2 narrow-side constructors

  • mk_string/mk_array/mk_map-style guards in the det/nondet Rust APIs and
    Det.Common now use not (sizet_fits_u64 (S.len …)) instead of
    uint64_to_sizet (2^64-1). Three vestigial assumes in the nondet map
    comparison were removed outright.

4. FFI / spec boundary

  • mk_string/mk_array/mk_map_from_arrayptr and cbor_det_get_string_as_slice:
    discharged via the new AP.pts_to_len (the precondition already gives
    Seq.length v == U64.v len).
  • AST.Literal text-string matcher: the length check became a portable
    sizet_eq_u64 comparison.
  • AST.Literal text-string constructor (a stack-array allocation of
    uint64_to_sizet len) is the one site whose bound is not recoverable at
    runtime, so it is made a static precondition: wf_literal's LTextString case
    is tightened from String.length s < pow2 64 to < pow2 16 (which propagates
    through wf_elem_typ → typ_bounded, i.e. the elaborator/tool boundary), and
    the combinator takes SZ.fits (U64.v len), discharged automatically via
    fits_at_least_16. 2¹⁶ = 64 KB is far above any realistic CDDL literal.

5. Downstream Rust cleanup

  • Removed the four const_assert!(usize::BITS >= 64) and the now-unused
    static_assertions dependency from the cborrs, cborrs-nondet,
    evercosign, and cborrs-test crates. Every u64 → usize widening in the
    generated Rust is now either runtime-guarded by the base-2¹⁵ check (rejects
    oversized lengths gracefully) or bounded by an actual in-memory slice length,
    so the assertion is obsolete (Rust guarantees usize ≥ 16 bits; Reference
    [type.numeric.int.size.minimum]).

6. EverCDDL-generated F* (CDDL.Tool.Gen) + the .size control

The tool emitted a fixed preamble line let _ : squash (SZ.fits_u64) = assume (SZ.fits_u64) into every generated .fst. It was load-bearing for a
SZ.fits_u64 precondition threaded through the entire
CDDL.Pulse.AST.{Validate,Bundle,Parse} validator/bundle/parser combinator API,
whose only real consumer was converting the .size control's bounds
(WfTStrSize lo hi) to size_t during validation.

  • CDDL.Pulse.Misc.impl_str_size now takes lo hi : U64.t (spec
    str_size ty (U64.v lo) (U64.v hi)) and compares them against the runtime
    size_t string length with the portable base-2¹⁵ helpers (u64_lte_sizet /
    sizet_lte_u64) — matching the already-U64-based serializer side
    (bundle_str_size). The bounds are converted with U64.uint_to_t (always
    valid since the well-formedness invariant gives hi < 2⁶⁴), so the .size
    upper bound stays at < 2⁶⁴no language restriction, unlike the literal
    case. This is also strictly more correct on a sub-64-bit target: when
    hi > 2³², len ≤ hi is correctly true; when lo > 2³², lo ≤ len is
    correctly false (no string can exceed SIZE_MAX).
  • The now-vestigial SZ.fits_u64 conjuncts/refinements were dropped from all 15
    CDDL.Pulse.AST.{Validate,Bundle,Parse} signatures, the dead sz_uint_to_t
    helper was removed, and the generated preamble assume line was deleted from
    CDDL.Tool.Gen. Rebuilding the tool, regenerating, and re-verifying the COSE +
    cddl test suites all pass with zero fits_u64 in the generated .fst.

Pulse.Lib.ArrayPtr: add the pts_to_len ghost lemma
(pts_to s #p v ⇒ SZ.fits (Seq.length v)), proven from the model exactly like
the existing pts_to_not_null (A.pts_to_range_prop s.base bounds
Seq.length v by A.length s.base, which fits). This is required by the
_from_arrayptr and get_string_as_slice sites above.

Verification & testing

  • Full proof + snapshot build green throughout
    (make -j16 cbor-snapshot && make -j16 cose-snapshot && make -j16 cbor-test cddl-test cose-test): 724 passed, 0 failed (+ 363 nondet conformance).
    No remaining assume in the CBOR validator/jumper path; zero
    SZ.fits_u64 assumptions anywhere in src/cbor/pulse, src/cddl/pulse,
    src/cddl/spec.
  • EverCDDL tool rebuild + regeneration: rebuilding cddl.exe from the edited
    CDDL.Tool.Gen, regenerating the COSE (make -j16 cose-snapshot) and cddl
    (make -j16 cddl-test cose-test) .fst from .cddl, and re-verifying +
    re-extracting them to C is green; no fits_u64 appears in any regenerated
    .fst.
  • One pre-existing heavy elaboration proof (array_group_included in
    CDDL.Spec.AST.Elab.Included.Array, rlimit 512 / ifuel 8) was destabilized
    by the strictly-stronger wf_literal definition into nondeterministic Z3
    unknown; stabilized with --retry 5 (F*'s sanctioned recovery for a
    library-definition change). The proof remains valid since < 2¹⁶ ⟹ < 2⁶⁴.
  • 32-bit cross-platform validation (i686-unknown-linux-gnu, usize = 32,
    confirmed genuine ELF 32-bit Intel 80386):
    • cborrs (CBOR det): 29/29 tests pass.
    • cborrs-nondet: 1/1 passes.
    • cborrs-test (CDDL): builds + links.
    • evercosign (COSE, lib + bin + ed25519-dalek/clap/openssl-sys):
      interop tests 2 passed, 1 ignored.
    • share/everparse/tests/cbor/cbor_tests_rust conformance suite (re-validates
      the Rust det/nondet APIs against the 1082 C-emitted artifacts, byte-for-byte
      serializer equality, incl. the depth-2200 recursion stress test and the full
      UTF-8 battery): det 363/363, nondet 363/363.

Notes for reviewers

  • The C/Rust extraction snapshots are regenerated by this work (the base-2¹⁵
    comparisons are executable). They are committed separately from the source
    changes.
  • No new platform axioms are introduced. The only new library surface is
    Pulse.Lib.ArrayPtr.pts_to_len and the SizeComparison/CDDL.Pulse.Misc
    helper functions.
  • Net effect: the verified CBOR/CDDL stack is now sound on any
    ≥16-bit-size_t/usize platform — a strictly stronger portability guarantee
    than before, with the 64-bit assumption fully discharged rather than assumed.

tahina-pro and others added 22 commits June 19, 2026 07:31
… bound in hand)

Following the weakening of `uint64_to_sizet` in FStar.SizeT (precondition now
`fits_u64 \/ fits (U64.v x)`), discharge the "Class A" uses of `assume
SZ.fits_u64` -- u64->size_t widenings where a size_t bound is already available
-- and drop the now-unnecessary `squash SZ.fits_u64` parameters/assumes.

- API.Base.fst (mk_string/array/map_from_array): array-backed length, so
  `A.pts_to_len a` gives `length a == U64.v len` and `length` ensures `SZ.fits`.
- API.Det.C.fst (mk_map_from_array_safe): precondition already gives
  `A.length a == U64.v len`.
- Raw.Iterator.fst (cbor_raw_iterator_truncate): `U64.v len <= length r ==
  SZ.v (S.len c'.s)` via pts_to_len.
- Raw.Read.fst: drop `squash SZ.fits_u64` from `cbor_array_item` (index < array
  length), and the vestigial params from `cbor_array_iterator_init` /
  `cbor_map_iterator_init`; update all callers (Det.Common, Nondet, Compare,
  Nondet.Compare). The serialized-iterator `next` family keeps its parameter
  (jumper, not Class A).
- Format.Nondet.Compare.fst: prove the serialized array/map element count fits
  size_t via the "at least one byte" argument (parse_raw_data_item_kind
  parser_kind_low == 1, so a valid nlist of n items occupies >= n bytes <=
  SZ.v (S.len payload)); add `serialized_array_count_fits` /
  `serialized_map_count_fits` helpers.

The remaining `assume SZ.fits_u64` (streaming validator/jumper family,
arrayptr-backed lengths, and unbounded narrow-side comparisons) are out of
scope for Class A. No extracted C/Rust changes (the dropped parameters are
erased). Verified with `make cbor-snapshot cose-snapshot cbor-test cddl-test
cose-test`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The size_t->u64 narrowings in the string constructors are exact (non-truncating):
`sizet_to_uint64 (S.len s)` has postcondition `% pow2 64`, and the
`mk_string_t` precondition already gives `FStar.UInt.fits (Seq.length v) 64`,
so `FStar.Math.Lemmas.small_mod (SZ.v (S.len s)) (pow2 64)` discharges the
modulus to the identity -- the same exactness argument already used assume-free
by the array/map constructors (cbor_det_mk_array via `fits_mod`).

- cbor_det_mk_string (Det.Common.fst), cbor_nondet_mk_string (Nondet.fst):
  replace `assume SZ.fits_u64` with the `small_mod` exactness lemma.
- cbor_det_mk_tagged (Det.Common.fst), cbor_nondet_mk_tagged (Nondet.fst):
  drop the `assume SZ.fits_u64` entirely -- it was vestigial (these construct a
  tag from a u64 with no size_t conversion).

No extracted C/Rust changes (lemma/assume are ghost). Verified with
`make cbor-snapshot cose-snapshot cbor-test cddl-test cose-test`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the unsound `assume SZ.fits_u64` in the streaming validator/jumper with
a portable, TCB-neutral u64<->size_t bridge, removing the `squash SZ.fits_u64`
parameter from the entire validator/jumper public API.

New module CBOR.Pulse.Raw.EverParse.SizeComparison provides `u64_lte_sizet`, a
comparison of a U64.t against a SZ.t via base-2^15 (radix 32768) multi-limb
arithmetic. 32768 = 2^15 is the largest power-of-two literal guaranteed to fit
any size_t (the C standard mandates size_t >= 16 bits, exposed as
FStar.SizeT.fits_at_least_16), so div/rem by 32768 work in both types and a u64
decomposes into <= 5 limbs. This needs no `fits_u64`, no platform axiom, and no
new FStar.SizeT primitive.

In EverParse.Format:
- `validate_total_constant_size_u64`: a size-as-u64 variant of
  validate_total_constant_size that uses `u64_lte_sizet` for the short-input
  failure path (sound on 16/32/64-bit size_t). Drives the string/bytes leaf
  content validator with no `fits_u64` and no `parse_leaf_content` reasoning
  (the existing filter/synth/ext wrappers propagate failure).
- `jump_constant_size_u64`: derives `fits` from the jumper precondition (the
  parse already succeeds, so the length <= remaining <= max_size_t) via fits_lte.
- The `squash SZ.fits_u64` parameter is removed from validate_raw_data_item,
  jump_raw_data_item, validate/jump_leaf(_content)(_seq/_empty),
  impl_remaining_data_items_header (fits from the count<=bound precondition),
  jump_recursive_step_count_leaf (the array/map count vs remaining-item budget
  is now decided with u64_lte_sizet), and impl_holds_on_raw_data_item (+ .fsti).

Serialized readers and downstream callers updated: serialized array item access
and the nondet map-count conversions discharge `fits` via the "at least one
byte" argument (parser_kind_low == 1/2, so a valid nlist of n items occupies
>= n bytes <= payload length); Read.fst iterator_next, Format.Parse,
Format.Nondet.Validate, and the iterator-next callers (Det.Common, Nondet,
Compare, Nondet.Compare) drop their `fits_u64` arguments/assumes.

Residual: two localized `assume (pure SZ.fits_u64)` remain in the deeply-nested
Nondet.Gen streaming helpers (impl_check_valid_item, impl_check_map_depth_aux),
documented as TODO -- the counts there are bounded by the traversal-tail bytes,
but threading that bound destabilized a tight z3rlimit proof.

Verified with `make cbor-snapshot cose-snapshot cbor-test cddl-test cose-test`
(exit 0). The extracted C/Rust snapshots change (the 2^15 comparison replaces the
unchecked cast) and are intentionally not committed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lidators

The two residual `assume (pure SZ.fits_u64)` in the nondet streaming
helpers are now discharged soundly:

- impl_check_map_depth_aux: the array/map child counts are obtained from
  the total `impl_remaining_data_items_header (S.len tl) h` (whose bound
  is established by a `parse_nlist_kind_low` byte-length argument on the
  traversal tail), exactly mirroring the verified pattern already used in
  the equality checker. This avoids the `uint64_to_sizet` conversion
  entirely (for a map header, remaining_data_items_header = 2*pairs, which
  is precisely the flattened recursion count).

- impl_check_valid_item: the map entry count fits size_t because the
  serialized payload holds that many >= 1-byte entries. This is packaged
  as a new reusable ghost lemma `nlist_count_fits` so that only the final
  `SZ.fits n` fact enters the tight streaming proof context.

Full build (cbor/cose snapshots + cbor/cddl/cose tests) passes with no
fits_u64 assumptions remaining in the CBOR validator/jumper path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Removes the serializer-side `assume SZ.fits_u64` / `squash SZ.fits_u64`
threading via three mechanisms (no new typeclass, no broad LowParse change):

1. nlist array/map count (the `nlist (SZ.v (uint64_to_sizet count))` pattern):
   the count is the in-memory slice length, already equal to the U64 header
   count by the representation invariant (CBOR.Pulse.Raw.Match.fst array/map:
   SZ.v (S.len ptr) == U64.v count). Refactor l2r_write_nlist_as_slice and
   compute_remaining_size_nlist_as_slice (CBOR-only callers) to take the count
   as Ghost.erased nat and read the runtime loop bound from S.len of the slice
   itself, so no U64->size_t conversion is needed. This drops uint64_to_sizet
   from the spec index (now U64.v count, matching serialize_content) and
   cascade-removes the f64 squash parameter from the whole ser/siz tree in
   Format.Serialize.fst, including the two top-level cbor_serialize/cbor_size
   assumes. cbor_serialize_string discharges directly from its precondition.

2. size_t-vs-u64 length comparisons (CDDL Serialize.Misc, CBOR Raw.Nondet):
   replace `uint64_to_sizet pow2_64_m1`-style guards with portable base-2^15
   comparisons (sizet_fits_u64 / sizet_lte_u64 / u64_lte_sizet). CDDL cannot
   import the CBOR raw layer (its C-extraction include set excludes it), so the
   generic helpers are provided in CDDL.Pulse.Misc; they are real (non-inline)
   C functions so they compose under && / || without tripping KaRaMeL's Low*
   short-circuit check.

3. CDDL Serialize.Gen.ArrayGroup: the assume was vestigial (no conversion).

Full build (cbor/cose snapshots + cbor/cddl/cose tests) passes: 724 + 363
tests, 0 failed. Snapshots intentionally not committed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Removes the narrow-side `assume SZ.fits_u64` guards in the det/nondet
mk_string / mk_array / mk_map constructors and the nondet map comparison:

- API.Det.Rust, API.Nondet.Rust (mk_string, mk_array) and API.Det.Common
  (mk_map_gen_by_ref) gate construction on whether the in-memory slice
  length fits a u64 header field. Replace the
  `SZ.gt (S.len x) (uint64_to_sizet (2^64-1))` guard (which needed the
  assume just to form uint64_to_sizet) with the portable base-2^15
  `not (SizeComparison.sizet_fits_u64 (S.len x))`. The post still relates
  Some?/None to FStar.UInt.fits (SZ.v (S.len x)) U64.n since
  sizet_fits_u64 returns exactly SZ.v x < pow2 64.

- Raw.Nondet.Compare: the three assumes preceding the map-iterator
  comparison loops were vestigial (nothing in those functions converts
  between size_t and u64); removed.

Full build (cbor/cose snapshots + cbor/cddl/cose tests) passes: 724 + 363
tests, 0 failed. Snapshots intentionally not committed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The mk_string/mk_array/mk_map_from_arrayptr constructors took a raw
AP.ptr plus a U64 length with precondition Seq.length v == U64.v len, and
assumed SZ.fits_u64 just to form uint64_to_sizet len for the slice.

Use the new Pulse.Lib.ArrayPtr.pts_to_len lemma instead: from AP.pts_to a v
it yields SZ.fits (Seq.length v) == SZ.fits (U64.v len), discharging the
weakened uint64_to_sizet exactly. No _from_arrayptr_t precondition change
needed — the bound is recovered from the arrayptr's backing array.

Full build (cbor/cose snapshots + cbor/cddl/cose tests) passes: 724 + 363
tests, 0 failed. Requires the ArrayPtr.pts_to_len lemma added in the Pulse
submodule. Snapshots intentionally not committed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The literal text-string matcher (impl_typ) only used the assume to form
`uint64_to_sizet len` in the length check `S.len s <> uint64_to_sizet len`.
Replace with the portable base-2^15 `not (sizet_eq_u64 (S.len s) len)`
(new real C function added to CDDL.Pulse.Misc alongside the other
size_t<->u64 comparison helpers). No assume, no precondition change.

Full build (cbor/cose snapshots + cbor/cddl/cose tests) passes: 724 + 363
tests, 0 failed. Snapshots intentionally not committed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…AP.pts_to_len

cbor_det_get_string_as_slice materializes the string's arrayptr as a
size_t-length slice. cbor_det_get_string returns an AP.ptr with
AP.pts_to a v', and the postconditions chain (get_string_post: v' == v;
get_string_length_post: Seq.length v == U64.v len) to give
Seq.length v' == U64.v len. So AP.pts_to_len a yields SZ.fits (U64.v len),
discharging the weakened uint64_to_sizet len. No precondition change.

Full build (cbor/cose snapshots + cbor/cddl/cose tests) passes: 724 + 363
tests, 0 failed. Requires Pulse.Lib.ArrayPtr.pts_to_len. Snapshots not committed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
with_cbor_literal_text_string allocates a stack array of
uint64_to_sizet len for a text-string literal, which needed SZ.fits.
This is the one site whose bound is not recoverable at runtime, so it
becomes a static precondition:

- wf_literal (CDDL.Spec.AST.Base): tighten the LTextString case from
  String.length s < pow2 64 to < pow2 16. This propagates automatically
  through wf_elem_typ -> typ_bounded (TElem (ELiteral l) = wf_literal l),
  so the elaborator/tool boundary now enforces it; concrete generated
  literals satisfy it by normalization (2^16 = 64 KB is far above any
  realistic CDDL literal).
- with_cbor_literal_text_string: refine len with SZ.fits (U64.v len) and
  drop the assume. The sole caller with_literal discharges it
  automatically from the tightened wf_literal via FStar.SizeT's
  fits_at_least_16 SMTPat (size_t is >= 16 bits).

The wf_literal definition change perturbs the SMT context of the heavy,
pre-existing elaboration proof array_group_included (rlimit 512, ifuel 8),
tipping it into nondeterministic Z3 'unknown'. Since < pow2 16 is strictly
stronger than < pow2 64 the proof remains valid; stabilize with --retry 5
(F*'s sanctioned recovery for a library-definition change).

This removes the final fits_u64 assumption: the CBOR/CDDL Pulse code now
has ZERO SZ.fits_u64 assumptions. Full build (cbor/cose snapshots +
cbor/cddl/cose tests) passes: 724 + 363 tests, 0 failed. Snapshots not
committed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…arnesses

The generated Rust no longer assumes a 64-bit usize: every u64->usize
widening is now either runtime-guarded by the portable base-2^15 size
check (sizet_fits_u64 / u64_lte_sizet, which reject oversized lengths
gracefully) or bounded by an actual in-memory slice length (so it cannot
exceed usize::MAX on any target). The proofs rest only on
FStar.SizeT.fits_at_least_16, and Rust guarantees usize is at least
16 bits (Reference [type.numeric.int.size.minimum]), so the code is sound
on any Rust target and the >= 64 assertion is obsolete.

Remove the four const_assert!(usize::BITS >= 64) and the now-unused
static_assertions dependency from each crate (cborrs, cborrs-nondet,
evercosign, cborrs-test). Cargo.lock format versions are left untouched
(det/cose v3, nondet/cddl v4). All four crates build; cose tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CDDL.Tool.Gen emitted `assume (SZ.fits_u64)` into every generated .fst,
discharging a SZ.fits_u64 precondition threaded through the
CDDL.Pulse.AST.{Validate,Bundle,Parse} combinator API. That threading
existed solely to convert the `.size` control bounds (WfTStrSize lo/hi)
to size_t at validation (Validate.fst, the sole consumer).

Instead of assuming size_t >= 64 bits, convert the bounds to U64.t (always
valid since hi < 2^64) and compare against the runtime size_t string length
with the portable base-2^15 helpers (u64_lte_sizet / sizet_lte_u64),
matching the already-U64-based serializer side (bundle_str_size). This keeps
`.size` bounds at < 2^64 and is sound on any platform with >= 16-bit size_t
(on a 32-bit target len <= hi is correctly true when hi > 2^32, and lo <= len
correctly false when lo > 2^32).

- CDDL.Pulse.Misc: impl_str_size takes lo hi : U64.t, compares via base-2^15
- CDDL.Pulse.AST.Validate: WfTStrSize uses U64.uint_to_t; drop dead sz_uint_to_t
  and the SZ.fits_u64 conjuncts from the 5 wf refinements
- CDDL.Pulse.AST.Bundle/Parse: drop the now-vestigial SZ.fits_u64 refinements
- CDDL.Tool.Gen: remove the generated preamble assume

Generated COSE + cddl test code now verifies with zero fits_u64 assumptions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The `array_group_included` decision procedure (a single ~180-line
match-based function) intermittently failed in the CI build-cbor-cddl
job while succeeding locally. Reproducing CI faithfully (same pinned
F*/Pulse/Z3 deps image, lowparse admitted, `make cddl cbor-verify
ADMIT_LOWPARSE=1`) showed split-query project-everest#61 deterministically failing at
the default Z3 seed with "incomplete quantifiers" after ~130s, using
only ~30 of its 512 rlimit. This is a context-pollution / seed
sensitivity problem (Z3 saturates without finding the instantiation),
not a resource limit, so neither the prior `--retry 5` band-aid nor a
larger rlimit helps robustly.

Adopt the same stabilization the sibling `CDDL.Spec.AST.Elab.Disjoint.Array`
already uses: `--z3refresh` (fresh Z3 process per query, eliminating
cross-query state pollution) with a pinned seed and `--z3rlimit 4096`.
With `--z3refresh --z3seed 0` the module verifies deterministically;
the slowest query drops from a 130s timeout to ~13s (max rlimit
~68/4096). Validated end-to-end with `make -kj16 cddl cbor-verify
ADMIT_LOWPARSE=1` in the CI deps container (exit 0).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The SZ.fits_u64-removal work in CBOR/CDDL/COSE relies on the F* prerequisite
that weakens FStar.SizeT.uint64_to_sizet from `requires fits_u64` to
`requires fits_u64 \/ fits (U64.v x)` (see PR_DESCRIPTION.md). Every u64->size_t
widening (~29 sites: mk_string_from_array, Iterator.split_trade,
validate_total_constant_size_u64, the map/array count casts in
EverParse.Nondet.Gen, etc.) discharges the cast from a local `fits (U64.v x)`
fact (via A./AP.pts_to_len, S.pts_to_len, the >=1-byte nlist bound, or
SZ.fits_lte) rather than the platform assumption `fits_u64`.

Commit b98f074 "Revert (TEMP) use my F* fork" reverted FStar_hash all the way
to 6099b75c, which predates the weakening, instead of staying on a commit that
includes it. With strict 6099b75c, uint64_to_sizet again requires fits_u64, so
all of those sites fail to typecheck. CI's `make -kj cddl cbor-verify` surfaced
4 of them (CBOR.Pulse.API.Base.fst:2120, CBOR.Pulse.Raw.Iterator.fst:715,
CBOR.Pulse.Raw.EverParse.Format.fst:658,
CBOR.Pulse.Raw.EverParse.Nondet.Gen.fst:1168); the rest were skipped by -k as
downstream of the failures. These pass locally only because the local opt/FStar
still has the pre-revert (weakened) F* binary.

Restore FStar_hash to a22c9932 ("weaken precondition of uint64_to_sizet, etc."),
which is on FStarLang/FStar's fstar1_sizet branch, so the official repo URL is
kept and karamel/pulse hashes are unchanged (the pre-revert known-good config).

Verified in a CI-faithful Docker container (deps image + F* rebuilt at a22c9932,
LowParse ADMIT=1): `make -j16 cddl cbor-verify ADMIT_LOWPARSE=1` completes with
0 errors, including all 4 originally-failing modules.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tahina-pro
tahina-pro marked this pull request as ready for review June 29, 2026 18:13
@tahina-pro
tahina-pro merged commit b3544a1 into project-everest:master Jun 29, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant