- All proofs are Iris WP over the REAL Sail model.
runis the relational interpreter,execthe functional partial mirror;exec_run_detbridges them.Loopisto_val-None and steps toLoop(never a value), so it is NOTAtomic—wp_atomic/iInv-on-WP do not apply; the fupd-flavoured step rules are the only route to open an invariant across a step. - WP layering (each supplies more):
wp_exec_step(caller givesexec riscv_step σ = Some(tt,σ')) →wp_exec_step_hart_active(_inv)(owns only the wrapper regs hart_state/minstret/minstret_increment/PC, does the try_step bookkeeping) →wp_exec_step_decode_execute_inv(caller gives fetch/decode/execute exec-facts, generalized over anyFetchResultviadecode_fetch) →wp_instr(InstrBytes.v). Invariant-friendly variants:wp_exec_step_fupd E Ei(caller picks inner maskEi) andwp_exec_step_minstret E Ei(opens the minstret invariant on top);wp_exec_stepis theEi:=∅case. - Axiom budget: every finished proof should reduce to exactly the 5 model platform axioms
valid_reservation,plat_term_write,match_reservation,load_reservation,cancel_reservation, plusfunctional_extensionality_dep. A U-mode proof adds the two reservation platform-EFFECT axiomsexec_load_reservation/exec_cancel_reservation(UserMemAccess.v §0), which say those opaque monadic effects leave the modelled architectural state alone. Anything else is a kernel-level assumption, and each one is an assumed callee in its ownLink*.v— grepLink*.v/Spec*.vfor theAxiomkeyword rather than trusting a count written here, since the list shrinks as functions are proved.kerneltrap_returnsIS GONE, and so is the whole handler-contract debt it stood for: kerneltrap is proven,intr_handler_specis a Banach fixpoint over the folded bundle,ProofKernelvec.vis a functor over the REALKERNELTRAP, and the boot credentials that axiom used to hide are minted by the boot chain (SpecBootDevCaps.v/LinkBootDevCaps.vno longer exist). Seecompleted/kerneltrap.md.get_config_rvfiis a Definition, not an axiom. RunPrint Assumptionsin a file ALONE (never beside About/Check) and read only the Axioms block;tools/proof_coverage.pyreports the same per function. - Multi-byte memory:
write_bytes/read_bytes/nth_byte/pa_add(RiscvModelBytes.v); the MemWrite case must be changed IDENTICALLY in all four interpreters (exec/run/runR/execR) orrunR_liftRbreaks.bv_eq_of_bytes: a word is determined by its bytes. MachineWord reduces to stdpp bv:mword n = bv (Z_idx n),to_word/get_word= identity.
riscv_steptakes a tick flag (bind (try_step 0 false) (fun _ => if tick then tick_clock tt else ret));prim_stepchooses it NONDETERMINISTICALLY — the sound weakening of the modelloop's deterministic every-plat_insns_per_ticktick (the language has no instruction counter, andexectreatsChooseas stuck so the choice can't live inside the monad).wp_exec_step(RiscvExec.v) therefore takes exec witnesses for BOTHriscv_step falseandriscv_step true(continuation ∀-quantified over the tick);exec_riscv_step_tickcomposes the tick witness from the no-tick one + atick_clockexec fact.tick_clockis register-only and TOTAL:exec_tick_clock(MinstretInv.v) gives, at ANY state, a successor that is exactly the towerset_reg (set_reg (set_reg s mcycle c) mtime t) mip p(mtime += 1 always; mip.MTIP := mtimecmp ≤u mtime and — STCE is menvcfg bit 63, which MENVCFG_S pins to 1, so the Sstc branch is LIVE — mip.STIP := stimecmp ≤u mtime; mcycle conditional on mcountinhibit.CY/mcyclecfg). Proof machinery there:register_set_bv64_id/_overwrite(funext),exec_clint_dispatch_false, Sstc-gate clones (primed — unprimed originals live downstream in WpGprCsrwCommon.v).clock_inv(clockN = nroot.@"clock") owns value-agnostic cells for the three written registers {mcycle, mtime, mip}; it is APPENDED tominstret_inv(minstret_inv = inv minstretN minstret_inv_body ∗ clock_inv), so callers thread ONE persistent proposition.wp_exec_step_clock(layered BELOW the minstret rule) absorbs the tick entirely: the caller supplies only the no-tick witnessexec (riscv_step false) σ = Some (tt, σ')and reasons at the FULL mask — the clock invariant is opened only inside the tick branch, strictly AFTER the caller's continuation, so an instruction whose own execution touches mtime/mip mayiInv clock_invitself. Unlike the minstret cells, the clock cells are NEVER loaned to the caller.- Consequence: no frame may hold ANY fraction of mcycle/mtime/mip — not merely "may not pin the value".
clock_invowns the three atDfracOwn 1, which isExclusive, so a second fraction (owned or discarded) is invalid, and a bundle carrying one is UNSATISFIABLE besideminstret_inv— vacuous, not over-specified. UNDER PER-NODE STEPPING THE U-MODE TIERS DO NOT BORROW mip AT ALL — the hart OWNS mcycle/mtime/mip throughMinstretInv.clock_res, which rides insideuser_regs, so there is noclock_invleft to borrow from andUserExec.clock_mip_accis DELETED along with everyiInv "Hwinv"in the U-mode step engines. The borrow survives only where a step still opensclock_inv(the M/S-mode side). What made the borrow necessary was the whole-cycle σ-callback; per-node stepping answers a wire or mip read from the hart's own read-only frame instead (HartRunFull.swp_run_hart_active_Utakes the wire VALUES plus agoodbcertificate), so nothing is opened across the step. Safe besidewp_exec_step_clock's ownclockNopening, which is in the tick branch only, strictly after the caller's continuation closes its invariants. A leaf READING one (rdtime =wp_csrr_time_gpr, WpGprCsrrB.v) takes NO cell and ∀-quantifies its continuation over the read valuetv(the exec witness reads σ directly — no invariant opening needed just to read).wp_timerinit/wp_start/wp_entry_bootthread thattvoutward: their continuations are∀ tv, … stimecmp ↦ᵣ stimecmp_legalized stimecmp0 (ti_deadline tv) ….stimecmp/mtimecmpare only READ by the tick, so they stay client-owned. - The interrupt-off keystones survive ticks (MIE=0/SIE=0 arguments never depend on mip's value).
- The Sstc timer capability (
TimerCap.v+ the S-mode leaves inWpSconfTimer.v). S-mode use of the timer (rdtime,csrw stimecmp) is gated, at Supervisor, on mcounteren.TM=1 for BOTH instructions plus menvcfg.STCE=1 for the write (whichsconfalready gives, pinning menvcfg =MENVCFG_S, bit 63 set). So the only configuration fact a timer leaf needs is the TM bit, and both it and the deadline cell are packaged into ONE persistenttimer_cap=sstc_enabled(∃ mcen, mcounteren ↦ᵣ□ mcen ∗ ⌜TM=1⌝— mcounteren is written once, in timerinit, and registers are per-hart, so it can be frozen) ∗stimecmp_inv(inv timerN stimecmp_free,stimecmp_free := ∃ d, stimecmp ↦ᵣ d).timer_cap_introis the ghost step from timerinit's postcondition. The write leaf openstimerNacross its own single step (disjoint fromminstretN,solve_ndisj) and reseals at the new deadline, so NOTHING timer-shaped is threaded by callers and no postcondition mentions the timer: the deadline is unnameable anyway (it ismtime + 1000000and mtime lives inclock_inv), and the alternative — a linearstimecmpcell — would have to be carried by every frame from the trap handler down. Do NOT go back to threading the cell unless a client genuinely needs to relate the deadline to something (then holdstimecmp_freedirectly; the invariant is one ghost step away). - Acknowledging a timer interrupt is invisible here.
w_stimecmp(...)is what clears the pending timer interrupt (the next tick recomputes mip.STIP := stimecmp ≤u mtime), but mip is value-agnostic inclock_invand the dispatch reads it straight off σ, so "the interrupt is acknowledged" is not a statement this logic can make — nor does it need to be: the interrupt machinery is correct for ANY mip.
minstretis not threaded through pre/postconditions — every WP takes the duplicable persistentminstret_inv(MinstretInv.v), and a leaf obtains the two cells transiently by opening the invariant across its step. Top boot theorems takeminstret_inv; a future cells-in wrapper canminstret_inv_alloc.- All WP-Loop masks are pinned to
⊤(mWP Loopis the top level): there are NO↑minstretN ⊆ E/↑lockN ⊆ E/↑clockN ⊆ Eside premises anywhere —iInvside conditions discharge automatically at ⊤. Leaf σ-callbacks run at⊤ ∖ ↑minstretN. - Namespaces are deliberately disjoint (
minstretNvslockN = nroot.@"xv6spinlock"vsclockN), because a leaf's σ-callback runs at mask⊤ ∖ ↑minstretNand must still be free to open the lock and clock invariants.
- RISC-V x0 is virtual:
rX (Regno 0) = returnM zero_reg,wX 0a no-op. The register file is a total functionregfile := regidx → mword 64(RegFile.v), withInsert/LookupTotalinstances so<[k:=v]> f/f !!! kis the usual map syntax;gpr_file f := ⌜∀ r:regidx, r ∈ dom (rf_to_gmap f)⌝ ∗ [∗map] r↦v ∈ rf_to_gmap f, gpr_pt r vfolds over therf_to_gmapbridge (sie_capandcallee_savedlikewise). Indexed byregidx(Regidx (mword 5); index 0 = x0 IS a key but owns nothing / assertsv=zero_reg). Discharge lookups withreg_lookup, NOTreflexivity/repeat split— see completed/regfile-migration.md for that rule (async-Qedhazard) and thegpr_file_lookup_acc/_insert_accaccessors.gpr_fileis a LINEAR resource every instruction WP consumes and is NOT reconstructible from returned memory — a callee WP MUST return it. Helpers:gpr_pt_value(pure ⇒iDestruct … as %keeps inputs),gpr_pt_nz; lynchpinsexec_wX_bits_gpr/exec_rX_bits_gpr(symbolic reg index). Define your owngpr_of_Z(reg-number → constructor); the enumregister_bitvector_64has NO x0. - Reg disequalities for an ABSTRACT index (
register_beq (gpr_of_Z (uint r)) {PC,nextPC,minstret} = false) go throughgpr_trans/reg_ne/tmig(unfold gpr_of_Z; repeat case_match) — NEVERvm_compute(GPRs share theR_bitvector_64constructor with PC/nextPC/minstret).regval_into_regis the identity onmword 64(reflexivityclosesregval_into_reg v = v; unification passes through it).
mem_pointsto a dq v := ∃ ppn, kmap_at (svpn_of a) ppn KP_rw ∗ ⌜uint a < 2^38⌝ ∗ ⌜addr_is_ram (pa_of ppn a)⌝ ∗ pointsto (pa_of ppn a) dq v(addr_is_ram = ram_base <= uint a < ram_base + ram_size); notations↦ₘ{dq},↦ₘ□(DfracDiscarded = persistent/duplicable),↦ₘ(= DfracOwn 1). Because RAM ([0x80000000, 0x88000000)) is disjoint from CLINT and SIG, owning amem_pointstobyte automatically proveswithin_clint = falseandwithin_sig = false(ruling out CLINT/SIG MMIO accesses).mem_ram : a ↦ₘ{dq} v -∗ ∃ ppn, kmap_at (svpn_of a) ppn KP_rw ∗ ⌜addr_is_ram (pa_of ppn a)⌝.mem_pointstois SEALED (Typeclasses Opaque) — don't destruct the rawpointsto ∗ ⌜⌝conjunction directly (locallyrewrite /mem_pointstoif you must).- Kernel CODE bytes are
↦ₓ□(text_pointsto a dq v, carryingKP_rx+addr_is_text) sokernel_textis Persistent/duplicable (never borrowed/returned); DATA (load/store) bytes stay↦ₘ(DfracOwn 1). Physical memory accessed untranslated (walker/M-mode) uses physical points-to↦ₚ(phys_pointsto pa dq b := pointsto pa dq b ∗ ⌜addr_is_ram pa⌝). Discriminator in fetch-vs-data lists: fetch uses(fetch_pa pc) j, data uses(pa_add pa j). Reg points-to are Fractional (reg_pointsto_fractional,reg_pointsto_agree).mem_valid/mem_ram/reg_valid_dqare dfrac-generic. - Multi-byte cells sit on top of the byte layer:
↦₈/↦₄(RiscvPtsto.v) bundle the 8/4 byte points-to WITH the alignment fact, anda ↦ₛ{dq} s(string_pointsto) is the NUL-terminated C string ata—cstring_bytes s = string_bytes s ++ [0], bytejatpa_add a j, no alignment side condition. Each has a□(DfracDiscarded) form:↦₈□/↦₄□/↦ₛ□are Persistent, hence duplicable, and{word,word4,string}_pointsto_persist : … ==∗ …□discards the fraction of a cell that is written once and then immutable (a lock's name field, a string). Read-only kernel STRING literals come out of the data image withkernel_data_string A s a(KernelDataInv.v), thekernel_data_windowanalogue for a C string constant. - Declare a config dfrac as an explicit
(dqc : dfrac)forall arg, never implicit{dqc}— implicit defaultsiApplytoDfracOwn 1instead of inferringDfracDiscarded.
hw_config mc mcfg(2 args, fix the observable minstret-increment bit) andti_ctx …/instr_ctx …bundle the ~11 ambient config registers to shorten opcode WP statements; misa0/mseccfg0/pmar0 etc. are validation-only∃witnesses folded inside. Six frozen cells now (misa/mseccfg/pma_regions/htif_tohost_base/elp/senvcfg, all board-pinned inreset_regsand never written again). Bringhw_configin persistently at intro with a leading#;iDestruct "Hhw" as "#(…)"extracts per-reg points-to but clears the bundle name (iPoseProof "Hhw" as "#Hc"first if still needed). Do NOT use theiAssert (hw_config …) as "#H"copy idiom.hw_config's last conjunct is⌜Misa_A misa0 = 1⌝, so consumers get misa.A for free. ~120 leaf files destructurehw_configBY POSITION instead of going throughhw_config_kmap_claims/sconf_kmap_claims— adding or reordering a conjunct means sweeping all of them by hand.mmode_config (dq)(InstrBytes.v) is FRACTION-PARAMETRIC: a client owningDfracOwn 1splits off a fraction to handwp_instrwhile retaining the complement toreg_valid_dqthe config DURING the execute fupd (LOAD reads cur_priv/MPRV/pmpcfg at the execute state; the fupd hands onlystate_interp). This is why load/store leaves have a raw-cellwp_instr_s_configvariant — thesmode_configbundle hidesmstatus0/menvcfg0existentially and a bundle→cells round-trip loses the MXR/PMM facts.
fetchbranches onis_aligned_vaddr PC 4 && Ziccif: 4-aligned PC → ONE 4-byte read (a 4-aligned RVC grabs the next instr's 2 bytes); 2-aligned PC → 2-byte read,isRVC→F_RVC, else read 2 MORE at PC+2 →F_Base(so a 32-bit instr at a 2-aligned PC needs a 2+2 split fetch). Page crossings are the paging layer's: every single read is naturally aligned, so none leaves its page; a crossing falls only BETWEEN the split fetch's two reads, each translated on its own. The user tier'sUmodeMem.uinstrtherefore names each READ's translation (the pc's, andui_hiat pc+2 for the split fetch) and has NO in-page clause; composers get "this read stays on its page" fromualign4_nc/ualign2_nc.is_rvcininstr pc is_rvc iis the visible discriminant ruling out error FetchResults. Helpersfetch_from_pts_minstret{,_2,_RVC2,_RVC4}.- Kernel bytes are a per-BYTE
kernel_bytes : gmap Z (bv 8)(list_to_mapoverList.concatof 1000-entry chunk definitions — a flat literal that long overflows the stack, seedurable-notes.md;Typeclasses Opaque; alsokernel_data,kernel_symbols); 2-vs-4-byte fetch is UNIFORM (a 4-byte fetch at an RVC just reads 4 consecutive bytes — no regroup/split/join machinery).kernel_text := [∗map] a↦b ∈ kernel_bytes, (mword_of_int a) ↦ₓ□ b. Extract a window withkernel_window A w W Hbytes; dischargeHbytesinline:intros j Hj; do W (destruct j; [vm_compute; f_equal; apply bv_eq; reflexivity|]); lia(vm_compute ALONE can't closeSome bv = Some bv— bv well-formedness proofs differ; needf_equal; apply bv_eq). Regen the dump:make dump-force(never casually — see kernel-proofs.md), which runspython3 tools/dump_elf.py --format rocq …. - The same dumper produces the USER-space program images in
user-rocq/(logical nameUser) fromxv6-riscv/user/_<prog>:--prefix syncgivessync_bytes/sync_data/sync_instrs/syncMemBase/syncMemEnd/syncEntry/sync_segments+SyncSyms.<sym>, same shapes as the kernel's, so a proof canRequirethe kernel image and the program it runs without a name clash. A user image is linked at vaddr 0 (user/user.ld), its entry pc isstart(NOT the lowest text address, and NOTmain), and it has TWO PT_LOAD segments — the second is afilesz=0.bss that exists only in<prefix>_segments. Add a program viaUSER_DUMPSin the top-level Makefile +user-rocq/_CoqProject; seeuser-rocq/README.md.
- For 32-bit decode use the concrete-state bridge
decode_bridge_ms/decode_bridge_{m,s}(WpDecodeBridge.v), ~0.02 s/word vs symbolicdecode_any~3.4 s/word:vm_computethe decoder once on a fully-concrete reference state, then transport to the abstract state viaexec_goodb_congr(read-frame congruence, axiom-free). Requires whole-value agreementagree_on D s dston every register the decoder READS (machine-discovered read-sets: Machine{misa?,cur_privilege,mseccfg}, Supervisor{misa?,cur_privilege,menvcfg}; misa only for Zicsr words; a value-discarded read like mseccfg still must be in D). Established tradeoff (don't re-litigate): there is NO fast + config-don't-care decode witness; the fast bridge'sagree_on Dimpliescur_privilege = Machine/Supervisor.rvc_oneshot/rvc_bridgeare the concrete-word RVC analog (finisher runsbv_eqBEFOREf_equal, else it peels into the bv wf-proof). encdec_backwardscannot bevm_computed whole (Acc well-founded recursion hangs). Reduce clause-by-clause by exec-stepping (skip_pure_clause); extension-gated clauses (currentlyEnabled Ext_*) need hand-writtenexec_currentlyEnabled_X/exec_cE_Xlemmas. Acc-reduction recipe (reusable for pt_walk / currentlyEnabled / untilMT loops):unfold fn, Zwf_guarded; cbn [_rec_fn]steps one level — do NOTdestruct (Zwf_guarded _)(makes acc opaque → stuck).- menvcfg/misa pinning:
MENVCFG_S = 0xA000000000000000(STCE bit 63 ∨ ADUE bit 61),MISA_C,cfg_ok(RiscvFetchExec.v). menvcfg is constant in S-mode; the kernel writes it TWICE at boot —start()sets ADUE (bit 61), thentimerinitsets STCE (bit 63), both legalized (wp_startmodels both,st_menv_adue= the post-ADUE value fed into timerinit). ADUE=1 is Svadu: an access needing an A/D update has the bit written back rather than page-faulting, and that write-back is REACHABLE and modelled — there is NO A/D-preset assumption (xv6'smappageswrites A/D-CLEAR leaf words, and the ptree layer keeps the leaf's A/D existential), soupdate_PTE_Bits = Someis a live arm on both the hit and the miss path. It is absorbed inside the page-table invariant (ptree_translateAddr_casesoutcome O3 /tlb_inv_pt_translateAddr_at, seetlb-translation.md), so clients never see the PTE store, but ADUE's value is load-bearing for the walk, not invisible. The sharedinstrobligation carriesmisa = MISA_Candcfg_ok σ, so every S-modeinstrconsumer suppliescfg_ok(Supervisor ⇒menvcfg0 = MENVCFG_S). Thesmode_configbundle carries⌜menvcfg0 = MENVCFG_S⌝; only raw-cell lemmas threadHmenvval(insert right afterHPBMTE). - Odd-halfword layout shifts flip jump/branch/return-target PARITY (2-vs-4-aligned), NOT just addresses. An upstream insertion of an odd number of halfwords (e.g. start's 14-byte ADUE block) shifts every later address by an amount ≢ 0 (mod 4), so a jal/branch/cret target that was 4-aligned becomes 2-aligned. The 4-aligned jump leaves (
wp_cret_gpr,wp_jal_gpr_s,wp_cbeqz/cbnez_taken_s) then can't discharge theirbit1 = 0(4-alignment) premise. Fix: switch to the C-extension 2-aligned (_zca) variants, which need onlybit0 = 0(misa.C ⇒ IALIGN = 2, viaexec_jump_to_zca). Most already exist (wp_jal_gpr_s_zca,wp_cbeqz/cbnez_taken_s_zca{,_scfg}, WpSmodeJal/WpSmodeBtype); the M-mode cret (wp_cret_gpr_zca, WpMmodeJalr) and a ▷-carrying loop-back branch (wp_cbnez_taken_s_zca_later, WpSmodeBtype — like the plain_zcabut exposes the step's LATER for a spin loop's Löb IH). Base plumbing:aligned2_jump_bit(RiscvExtras),exec_jump_to_zca(ExecCommon),exec_execute_JALR_ret_zca(WpMmodeLeafBase). Swapping a taken/jal leaf to its_zcaform is always safe (2-alignment is weaker) and drops the trailing bit1ltac:(vm_compute; reflexivity)arg. The same parity flip also changes FETCH WIDTH (see "Fetch geometry" above: the 4-aligned arm is one 4-byte read, the 2-aligned arm a 2-byte read or a 2+2 split), which is invisible to every whole-function proof — those spellKernelSyms.f + offand take alignment as a premise — but reshapes any proof pinned at a CONCRETE pc.HartPilot.vis the one such file; at 06ea57f its pilot's fetch went 4-byte → 2-byte and its wordbv 32→bv 16, andHartMDecode.hp_halfstopped needing to slice the low half out. The symptom ishread_req_at 4returningNone.