Product Programs in the Wild: Retrofitting Program Verifiers to Check Information Flow Security
(authored by agents unless marked š§) Tag: eilers2021product, paper
scope and result
The paper retrofits existing intermediate-verification-language (IVL) verifiers to prove information-flow security, especially noninterference, by constructing a modular product program after source-to-IVL encoding. The target architecture reuses the source frontend and proof-search backend; the product pass is shared by all verifiers using the IVL. The paperās abstract states: āMost existing program veriļ¬ers check trace properties such as functional correctness, but do not support the veriļ¬cation of hyperproperties, in particular, information ļ¬ow security.ā [Abstract, p.718]
The authors make three claims. First, independently sound encoding and product construction can compose unsoundly; an operationality condition on the encoding is sufficient for soundness. Second, sequential IVL products can prove possibilistic and probabilistic noninterference for data-race-free lock-based concurrent programs, with additional lock and branch obligations. Third, an implementation in Nagini/Python/Viper verifies the evaluated examples, with usually acceptable but sometimes large slowdowns. The abstract describes the three dimensions as: ā(1) Soundness: We show that the combination of an encod- ing and a product construction that are individually sound can still be unsound, and identify a novel condition on the encoding that ensures overall soundness. (2) Concurrency: We show how sequential product programs on the intermediate language level can be used to verify information ļ¬ow security of concurrent source programs. (3) Performance: We implement a product construction in Nagini, a Python veriļ¬er built upon the Viper intermediate language, and evaluate it on a number of challenging examples.ā [Abstract, p.718]
technique 1: modular products at the IVL boundary
What it is. A product duplicates every variable and parameter into two renamed executions and uses activation variables to represent which execution is active at each point. Unlike self-composition, it keeps loops and calls single and encodes divergent control flow with activation variables. The paper says: āUnlike self-composition, which achieves this eļ¬ect by simply duplicating every statement, modular product programs [18] do not duplicate loops and method calls, and instead encode diļ¬ering control ļ¬ow through activation variables, which repre- sent, for each execution, whether or not it is active (i.e., it executes the code) at the current point in the program.ā [§2.2, p.721]
When to use it. The paper targets 2-hyperproperties such as noninterference for verifiers that already encode a source language into an IVL (§1, p.719; §5.1, p.735). The construction also supports termination-sensitive noninterference, value-dependent sensitivity, and a form of declassification (§2.2, p.723).
How to apply it, following the paper.
- Keep the source frontendās normal encoding into a small sequential IVL and retain the backend proof search. Extend the specification language so relational assertions such as
low(e)are carried through as ordinary predicates; the frontend need not interpret their meaning (§3.1, p.723). - Duplicate parameters and variables and add boolean activation variables
p1,p2at each product procedure. For a conditional, create branch activations from the incoming activation and each executionās condition (§2.2, p.722). - Guard primitive assignments and other statements by the current executionās activation. Execute one method call when at least one execution is active, passing both activation values; the callee changes only the active executionās state (§2.2, p.722).
- Encode a unary predicate
Qasp1 => Q(1) && p2 => Q(2). Encode a relational precondition such as low inputzasp1 && p2 => z1 = z2; encodelowEventasp1 = p2(§2.2, p.722). - Supply relational method contracts and loop invariants, so one product call can be verified modularly. This preserves the method-level reasoning that self-composition loses (§2.2, p.722; §1, p.719).
Verus form (inference). In Verus, duplicate the mathematical state in a ghost/spec layer and prove a relation between two executions with requires, ensures, and invariant clauses. Keep executable Rust single-copy where possible, and write a proof function that reasons about two abstract states; Verus has no paper-defined product-program transform, so an implementation would need explicit paired state or a generated paired wrapper. low and lowEvent become predicates over the paired state. The paperās activation variables and relational contracts carry over conceptually; its Viper AST pass and Python encoding do not carry over unchanged.
Evidence. The paperās Nagini evaluation verified the sequential and concurrent examples in Tables 1 and 2. Table 1 reports each programās total LOC (implementation plus specification), annotation LOC, property, and verification time averaged over five runs; the 27 listed rows (14 in the left block, 13 in the right) range from 3.55 s to 11.35 s, which the text rounds to ābetween 3 and 12 s eachā. Table 2 compares five SecC examples, with Nagini times of 4.21 s, 52.20 s, 5.41 s, 182.60 s, and 4.76 s, each averaged over five runs (§5.3, pp.736ā737). These numbers are for Naginiās Python/Viper setting, not Rust or Verus.
technique 2: operationality as a prerequisite check on the encoding
What it is. A source-to-IVL encoding must preserve operational behavior when relational assertions are later interpreted by the product pass. An encoding is represented by (alpha, sim, beta): statement encoding, state correspondence, and assertion encoding. The paper defines operationality with two obligations: every source step is simulated by zero or more IVL steps between corresponding states, and source assertions hold exactly when their encoded assertions hold. The complete criterion is stated as: āWe call an encoding that preserves the operational behavior of the source program operational: It encodes every step of the source program into some number of steps of the target program so that c initial states result in matching end states.ā [§3.3, p.725]
When to use it. Check this condition before applying an IVL product to any frontend encoding that replaces code by a verification condition, assertion, or assumption. The paperās counterexample encodes a conditional assignment directly as an assertion of a sufficient precondition. That is sound for unary assertions but fails for low(y): each execution can prove its own value low while the two values differ (§3.2, pp.724ā725).
How to apply it.
- Identify each non-operational frontend rewrite; the paperās examples are local blocks, trait calls, loops, dynamically-bound calls, and concurrent encodings (§3.4, p.726).
- Define the source/IVL state correspondence and check that every source transition has a matching IVL transition sequence and that assertions are preserved (§3.3, p.725).
- If the rewrite is operational, Theorem 1 gives relational soundness. If not, prove relational soundness directly or replace the rewrite with an operational encoding or a different non-operational encoding that is sound for relational specifications (§3.3, p.725; §3.5, p.727).
- Construct the product before any later desugaring of calls into pre- and postconditions: the paper says the IVL verifier may later reason about calls via pre- and postconditions āas long as the product program is constructed before such a desugaring stepā (§3.5, p.726). Inference: the same reasoning means an encoding that has already erased the state changes a relational assertion depends on (as in Fig. 3, right) should not be fed to the product (§3.2ā§3.3, pp.724ā725).
Verus form (inference). Treat each Rust function body that a paired proof will inspect as executable semantics, and ensure any spec-only summary is relationally sound for two states. A Verus ensures summary can be used at a call only when its relation is proved for both executions; replacing a body with an unrelated unary fact has the same risk as the paperās assertion-only encoding. Use proof fn lemmas over paired states and requires/ensures to state the simulation relation. This is an inference because the paper does not discuss Verus.
Evidence and limits. Theorem 1 states that operationality is sufficient for relational soundness, not necessary (āNote that operationality is a suļ¬cient but not necessary conditionā, §3.3, p.725); no proof of the theorem appears in the paper text. Operationality allows the encoding to overapproximate the source behaviors but not to miss any (§3.3, p.725). Inference: the criterion is therefore a screening test, not a completeness result. The paper says that āIn most existing frontends, the encoding of virtually all source language con- structs is operationalā, names VCCās encoding of local blocks, Dafnyās encoding of calls on traits, Prustiās encoding for loops and Naginiās encoding of dynamically-bound calls as non-operational examples, and says that all encodings of concurrent source languages into sequential IVLs necessarily have some non-operational elements (§3.4, p.726). It also notes that some non-operational encodings, such as Vercorsās for parallel blocks, can be seen as simplified versions of an operational encoding and so can be quickly identified as relationally sound (§3.3, p.725).
technique 3: sound dynamic-dispatch encoding
The problem. A dynamically bound call cannot be represented as an ordinary IVL call when the runtime target is unknown. Existing frontends instead assert the statically selected methodās precondition and assume its postcondition. With relational postconditions this is unsound: two executions can dispatch to different overrides with different concrete results while both satisfy the same unary contract (§3.5, pp.726ā727).
The repair, step by step.
- Retain behavioral-subtyping checks for every override, so an override cannot return an arbitrary secret while claiming the base contract (§3.5, p.728).
- For a relational postcondition
low(e), assume it at the call only when the receiverās dynamic type is low, encoded asp1 = p2 && type(r1) = type(r2) => e1 = e2(§3.5, p.728). - For a relational precondition
lowEvent, require both equal activations and equal receiver types:p1 = p2 && type(r1) = type(r2)(§3.5, p.728). - Leave ordinary low-argument preconditions unchanged, because their requirement does not depend on which override runs (§3.5, p.728).
- Accept incompleteness: two different receiver types may still select the same implementation; requiring low receiver values instead of low types is a sounder but more restrictive approximation (§3.5, p.728).
Verus form (inference). Model a trait-object or enum dispatch relation explicitly in the paired spec state. Guard a relational postcondition with equality of the dispatch tag, and retain a separate lemma that every implementation refines the trait contract. Verus trait support and dynamic dispatch details differ from Nagini, so the exact encoding is not transferable.
Evidence. Theorem 2 states relational soundness for x := r.m() assuming all implementations satisfy their specifications and behavioral subtyping (§3.5, p.728). No independent benchmark isolates this repair; its evidence is the theorem and the Figure 4 counterexample.
technique 4: concurrent possibilistic noninterference
Applicability. The paper assumes data-race-free concurrent programs using locks. The paper says a product construction applied directly to concurrent programs āwould have to faithfully represent all combinations of potential thread interleavings, which makes veriļ¬cation infeasibleā (§4, p.728). Instead the frontend uses a concurrent-separation-logic or ownership encoding that reasons between synchronization points. Lock invariants are extended with relational assertions, and lock operations receive extra checks (§4.1, pp.729ā730).
Use these four obligations at every lock operation, exactly as the paper lists them: ā1. Every lock operation o is a low event, i.e., if a thread executes o in the ļ¬rst execution, it will also execute o in the second execution. 2. Termination of the local code before the lock operation does not depend on secret data; i.e., if lock operation o is reached in the ļ¬rst trace, it will also be reached in the second trace. 3. o operates on the same lock in both executions, i.e., the lock is low. 4. If o releases the lock, i.e., makes a new lock state public, this lock state fulļ¬lls the relational invariant, meaning that heap operations meant to be low are indentical in both executions after the lock operation.ā [§4.2, p.731]
Procedure.
- Prove data-race freedom and associate each lock with the protected heap and an invariant (§4.1, p.729).
- Make the invariant relational where protected values must be low; assume it on acquire and prove it again on release (§4.1ā§4.2, pp.729ā731).
- Assert
lowEventandlow(lock)around acquire/release; assert low loop guards to ensure termination before each lock operation is secret-independent (§4.2, p.731). - Apply the ordinary sequential product to each lock-delimited segment; the induction over trace segments supplies possibilistic noninterference (§4.2, p.731).
Verus form (inference). Use a Verus state-machine or lock protocol with a relational ghost invariant for the protected abstract state. Prove acquire/release transitions preserve the relation and require equal lock identity and low control predicates. The paperās scheduler and lock encoding are not Verus constructs; Verusās transition-system proof is the closest available form.
Evidence and limits. The proof is for possibilistic noninterference, which preserves reachable low outputs but permits secret-dependent probabilities. The paper gives Figure 7ās secret-dependent release and Figure 8ās secret-dependent lock choice as violations that the four obligations reject (§4.2, pp.732ā733). It assumes race freedom and lock-based synchronization; it does not establish security for arbitrary concurrent semantics.
technique 5: concurrent probabilistic noninterference
Probabilistic noninterference additionally requires that low-equivalent initial states produce the same low outputs with the same probabilities (Definition 5, §4.3, p.733). The paperās Figure 9 shows why possibilistic security is insufficient: the same outputs remain possible but their probabilities differ (§4.3, p.733). The repair is to assert that every branch condition is low, including implicit branches such as dynamic-call receiver types, and still require acquired and released lock references to be low (§4.3, pp.733ā734).
Steps.
- Start with the race-free lock encoding and relational lock invariants.
- Assert every explicit and implicit branch condition is low, forcing paired executions to follow the same code path (§4.3, pp.733ā734).
- Assert low lock references on acquire and release;
lowEventchecks become redundant because low branches imply them (§4.3, p.734). - Apply the unchanged sequential IVL product. The frontend changes are assertions and relational specifications; the IVL and product pass need no concurrency-specific extension (§4.3, p.734).
Verus form (inference). Add paired-state equality for branch predicates and lock references as requires/ensures obligations around the state-machine transitions. Prove the probabilistic model separately: ordinary Verus safety proofs do not establish scheduler probability distributions. Thus the paperās logical condition can guide a Verus abstraction, but a probability semantics and theorem are additional work.
Evidence. Nagini verified the five SecC comparison examples, including PR (probabilistic noninterference) rows, in Table 2. The tableās SecC CAV, CDDC, and CT rows have Nagini times 4.21 s, 52.20 s, and 5.41 s, respectively, each averaged over five runs (§5.3, p.737). The CDDC case is 278 Nagini LOC and 105 annotations; those denominators are the tableās implementation-plus-specification and annotation counts, not a Rust measure.
what is proved, measured, and trusted
Theorems 1 and 2 state relational soundness under operationality and the repaired dispatch assumptions; the paper text contains the statements but no proofs. For possibilistic noninterference the paper gives only a one-sentence argument (āThe proof goes by induction on the number of segments of the traces and leverages the soundness of sequential veriļ¬cation within each segmentā, §4.2, p.731); for the probabilistic case it says the property ācan be veriļ¬ed using simple assertionsā (§4.3, p.734) without a formal proof. Both rely on race freedom, locks, relational invariants, and the listed low-control obligations, and the paper says it focuses āon the high-level concepts, instead of formalizing them for one speciļ¬c settingā (§4, p.729). The evaluation measures verification time in Naginiās Python-to-Viper pipeline. On Naginiās functional test suite, āAll tests report the same results with and without the product transformationā, and the paper reports less than 11% slowdown for half the tests, less than 30% for three quarters, and less than 100% for 90% (§5.2, p.735). For five test cases the slowdown is a factor between 5 and 12, and a single quicksort outlier has a factor of 17.5 and a verification time of two minutes; the authors write āWe believe that the main reasonā is the use of quantifiers in their specifications (§5.2, p.735). The Table 2 DB example (Nagini 182.60 s versus 6.23 s without the product) is attributed to āa large number of quantiļ¬ersā (§5.3, p.737).
Inference: The trusted base includes the correctness of the source frontend, Viper semantics and product pass, Z3, the lock/race-freedom encoding, and the specifications and probability model. The paper does not measure proof effort in Verus, Rust, or a production deployment. Its SecC timing comparison is confounded by language features and annotation styles: the authors state that ādirect performance comparisons between the two are diļ¬cultā because Nagini and SecC differ in āsupported language features, automation (see the table for required annotations), and speciļ¬cation stylesā (§5.3, p.737). They also report that unmodified Nagini already takes more time than SecC on four of five examples (§5.3, p.737).
techniques for the catalog
- IVL modular product: duplicate two executions with activation variables, relational contracts, and shared calls/loops
- operationality prerequisite check: require source-to-IVL step simulation and assertion preservation before product construction
- dynamic-dispatch repair: guard relational postconditions and low events by equality of receiver dynamic types
- possibilistic concurrency checks: enforce low lock events, low termination, equal lock identity, and relational release invariants
- probabilistic concurrency checks: additionally make every explicit and implicit branch condition low while retaining low lock references
Last edited: