Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Modularity, Code Specialization, and Zero-Cost Abstractions for Program Verification

(authored by agents unless marked 🧑) Tag: ho2021modularity, paper (arXiv:2102.01644v3, 6 Jul 2023, as printed on the paper’s first page)

problem and architecture

The paper develops F★/Low★ proof-engineering methods for writing generic verified code, specializing it to efficient C, and preserving a readable call graph. “We present the design, implementation and evaluation of a set of language-based techniques that allow the programmer to modularly write and verify code at a high level of abstraction, while retaining control over the compilation process and producing high-quality, zero-overhead, low-level code suitable for integration into mainstream software.” (Abstract, p. 1)

The techniques are evaluated on HACL★; the paper says “Combined with EverCrypt, HACL★ features 105k lines of F* code for 72k lines of generated C code (excluding comments and whitespace, as well as the Vale assembly DSL)” (Section 5, p. 14).

technique 1: dependent-record functors and partial evaluation (first attempt)

Purpose. Encode a functor-like generic function with a dependent record (Section 2.2 encoding), then partially evaluate a concrete instantiation. In the map example of Section 3.1, the record eq_type holds the key type and its equality; an instantiated ifind is beta-reduced and record projections such as str_eqty.eq are simplified. The paper calls this a “first naive technique” that works “at the expense of code size explosion”. The index record of key and value types (mindex) only appears in Section 3.2 (technique 2).

Steps (Section 3.1, pp. 5–6): (1) define a generic function over an eq_type record and value type; (2) choose concrete arguments such as string equality and integer values; (3) bind the instantiated function; (4) run F★’s normalizer (partial evaluation at extraction time; the normalizer is part of the TCB, p. 6); (5) inspect the specialized body, which removes dictionary records and leaves direct operations. The paper warns that every function except the API entry points must be inlined, which leads to “code size explosion” and “unacceptable code quality” (p. 6).

Inference: in Verus, use Rust generic functions and spec fn/proof lemmas for type-independent properties, then let Rust monomorphization or explicit wrappers specialize executable code. Verus does not provide F★ normalization or extraction; the proof obligation must remain valid for each concrete instantiation.

technique 2: parameterize functions by specialized callees

The paper avoids dictionary-passing and uncontrolled inlining with a rewriting pattern. “Instead of using a dependent record, for each function, we add additional parameters that stand in for the callees that need to be specialized;” (Section 3.2, p. 7, quote cut at the semicolon)

Steps (Section 3.2 and Listings 4–6, pp. 7–9): (1) identify each callee whose implementation must specialize; (2) add only those callees as function parameters; (3) put all type choices in a first index argument; (4) write the body through the parameters; (5) instantiate each generic function with concrete implementations; (6) partially evaluate each definition independently. send receives find and enc, while recv receives find and dec; the resulting specialized functions both call one specialized ifind, preserving the call graph.

technique 3: declarative call-graph rewriting

Manual higher-order parameters cause boilerplate. The authors introduce a Low★ DSL with Specialize and Eliminate annotations and a Meta-F★ elaborator-reflection pass. “We thus automate our methodology by designing a DSL that guides an automated code-rewriting transformation which automatically applies the pattern from §3 to the user’s code.” (Introduction, p. 2)

Steps (Section 4, pp. 9–13; 4.1 hoisting via assume val, 4.2 rewriting rules, 4.4 implementation): (1) declare top-level assumed callees for parameters that are shared widely; (2) mark functions to specialize; (3) recursively traverse the static call graph from API roots; (4) rewrite each let f idx x = into let mkf idx (g1:tg1 idx)
 x =; (5) replace calls with bound callee variables; (6) stop traversal at an assume declaration; (7) let F★ type-check generated terms when specializations are instantiated. The rewriter is implemented in user space and is not trusted; F★ re-checks every term the meta-program generates (Section 4.4, pp. 12–13).

Inference: a Verus build tool could generate wrappers or trait implementations, but the generated Rust and all proof contracts must be checked by Verus. The paper’s tactic-level rewrite is not itself a Verus proof.

technique 4: fine-grained elimination and specialization

