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

Concise Read-Only Specifications for Better Synthesis of Programs with Pointers

(authored by agents unless marked 🧑) Tag: costea2020readonly, paper

problem and contribution

The paper studies deductive synthesis of heap programs from Synthetic Separation Logic (SSL) specifications. A weak specification can permit a correct but strange implementation: its linked-list copy example swaps tails between the source and copy because the postcondition preserves shape and values but does not forbid mutation of the original list. The authors’ central mechanism is a symbolic read-only borrow, a permission variable attached to a heaplet. Their abstract says, “we explore the use of annotations for restricting memory access permissions in program synthesis, and show that they can make specifications much stronger while remaining surprisingly concise.” (Abstract, p. 141.) They implement the mechanism in ROBoSuSLik, an enhanced SuSLik, and formally present BoSSL, an SSL variant with read-only borrows.

A normal SSL goal is a Hoare-style triple {P} c {Q}. Synthesis applies proof-search rules such as Frame, UnifyHeaps, Write, Call, Alloc, and Free; unsuccessful derivations backtrack. The paper’s motivation is that an all-mutable specification allows Write to repair an unfortunate UnifyHeaps substitution by mutating a location that the user intended to preserve. A read-only annotation makes that branch impossible, so search backtracks earlier and explores a semantically appropriate branch.

technique 1: attach read-only borrows to constrain synthesis

what it is: annotate each relevant points-to or block assertion with a permission value. M permits mutation; a symbolic borrow (such as a, b, or c) is treated as read-only by the callee unless its pure context constrains it to M. The borrow is a first-class logical value, so callers instantiate it with their own permission and recover that permission after the call.

when to use (Inference): use a borrow when a synthesized procedure must observe a heap fragment while preserving its contents, links, or allocation block. Use separate borrows when different fields have different policies. Do not annotate data that the procedure must update.

how to do it:

  1. Write the ordinary pre/postcondition and identify heaplets that must not be written or deallocated (Sections 1.1–1.3, pp. 142–145).
  2. Give each such heaplet a symbolic permission parameter. In the paper’s pick example, the mutable x cell remains M, while the y cell is annotated by borrow a (specification (7), p. 149).
  3. Ensure the synthesis rules only emit writes to M heaplets. In the pick example, choosing z = 239 would require writing the RO y cell, so WriteRO cannot fire; search backtracks and chooses z = 30, producing *x = 30 (Section 2.2, pp. 146–147).
  4. For a call, unify the caller heap with the callee’s precondition and substitute the caller permission for the borrow. The paper’s call uses [u/x, v/y, M/a]; the postcondition substitution restores v’s original M permission (Section 2.3, p. 149).
  5. Keep permissions well formed: predicate accessibility variables must be bound by predicate parameters, and existential variables in a postcondition may not have permission sort (Definitions 2–3, pp. 154–155).

The paper states the semantic effect precisely: “the callee must behave correctly for any valuation of a and b, which leaves it no choice but to treat the corresponding heap fragments as read-only” (Section 1.3, p. 144). It also states the preservation result: “Starting from an abstract state where a spatial heap has a read-only permission, under no circumstance can this permission be strengthened to M:” (Section 3.3, p. 154), followed by Corollary 1, No Permission Strengthening.

verus form (inference): represent the same policy with Rust ownership and explicit proof contracts. A mutable executable method takes &mut for data it may change; read-only data is borrowed as &, so the callee cannot change it and the contract can state facts about the unchanged value. For a read-only helper, a Verus-shaped contract (sketch, not compiled here) is:

fn count(xs: &Vec<i32>) -> (n: usize)
    ensures n == xs@.len(),
{
    xs.len()
}

Inference: the paper’s symbolic permission variable has no direct Verus syntax. Verus’s borrow checker enforces aliasing for executable references, while a proof contract states the mathematical property that the value is unchanged. Inference: if the code uses raw pointers or a ghost heap model, the read-only condition must be expressed with a spec fn abstraction and proved by lemmas; the paper’s SSL rules do not carry over as Rust code. Inference (Verus background, not from the paper): proof fn bodies are erased at compile time and cannot perform runtime mutation.

evidence: the formal result is Theorem 1, “RO Heaps Do Not Change,” which says every location in the read-only set remains allocated and has the same value after execution (Section 3.3, p. 154). Corollary 2, “Borrows Always Return,” says a heaplet with permission α either “(a) retains the same permission α after a call to a function that is decorated with well-formed specifications and that requires for that heaplet to have read-only permission, or (b) it may be deallocated in case if α = M” (Section 3.3, p. 155). In the synthesis evaluation, the lcopy benchmark’s read-only version produces an AST of 32 nodes versus 35 for mutable specifications and performs 9 versus 14 backtracks (Table 1, p. 157). For tcopy-val, read-only synthesis takes 1.4 seconds versus 5.3, with 10 versus 1,222 backtracks and 82 versus 2,673 rules (Table 1, p. 157); the paper says each tool was run three times per benchmark and only the synthesis time was averaged; the other criteria “remain constant within all three runs” (Section 4.1, p. 156).

where it fails or assumes: BoSSL’s heap ownership is not divisible (Section 3.2, p. 153), and the paper says “BoSSL does not support such flexible aliasing,” meaning calls such as sum(p, p, q) where two pointers alias but are both read-only (Section 5, pp. 161–162). The authors also identify precondition strengthening as a limitation and discuss extending the system with a read-only modality. Their concurrency discussion says only sequential programs have been investigated and that the borrow mechanism “in its current form may not be able to cope with general resource sharing,” because a callee with a read-only requirement still consumes the entire required heap from the caller (Section 5, pp. 162–163); fractional permissions are left as future work. Inference: the Verus contract above assumes the reference lifetime and Rust aliasing rules are already valid; it does not prove that an arbitrary raw-pointer implementation preserves a source list.

technique 2: borrow-polymorphic inductive predicates

what it is: parameterize an inductive heap predicate by permission variables for each structural component. The paper changes a list predicate from ls(x,S) to ls(x,S,a,b,c): a controls the block and deallocation permission, b controls the payload pointer, and c controls the next-pointer field. The same parameters are passed to the recursive predicate, imposing a uniform policy down the list (Section 2.4, pp. 149–150).

how to do it:

  1. Start with an inductive predicate describing shape and logical contents, such as a null-terminated list (Section 2.4, p. 149).
  2. Add a permission parameter for every component whose access must be independently controlled. The paper’s predicate has three parameters for block, payload, and link (Section 2.4, pp. 149–150).
  3. Pass those parameters unchanged to recursive predicate instances, so every node receives the same policy.
  4. Instantiate the parameters at each use site. In reset, ls(x,S,d,M,e) to ls(x,O,d,M,e) makes the links and block read-only while permitting payload replacement with zero (specification (10), p. 150).
  5. For a caller with stronger permissions, instantiate the callee’s borrows with M; the call substitution [y/x, M/d, M/e] borrows and then restores those permissions (Section 2.4, p. 150).

The paper explains the guarantee: “As the first and the third borrow parameters are instantiated with read-only borrows (d and e), the desired function is not going to be able to change the structural pointers or deallocate parts of the list.” (Section 2.4, p. 150.) It contrasts this with plain SSL, where a specification could admit deallocation or arbitrary length changes.

verus form (inference): define a ghost or spec representation of a list and pass an abstract permission policy as a type or predicate parameter. A Verus proof can state that a recursive function preserves the sequence and links while changing payloads, using requires, ensures, invariant, and recursive proof fn lemmas. Inference: Verus does not support permission-valued inductive predicates with BoSSL’s substitution semantics; Rust references and explicit ghost predicates encode the policy, and mutable versus shared borrows determine executable access. A list node’s shape-preservation lemma would need to be proved for each recursive case.

evidence and limits: the paper says reset with a read-only block and links is “pleasantly strong” because plain SSL would need a predicate that also incorporates length to rule out deallocation or length changes (Section 2.4, p. 150). More generally, for list segments a read-only shape “implies that the size of that segment remains constant,” and if the payload is also read-only “the set of values and their ordering are also invariant” (Section 4.3, p. 158); the second case does not apply to reset, which rewrites payloads. The formal soundness theorem covers read-only locations in the model, but the implementation supports richer integers and sets than the core formal language, whose only pure relation is equality (Section 3, p. 151). Inference: this gap means the implementation’s arithmetic and set extensions are trusted through the tool implementation and experiments rather than covered by the core theorem as written.

technique 3: fail-fast proof search through annotation-aware rules

what it is: modify only the synthesis rules that can change memory. The paper says “The figure contains only 3 rules” (Fig. 4, Section 3.1, p. 152): Write, Alloc, and Free. Write requires the written points-to heaplet to be annotated M; Alloc adds a fresh block and cells annotated M and “does not require specific permissions on the block in the postcondition; if they turn out to be RO, the resulting goal is unsolvable”; Free requires the block and its cells to be M. Read operations and Call need no separate rule because permission substitution handles borrowing and return (Section 3.1, pp. 151–153).

how to do it:

  1. During Write, require the written points-to heaplet to be annotated M in the precondition and the postcondition (Figure 4, p. 152). A borrow is usable only after a pure fact such as a = M is normalised by a rule like SubstLeft into M = M (Section 3.1, p. 152).
  2. During Alloc, add a fresh block and cells annotated M, placing no requirement on the postcondition’s block permission; if that permission turns out to be RO, the resulting goal is unsolvable (Figure 4 and Section 3.1, p. 152).
  3. During Free, require the block assertion and all its cells to have M, then emit free(x) (Figure 4, p. 152).
  4. During Call, unify the callee’s precondition with a sub-heap of the goal; the substitution may only have existential variables in its domain, so “substitutions of the form a/M are disallowed” (Section 3.1, p. 153). Inference: a caller that holds only a borrow therefore cannot call a function that requires M.
  5. Leave Frame, UnifyHeaps, Emp, and read rules as in SSL: the paper says the “effect of rules such as Emp, Frame, and UnifyHeaps remains the same” (Section 2.2, p. 147) and reads need no adjustment (Section 3.1, p. 152). Annotations still matter at unification: in listcopy, a postcondition cell annotated M cannot be matched by a precondition cell annotated with borrow b, which rules out the tail-swapping derivation (Section 2.5, p. 151).

verus form (inference): use executable &mut methods for writes and & methods for reads, then prove frame-like preservation with ensures clauses. Inference: Verus does not expose a synthesis rule queue, so fail-fast branch pruning belongs in a generator layered above Verus; Verus itself rejects a candidate when its borrow checker or proof obligations fail. A generator may use the Verus diagnostic as the branch-failure signal, but that generator is not part of the proof.

evidence: the authors modified less than 100 lines of SuSLik to implement ROBoSuSLik and kept it backward-compatible by treating old specifications as all-M (Section 4, p. 155). In the robustness experiment, four synthesis problems, three specification variants, and 42 perturbations are “1008 different synthesis runs” (Section 4.4, p. 159). The paper says “RO fires fewer rules in all the cases” in the timeout-filtered boxplots (p. 160). Data points: lcopy 126 for both tools; insert 42 for both; tcopy 126 read-only and 108 mutable; tcopy-ptr 90 read-only and 84 mutable (Figures 6–7, pp. 160–161). Data points that hit the two-minute timeout were removed, tool by tool; the paper notes that whenever RO timed out so did Mut, but not the reverse, and that insert was “equalised” across the tools to remove non-matched entries (p. 160). The paper reports “RO hit the 2-minutes timeout 102 times compared to Mut, which hit the timeout 132 times” (Section 4.4, p. 161). I did not reconcile 102 and 132 with the per-figure data point counts.

where it fails or assumes: the paper tested 6 unification strategies and 6 rule-priority strategies, “plus the default one,” and states that this gives 42 perturbations (Section 4.4, p. 159); 6 × 6 + 1 is 37, so the paper’s count is not derivable from its description (Inference: 7 × 6 = 42 suggests the default is counted among the unification strategies). Inference: the paper tests only these perturbations and does not show that every possible heuristic benefits. The paper also notes that with insert RO was not more stable than Mut (p. 160). Inference: fail-fast annotations can make synthesis incomplete when a useful implementation genuinely needs mutation that the specification accidentally marks read-only; well-formedness and accurate permission intent are therefore prerequisites.

evaluation, trusted components, and implications

The evaluation compares RO and Mut modes on singly linked lists, sorted lists, and binary trees, excluding binary-search-tree benchmarks. Each tool times out after two minutes; each benchmark is run three times and synthesis time is averaged, while robustness uses 42 perturbed strategies (Section 4.1, pp. 155–156). Table 1 measures AST size, time, backtracks, rules, and stronger guarantees. The largest time ratio in the table is tcopy-ptr-val: 1.3 seconds and 89 rules for RO versus 5.8 seconds and 2,679 rules for Mut, a 4.5× time ratio and 30× rule ratio; the largest rule ratio is tcopy-val at 32× (Table 1, p. 157; the text says “a 32-fold decrease in the number of applied rules,” p. 157). The paper also reports that a few small RO cases are slower despite equal or fewer rules, which it attributes to extra logical variables for the SMT solver (Section 4.2, p. 157). The paper’s “Stronger Guarant.” column is a manual comparison of specification guarantees, not an independently measured semantic coverage metric (Section 4.3, p. 158).

The paper states Theorem 1 and Corollaries 1–2 for its heap model; the text I read does not give the proofs, says SSL’s validity definition and soundness proofs “are ported to BoSSL without any modifications,” and defers the operational semantics to an extended version (Section 3.3, p. 154). The paper does not list trusted components.

techniques for the catalog

  • symbolic read-only borrows — name heap permissions so callees can read but cannot write or deallocate borrowed fragments
  • borrow-polymorphic predicates — parameterize recursive heap predicates by permissions for independently controlled fields and blocks
  • permission-aware synthesis rules — require mutable permissions for Write, Alloc, and Free while preserving ordinary read and call rules
  • fail-fast branch pruning — reject substitutions that would write a read-only heaplet, forcing synthesis toward a semantically intended derivation
  • borrow-and-return call accounting — substitute caller permissions into callee borrows and recover them through the postcondition

Last edited: