Skip to content

refactor prepareSCMP - #425

Draft
jcp19 wants to merge 43 commits into
masterfrom
claude/preparescmp-verification-plan-xwm6n1
Draft

refactor prepareSCMP#425
jcp19 wants to merge 43 commits into
masterfrom
claude/preparescmp-verification-plan-xwm6n1

Conversation

@jcp19

@jcp19 jcp19 commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

Generated by Claude Code

claude added 30 commits July 9, 2026 22:43
Analyzes whether Mem(ubuf) can be established for the layers passed to
gopacket.SerializeLayers in prepareSCMP (it cannot, for any buffer, with
the current predicate definitions) and lays out a nil-buffer
'serialize-mode' predicate design, the required SerializeTo/interface
spec changes (FixLengths, error-case preservation, checksum permission
accounting), a route to derive !IsSupportedPkt(result), and milestones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
Executable Go code cannot change, so: keep the gopacket.SerializeLayers
call and give it a trusted quantified spec; derive !IsSupportedPkt via a
ghost pure hook on the SerializableLayer interface plus a gopacket-level
IsSupportedRawPkt twin and bridging lemma; handle the internalIP
wildcard problem with asymmetric permission amounts in the serialize-mode
predicates (SetSrcAddr switched to wildcard mode via its ghost argument)
and strengthened SetDstAddr/SetSrcAddr postconditions. Drop the
overflow-bound concern (CI runs with overflow checking disabled).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
Generalize BaseLayer.Mem with a fresh mode (ub == nil): a freshly
constructed, not-yet-decoded layer holds only the field permissions, with
zero-valued Contents/Payload, instead of the buffer-aliasing facts. This
makes Mem(nil) foldable for freshly constructed SCMP and SCMP message
layers, a prerequisite for serializing them in prepareSCMP.

- extnBase.Mem pins ActualLen == 0 in fresh mode so all facts previously
  derivable for a nil buffer are preserved.
- LayerPayload specs over BaseLayer are conditionalized on ub != nil
  (their conclusions still imply the gopacket.Layer/DecodingLayer
  interface contracts).
- DecodeFromBytes of SCMP and the seven SCMP message types now ensure
  their minimum length on success, so decoded-mode proof sites can
  derive data != nil and keep the aliasing facts (asserts added at the
  traceroute handler's unfolds).
- FoldFreshMem witness lemmas for SCMP and all seven message types
  machine-check that fresh layers satisfy Mem(nil).
- Add gopacket.IsSupportedRawPkt (twin of the slayers definition, which
  gopacket cannot import) plus a bridging lemma, needed later to thread
  the router's !IsSupportedPkt postcondition through SerializeLayers.

Annotations/contracts only; no executable code changes. Not yet run
through Gobra (unavailable in this environment) - needs a CI run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
The SerializableLayer interface previously only guaranteed Mem(ubuf) and
the buffer resources when serialization succeeded. All implementations
in fact keep them intact on error (SCMP and the SCMP message types fail
only before unfolding Mem or re-fold it before every error return;
Payload and BFD are trusted stubs; SCION's postconditions were already
unconditional), so the guarantee can be made unconditional. prepareSCMP
will rely on this to restore its resources on the error path of
SerializeLayers.

Also record in the plan that (*Decoded).Widen already provides the
buffer-transfer lemma needed to re-fold the reversed path as Mem(nil).

Annotations/contracts only; no executable code changes. Not yet run
through Gobra (unavailable in this environment) - needs a CI run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
Instead of adding a nil-buffer mode to SCION.Mem (which would make
Mem(nil) satisfiable and thereby force nil-handling into every pure
function and lemma that unfolds the predicate - they currently verify
vacuously for nil), give freshly constructed SCION headers a separate
predicate:

- SCION.MemSerialize: field permissions, Path.Mem(nil) (satisfied for
  reversed paths via (*Decoded).Widen), fractional raw-address
  permissions that coexist with ChecksumMem, and the path pool. No facts
  about HdrLen/PayloadLen, which FixLengths computes during
  serialization.
- SCION.IsSupportedSerialization: whether serializing this header
  yields a supported packet, expressed over PathType/NextHdr.
- ChecksumMem re-fractioned (R25; wildcard for RawSrcAddr's bytes,
  which prepareSCMP can only obtain at wildcard amount from the
  data-plane's internal IP); computeChecksum/pseudoHeaderChecksum
  weakened to match (their only verified caller is SCMP.SerializeTo).
- SerializableLayer gains MemSerialize and IsSupportedSerialization;
  trivial implementations for SCMP, the SCMP messages, Payload and BFD.
- SerializeLayers gets a real trusted contract (replacing requires
  false): MemSerialize for the header layer, Mem(layerBufs[i]) for the
  rest, resources preserved also on error, and the trusted bridge
  IsSupportedRawPkt(w.View()) == old(layers[0].IsSupportedSerialization()).

With this design the FixLengths relaxation of the SerializableLayer
interface is unnecessary and verifying SCION.SerializeTo's FixLengths
branch becomes optional hardening rather than a prerequisite: the
fresh-flow serialization is axiomatized by the trusted SerializeLayers
contract, documented as the load-bearing trusted assumption.

Annotations/contracts only; no executable code changes. Not yet run
through Gobra (unavailable in this environment) - needs a CI run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
- SetDstAddr (IP case) no longer applies packAddr's magic wand
  internally; it exposes the R20 fraction of the bytes underlying
  RawDstAddr together with the wand to give it back, so that
  prepareSCMP can store byte fractions in the fresh header's
  MemSerialize/ChecksumMem predicates during serialization and still
  restore dst.Mem() afterwards. Its only verified caller is
  prepareSCMP's currently-unverified tail, so the post shape is free to
  change.
- packAddr/SetDstAddr/SetSrcAddr gain 'T4Ip implies length 4'
  postconditions, provable from To4's contract.
- Add FoldFreshMemSerializeWithChecksumMem: the machine-checked witness
  that MemSerialize and ChecksumMem fold together from the resources
  prepareSCMP holds (fraction accounting 2xR25 within R20, wildcard
  source bytes, path with nil buffer).

Annotations/contracts only; no executable code changes. Not yet run
through Gobra (unavailable in this environment) - needs a CI run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
The pairwise-distinctness clause dereferenced layers[i]/layers[j] before
the quantifier granting permission to the slice cells; contract
well-formedness is checked left to right, so all three CI jobs failed
with 'Permission to layers[i] might not suffice' (writer.gobra:153).
Reorder the clauses, permissions first, matching decodeLayers.

This was the only error in the entire run: router/ verified across all
10 chops otherwise, and every package that ran before the error was
clean (pkg/slayers had not run yet - verify-deps aborts at the first
package importing gopacket).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
pkg/slayers reported two errors on the previous run (everything else,
including the router job, was green):

- scion.go:684: the postcondition wand over s.RawDstAddr could not be
  matched against the wand instance created inside packAddr (which is
  shaped over packAddr's own variables; Silicon matches wand instances
  structurally). Re-package the wand in SetDstAddr's body with exactly
  the postcondition's shape, applying packAddr's wand inside the
  package block.
- scion_test.gobra:75: testDstSetter expected the full dst.Mem() back
  immediately; under the new contract the caller first applies the
  returned wand to give back the loaned byte fraction. Adapting the
  test also documents the intended usage of the new postcondition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
CI showed that exposing the RawDstAddr byte fraction through
SetDstAddr's postcondition cannot work: a magic wand whose assertion
dereferences a field is not self-framing, so it can neither be stated
in the postcondition (Silicon cannot match it against packAddr's
variable-shaped instance) nor re-packaged in the body ('Permission to
s.RawDstAddr might not suffice'), and a ghost out-parameter would
require restructuring the real call sites. Restore the original
acc(dst.Mem(), R18) contract (keeping the new T4Ip length
postconditions) and revert testDstSetter.

The plan now designates the caller-side extraction for M5: unfold a
fraction of dst.Mem(), transfer the element permissions to RawDstAddr
via SetDstAddr's pointwise aliasing postconditions, and package the
restoring wand over locals - the verified addEndhostPort pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
prepareSCMP is now fully annotated end to end; the trusted-assumption
TODO() marker is gone. The proof:

- closes the path section early (mirroring the error cases) and refolds
  the SCION layer, then re-associates the reversed path with a nil
  buffer via the new (*Decoded).ChangeUbuf (Mem is buffer-independent;
  Widen requires a superslice and does not apply);
- carves the source-address bytes out of ub via the new ExtractAccSrc
  (mirroring resolveLocalDst's use of ExtractAcc) and, after
  SetDstAddr, extracts a fraction of the destination-address bytes from
  srcA via the new ExtractIPBytes (addEndhostPort-style wand);
- calls SetSrcAddr in wildcard mode; packAddr/SetSrcAddr now take the
  resources of wildcard IP addresses component-wise, since Mem() cannot
  be folded at a concrete amount for the wildcard-held internal IP.
  net.IP.Mem()'s length constraint (4 or 16), exposed through the new
  getInternalIPMem getter, provides the checksum evenness facts
  together with new length postconditions on packAddr/SetSrcAddr/
  SetDstAddr/parseAddr/SrcAddr/PackWithPad;
- folds MemSerialize + ChecksumMem + the SCMP header's and payload's
  Mem(nil), builds the ghost layerBufs (now a seq[[]byte]; payload
  buffers at fraction R55 so the quote can be carved from the split
  ub), and invokes SerializeLayers against its trusted contract;
- derives !IsSupportedPkt(result) from the contract's IO bridge, the
  IsSupportedRawPktEqGopacket lemma, and revealing both definitions;
- restores all resources on every path (the SerializeLayers contract
  preserves them also on error).

packSCMP receives the payload layer's resources field-wise (it is
constructed inline at the call sites, where nothing can be folded) and
folds Mem(nil) via the new slayers.FoldFreshSCMPPayloadMem; the
rawPkt === ub relation is threaded through the ten functions between
process/processPkt and packSCMP for the quote.

Annotations/contracts only; no executable code changes (go build
unchanged). Needs CI iteration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
Gobra crashes with 'Logic error: found unexpected type: seq[[]byte]'
when desugaring an untyped nil inside a seq composite literal
(Desugar.litD -> nilType). Bind a typed nil []byte variable first and
use it as the element.

verify-deps and verify-third-party-libs were green on the M5 commit:
all slayers/gopacket lemmas (ExtractAccSrc, ExtractIPBytes,
FoldFreshSCMPPayloadMem, the seq-based SerializeLayers contract, and
the address-setter postconditions) verify. Only the router package
remains to be checked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
The M5 run surfaced two follow-up errors from the component-wise
wildcard preconditions of packAddr/SetSrcAddr:

- SetDstAddr still held dst.Mem() folded when forwarding to packAddr in
  wildcard mode; unfold it (at wildcard amount) for IP addresses first.
- testSrcSetterWildcard called SetSrcAddr(src, true) with the folded
  predicate; unfold it in the test body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
The first full run of the prepareSCMP body proof came back with only
four errors:

- the read of p.scionLayer.SrcIA needs the HeaderMem sub-predicate
  unfolded (unlike FlowID/TrafficClass, whose permissions sit at the
  top level of SCION.Mem);
- validateSrcDstIA, handleIngressRouterAlert, and
  handleEgressRouterAlert are intermediate callers on the way to
  invalidSrcIA/handleSCMPTraceRouteRequest and were missed in the
  rawPkt threading.

Everything else in the proof - the ExtractAccSrc/ExtractIPBytes wand
chains, the MemSerialize/ChecksumMem folds, the SerializeLayers call
against its trusted contract, and the !IsSupportedPkt derivation -
verified on the first attempt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
The last remaining router error was the quote-length bound assert in
prepareSCMP: revPath.LenSpec(nil) <= 796 is not derivable at the call
site because the NumINF/NumHops bounds are buried two predicate
unfoldings deep (Decoded.Mem -> Base.Mem). Add (*Decoded).LenSpecBound,
which surfaces the bound with explicit unfolds, and call it before the
assert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
The verify-deps job on 85ec14e died before starting ('Failed to
resolve action download info. Error: Service Unavailable') and the
GitHub App integration is not permitted to re-run failed jobs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
The lemma-head run is down to a single error: 0 <= maxQuoteLen. The
LenSpecBound lemma discharges the concrete bound
revPath.LenSpec(nil) <= 796, but hdrLen's path summand comes from the
interface-dispatched scionL.Path.Len(nil), whose postcondition speaks
about the interface-level LenSpec. Assert the identity of the receiver
and the equality of the two LenSpec applications so the bound
transfers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
The quote-bound assert still failed with the lemma in place: the
lemma consumed the full Decoded.Mem, so its internal unfold/refold
produced a fresh predicate snapshot, severing the link between
LenSpec's value at the earlier Path.Len(nil) call and at the asserts.
Taking (and giving back) only an R50 fraction leaves the caller with an
anchoring fraction, so the heap-dependent LenSpec provably keeps its
value across the lemma - the same mechanism by which Decoded.Len's own
'l == LenSpec(ubuf)' postcondition composes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
The scion path package hit its 30-minute wall-clock timeout: unrelated
members (Widen*/Xover* IO lemmas, DecodeFromBytes) were reported as
'did not terminate' while LenSpecBound itself finished, i.e. the new
member consumed budget the rest of the package needed. Strip the lemma
to a bare unfold/refold of the two predicates: InfoLen and HopLen are
compile-time constants, so the bounds already stored in Base.Mem
(NumINF <= MaxINFs, NumHops <= MaxHops) discharge the postcondition
without ghost variables, extra asserts, or nonlinear arithmetic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
The last router error was the append of the quote payload onto
scmpLayers: the ghost permission was noPerm (perm 0), copied verbatim
from upstream where it had never been verified. scmpLayers is a
length-3 composite literal (len == cap), so this append always
reallocates and Gobra must copy the three existing interface elements
into the new backing array - which requires positive read permission.
Full access is held from the literal, so pass writePerm. (The working
zero-permission appends elsewhere all append within spare capacity and
never reallocate.)

With this, the maxQuoteLen bound now discharges (the minimized
LenSpecBound worked) and this was the sole remaining router error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
verify-deps, verify-third-party-libs, verify and build are now green;
the append fix worked. The sole remaining router error was the
checksum-fold precondition len(scionL.RawSrcAddr) % 2 == 0.

It holds but needed the case split made explicit. The source address
comes from SetSrcAddr(&net.IPAddr{IP: p.d.internalIP}, wildcard) and
getInternalIPMem gives len(internalIP) in {4,16}. SetSrcAddr yields
either T4Ip (len 4) or T16Ip (len == the IP's length). To let Gobra
conclude:
- packAddr/SetSrcAddr gain a postcondition that an IP address maps to
  T4Ip or T16Ip (so the case split is exhaustive);
- prepareSCMP records len(internalIP) in {4,16} before the call (so the
  T16Ip postcondition's old(len(IP)) carries the even length through)
  and asserts the addr-type membership after.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
The previous evenness fix asserted len(internalIP) in {4,16}, but the
type invariant (internalIPInv) permits internalIP == nil (length 0), so
that assert was too strong and became the sole router error. Evenness
still holds - the internal IP length is 0, 4, or 16, all even.

- SetSrcAddr gains a postcondition: for a wildcard IP whose length is
  even, len(RawSrcAddr) is even (T4Ip gives 4; T16Ip preserves the
  input length). This localizes the old(len(IP)) reasoning to where src
  is in scope.
- prepareSCMP now asserts len(internalIP) % 2 == 0 with a nil-aware
  case split (nil -> length 0; non-nil -> 4 or 16 from getInternalIPMem)
  and drops the addr-type membership assert, relying on the new
  postcondition to carry evenness to the checksum fold.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
The next router error was !scionL.IsSupportedSerialization() at the
SerializeLayers call. The hook reads NextHdr/PathType by unfolding
MemSerialize, but FoldFreshMemSerializeWithChecksumMem folds that
predicate and its postcondition said nothing about the field values, so
across the lemma call Gobra lost NextHdr == L4SCMP.

Add a postcondition relating the hook to the pre-fold field values
(IsSupportedSerialization() == (old(PathType) == scion.PathType &&
old(NextHdr) != L4SCMP)), proved in the body by a fold-transparent
assert. prepareSCMP sets NextHdr = L4SCMP before the lemma, so the
caller now derives !IsSupportedSerialization() and, through the trusted
SerializeLayers IO bridge, !IsSupportedPkt(result).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
Past the IsSupportedSerialization assert, the next router error is the
SerializeLayers precondition forall i :: acc(&layers[i], R50) &&
layers[i] != nil. The acc follows from the full access held on the
scmpLayers literal/append; the gap is the non-nil quantifier, which
Gobra does not lift automatically from the individual element values.

Assert it explicitly: element identities and non-nilness for the base
three layers right after the literal, then re-establish the quantifier
after the quote append (element 3 is a non-nil Payload interface
value), and once more before the call so it survives the cause-branch
merge (len 3 or 4).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
The SerializeLayers call precondition could not be satisfied: the
router spreads a pre-built slice (SerializeLayers(..., scmpLayers...)),
so the quantified predicate preconditions (forall i :: layers[i].Mem)
had to hold over that slice, but Gobra cannot assemble a quantified
permission from individually folded layer predicates. (decodeLayers
avoids this by passing its layers as separate arguments, which Gobra
packs into a fresh slice.)

Rewrite the trusted SerializeLayers spec with per-index clauses
specialized to the router's three-or-four-layer call shape. Per-index
Mem/MemSerialize clauses are dischargeable directly from the individual
folds (linked to the slice cells by identity asserts at the call site),
and they make the layers implicitly distinct, so the hard-to-prove
injectivity clause is dropped. The postcondition returns the same
per-index resources; the existing post-call unfolds are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
…tity

The per-index SerializeLayers spec exposed the next error: the assert
that the appended element equals gopacket.Payload(quote) failed. The
working append pattern (extn.go) captures the element in a ghost var
before the append and asserts identity against it; those elements are
pointers, whereas here a Payload value is boxed into the interface
slice, and a direct expression comparison does not match.

Follow the precedent with an interface-typed ghost var pldElem =
gopacket.Payload(quote): fold its (resource-free) Payload.Mem(quote)
before the append and assert scmpLayers[3] === pldElem after, so the
payload's Mem is available to the per-index SerializeLayers
precondition. The executable append argument is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
Gobra preserves slice-element identity for composite literals (layers
0-2 verify) but not through an append that boxes a value into an
interface slice: scmpLayers[3] === <appended Payload> is unprovable in
any formulation, so the caller cannot supply layers[3].Mem for the
quote payload.

Since Payload.Mem is resource-free (just the slice identity), the
trusted SerializeLayers spec no longer requires layers[3].Mem or
non-nilness for the optional fourth layer - only its R55 byte access,
which the caller holds from splitting the quote out of ub. The
resource-free Mem and the non-nil check are reconstructed by the
(trusted, unverified) implementation. The pldElem ghost, its fold, and
the failing identity assert are removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
The verify-router job failed after only ~25 minutes (not the 6h
timeout) with a Silicon StackOverflowError that cascaded into
RejectedExecutionException as the thread pool shut down; the affected
members (process, processOHP, doXover, closureCall) were in-flight when
the engine crashed. prepareSCMP is not among them, and verify-deps,
verify-third-party-libs, verify and build all passed. As CI pins the
Gobra image to 'latest', this run (six days after the prior ones) may
have used a regressed image. Re-trigger to check whether the crash is
deterministic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
The verify-router job crashed with a Silicon StackOverflowError (deeply
nested evaluator recursion) that shut down the verification thread pool;
the members reported as not terminating were collateral, and the thread
that threw first may well have been in the SCMP path. As a small
perturbation to shift the evaluator off the pathological term, build the
four-element layerBufs sequence as a direct literal instead of
concatenating a single-element sequence. Semantically identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
With the SerializeLayers call now verifying, the remaining router error
was the post-call unfold of scionL.ChecksumMem: it is nested inside
scmpH.Mem (via SetNetworkLayerForChecksum), and after the trusted call
returns scmpH.Mem(nil) Gobra no longer knows scmpH.scn === &scionL, so
the nested ChecksumMem cannot be attributed to scionL.

The recovery does not need ChecksumMem: it only applies the
destination-address byte wand (R20), and scionL.MemSerialize alone
holds an R25 fraction of those bytes. scmpH.Mem and the ChecksumMem it
contains are local resources not mentioned in prepareSCMP's
postcondition, so they are dropped. Remove the two unfolds; keep the
MemSerialize unfold that supplies the wand.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
The verify-router run on 5bee81a crashed with a StackOverflowError
cascading into RejectedExecutionException; the member reported as not
terminating this time was dictMaxAssoc (an unrelated dictionary
helper), confirming nondeterministic engine instability rather than a
proof failure. Other runs of nearby heads (827a059 re-run, 99d6c34)
completed normally with a single real error, so re-trigger to obtain a
clean verdict on the ChecksumMem-leak fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
claude added 13 commits July 22, 2026 08:50
With ChecksumMem no longer unfolded, the post-call recovery reached the
next obligation: applying the SrcAddr magic wand failed with 'wand
instance not found'. SrcAddr's wand is stored with its right-hand side
over p.scionLayer.RawSrcAddr, but the apply used the equal-but-
structurally-distinct ub[startSrc:endSrc], which Silicon's wand matching
does not unify (resolveLocalDst gets away with the ub[...] shape only
because it applies immediately, before intervening operations). Apply
with the RawSrcAddr shape, then recombine into ub via the ExtractAccSrc
identity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
Applying the SrcAddr wand still failed as 'instance not found' even with
the RawSrcAddr shape, because a heap-read expression in the apply does
not match the wand chunk, whose right-hand side is let-bound to the
source-address slice value at SrcAddr's return. (The codebase never
applies these address wands - the analogous ones in resolveLocalDst are
commented out and the address is instead leaked to a caller; but here
srcA is local, so the bytes must be reclaimed.)

Capture p.scionLayer.RawSrcAddr into a ghost var immediately after
SrcAddr and apply the wand against that stable term; then bridge to
ub[startSrc:endSrc] via the identity for the recombine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
The ExtractIPBytes magic wand's LHS footprint is let-bound to the raw
argument passed at the call; applying it against a heap-read
scionL.RawDstAddr after SerializeLayers fails to match the wand chunk.
Capture the slice as a stable ghost term (mirroring the rawSrc fix for
the SrcAddr wand) and use it at both the call and the apply.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
Per the design, the destination-address bytes are split R25/R25 between
MemSerialize and the ChecksumMem nested in scmpH (via scmpH.scn), with the
remaining R20-2*R25 kept back at the fold. Recover both predicates after
SerializeLayers to reconstitute the full R20 the ExtractIPBytes wand needs,
rather than dropping ChecksumMem (which left the wand R25 short).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
…trapped ChecksumMem

The trusted SerializeLayers havocs predicate contents, so neither scmpH.scn
(needed to recover the SCION ChecksumMem) nor the SCION RawDstAddr field
survives the call. Both are needed to give the loaned destination-address
bytes back to the SrcAddr/ExtractIPBytes wand.

Rework the fresh-serialization resource layout so nothing reclaim-critical is
trapped:
- SCION.MemSerialize now holds the full R20 of the destination-address bytes
  (was R25) and, together with the wildcard source bytes, serves both header
  serialization and the upper-layer checksum. No ChecksumMem is folded for the
  fresh flow; FoldFreshMemSerialize replaces FoldFreshMemSerializeWithChecksumMem.
- The SCMP header is passed to SerializeLayers through its MemSerialize (its
  4-byte header fields only), not its Mem, so the network header's checksum
  bytes are not duplicated into a predicate the caller cannot reclaim. The
  checksum is computed from the first layer, whose MemSerialize holds the
  addresses.
- Add a SerializeAddrView accessor to the SerializableLayer interface (nil for
  all layers but SCION) and have the trusted SerializeLayers preserve it, so
  prepareSCMP can re-associate the recovered bytes with the slice its wand
  expects. Mirrors the SerializeBuffer.UBuf identity idiom.

prepareSCMP now reclaims the full R20 with a single unfold of the preserved
MemSerialize.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
…zeAddrView

The postcondition calls layers[0].SerializeAddrView() (a pure interface
method) in the post-state, which requires a non-nil receiver; the ensures
restated MemSerialize() but not non-nilness. Add layers[0] != nil.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
…ching

Reducing the two fold sl.Bytes(nil,0,0) to one (after the SCMP header layer
stopped requiring byte access) left Gobra's chunk matcher without slack: the
write buffer's cleared UBuf() may alias nil, so sl.Bytes(w.UBuf()) and the
nil-buffer layer's sl.Bytes(nil) compete for the same empty-bytes chunks.
Restore the second (leaked) empty-bytes fold.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
Add an explicit assert of the full sl.Bytes(p.buffer.UBuf()) permission right
before the call. This guides Gobra's chunk matcher to the buffer's own byte
chunk (rather than the just-folded empty-byte predicates) for the
SerializeLayers precondition, and pinpoints a framing loss if the bytes are
not actually held there.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
The diagnostic assert of writePerm sl.Bytes(p.buffer.UBuf()) was stricter than
the SerializeLayers call's own precondition matcher (which flexibly draws the
cleared buffer's empty bytes from the folded empty-byte predicates), and was
itself the blocker. Remove it; the two empty-byte folds restore the slack that
made the call's buffer precondition verify at 0795633.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
…yers buffer bytes

a5551b3 (two folds, no assert) still failed the SerializeLayers buffer-byte
precondition, so the fold count was never the cause: the len(UBuf())==0 and
UBuf() value facts from Clear do not survive the intervening joins/folds
(nothing consumes p.buffer between Clear and the call). Capture the cleared
buffer slice as an immutable length-0 ghost right after Clear, then re-fold its
empty byte predicate and assert p.buffer.UBuf() === that ghost before the call,
so the chunk matcher can discharge sl.Bytes(w.UBuf()).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
…f time

The router job's prepareSCMP task ran 2x+ longer than normal after the proof
started reaching the (previously error-blocked) post-call reclaim. The likely
cost is unfolding scionL.MemSerialize() inline: it materializes the whole bulky
header predicate (path memory, path pool, all fields) into prepareSCMP's already
fold-heavy SMT context, even though only the R20 destination-address bytes are
used and the rest is leaked.

Add slayers.(*SCION).ReleaseSerializeDstBytes, which performs that unfold-and-leak
in an isolated context and returns just the R20 bytes (tied via
old(SerializeAddrView()) to the slice the address wand expects). prepareSCMP now
calls it instead of unfolding inline, keeping the path memory out of its proof
context. The lemma is checked in verify-deps, not the slow router job.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
…lemma

The router task began timing out (6h) exactly when the post-call region first
became error-free: previous ~1h runs all errored in the reclaim, pruning the
tail, so the two reveal-asserts bridging the trusted call's gopacket-level
unsupportedness to !IsSupportedPkt(UBuf()) had never actually been explored.
Those reveals expand opaque, quantified definitions (IsSupportedPkt unfolds the
full byte predicate) inside a context holding ~10 byte chunks under mceMode=on,
a plausible blowup.

Add slayers.UnsupportedRawPktImpliesUnsupportedPkt, which performs the
IsSupportedRawPktEqGopacket bridge and both reveals in an isolated context
(checked cheaply in verify-deps), and call it from prepareSCMP instead of
revealing inline.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
The router job passes but takes ~4.4h of the 6h budget, dominated by
prepareSCMP's proof context. Shrink it by strategically hiding information:

- Trim the trusted SerializeLayers postcondition: stop returning the inner
  layers' predicates and slice-cell fractions (the router drops those local
  resources anyway); only the quote payload's byte fraction, the network
  header's MemSerialize, and the SerializeAddrView/IsSupportedRawPkt facts
  survive the call, so far fewer chunks are inhaled into the caller.
- Hide the bodies of the IsSupportedSerialization and SerializeAddrView
  hooks behind opaque helper functions (wrapper pattern). Their
  predicate-unfolding definitions no longer expand at every mention in the
  router; callers get their values equationally from FoldFreshMemSerialize's
  and ReleaseSerializeDstBytes' postconditions, whose bodies reveal the
  definitions once, in an isolated context.
- Add an extrinsic reclaim lemma (reclaimScionMemAfterSerialize) composing
  the three wand applications and the range recombination that return the
  loaned source-address resources; the wand chunk matching and the
  address-type ghost branch now happen in the lemma's small context instead
  of prepareSCMP's.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTJK8Rjoq7J76yn2GJ2y48
Comment thread pkg/slayers/scion.go
// @ ensures acc(sl.Bytes(s.RawDstAddr, 0, len(s.RawDstAddr)), R20)
// @ ensures acc(&s.RawSrcAddr, R30) && acc(&s.RawDstAddr, R30)
// @ ensures acc(&s.SrcIA, R30) && acc(&s.DstIA, R30)
// @ ensures acc(sl.Bytes(s.RawSrcAddr, 0, len(s.RawSrcAddr)), _)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how can this verift later on if we pass _ as the permission amount here? Don't we have to recover that permission in order to reestablish the onwership of the entire raw pkt later on?

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.

2 participants