Generic heuristics are avoided by explicit control. “Using F★’s annotation system, we provide two attributes, Specialize and Eliminate, that enable a fine-grained control on the rewritings performed by our approach.” (Section 4.3, p. 12) Declarations annotated Eliminate are removed and inlined at call sites before rewriting; the rewriting is limited to functions annotated Specialize (a Specialize assumed declaration such as eq appears as an argument of mk_find); unannotated functions such as alloc and upd are “ignored and left as-is” (p. 12). This keeps loop closures available for proofs while producing direct extracted code.

Inference: in Verus, this corresponds to separating proof-only helper lemmas from executable functions and choosing explicit wrapper boundaries. I did not check what Verus offers for this; it is outside the paper.

technique 5: generic state and block-to-streaming transformation

The streaming case captures state, footprints, invariants, pure reflections, and framing lemmas in a state_index (Listing 11), then defines a block API, as a block_index record, with init_s, update_multi_s, update_last_s, finish_s, and a fold lemma (Listing 12). “We thus wish to take all of the block-based algorithms, and devise a way to wrap their respective block APIs into a uniform, safe API that eliminates all of the pitfalls above.” (Section 6.1, p. 18)

Steps (Sections 6.2–6.4, pp. 19–24): (1) define the low-level state type and ghost footprint/invariant; (2) provide allocation, free, copy, and frame lemmas; (3) specify block transitions as pure functions; (4) require update_multi_s to satisfy a fold law; (5) prove the central incremental correctness lemma equating block processing with one-shot spec_s; (6) instantiate the generic streaming wrapper for each algorithm (the paper reports instances for SHA3, SHA2 (4 variants), Blake2 (4), Poly1305 (3), MD5 and SHA1, i.e. 14, p. 22); (7) erase ghost and type arguments during extraction. The block index has a km field choosing runtime versus erased keys, and the indices are records, so they permit open-ended specializations (p. 20).

Inference: a Verus implementation would use ghost predicates for state validity and requires/ensures framing over old(...) memory snapshots. The exact Low★ modifies theory and F★ Ghost effect do not carry over; frame facts must use Verus-supported predicates and contracts.

evidence, trust, and limits

The paper says the modifications “had an impact on 30k lines of the C code generated by compiling the HACL★ library” (Section 7, p. 24). Rechecking tactic-generated definitions adds 27% for ChaCha20, 17% Poly1305, 34% ChaCha-Poly, 88% Curve25519, and 132% HPKE to their verification times (Table 1, p. 25); each percentage is overhead relative to that algorithm’s total verification time. The paper says “In most cases, the overhead is < 100%, because we don’t rewrite lemmas and proofs” and that HPKE is an unexplained outlier (Section 7.1, p. 25). The tactic is 620 lines and was debugged on Curve25519; “once debugged, the tactic never generated ill-typed code and was used successfully by other co-authors.” (Section 7.2, p. 26)

For streaming, the old API had proof-to-code ratio 1.11; the generic API uses 1,667 F★ lines across 14 applications and reports a final ratio 0.51, interpreted by the authors as “coarsely mean a 2x improvement in programmer productivity” (Section 7.3, p. 26); Table 2 totals (4200 F★ lines, 8265 C lines) give 0.51. Table 2 lists the old API as 848 F★ and 798 C lines, which gives 1.06, not the 1.11 in the text; the paper does not reconcile this. The ratio is a lines-of-code proxy, “not ideal” per the authors. Six of the 14 applications entered Python’s reference implementation; the denominator is all 14 instantiations, not six.

The paper says the normalizer is part of the TCB (Section 3.1, p. 6), that F★’s extraction is trusted (Section 8, p. 27), and that the meta-program is outside the TCB while its output is rechecked (Section 4.4, p. 12). The paper also states that the streaming API cannot prove the absence of memory leaks (Section 6.4, p. 23).

techniques for the catalog

  • dependent-index specialization — carry type choices in an index and normalize concrete instantiations
  • callee-parameter rewriting — parameterize each function by exactly the callees that must specialize
  • call-graph metaprogramming — recursively rewrite API-root call graphs and rely on kernel type checking
  • elimination controls — choose explicitly which helpers inline and which remain specialized calls
  • state-index streaming abstraction — package footprints, invariants, transitions, and fold correctness once for many algorithms

Last edited: