Reading a machine theorem
This page reads one HOL Light machine theorem as an engineering contract. You do not need prior HOL Light knowledge.
The shape of the claim
The x86-64 addition theorem has this simplified shape.
!c a0 a1 b0 b1 pc.
valid_offset c ==>
ensures x86
(\s. bytes_loaded s (word pc) code /\
read RIP s = word body_pc /\
read RDI s = a0 /\ read RSI s = a1 /\
read RDX s = b0 /\ read RCX s = b1 /\
read R8 s = word c)
(\s. read RIP s = word end_pc /\
(value a0 a1 < p(c) /\ value b0 b1 < p(c)
==> value (read RAX s) (read RDX s) =
(value a0 a1 + value b0 b1) MOD p(c)))
(MAYCHANGE registers ,, MAYCHANGE flags ,, MAYCHANGE events)
The actual theorem uses library names for each concept. The structure above is the same.
Universal inputs
The first line starts with !.
!c a0 a1 b0 b1 pc.
This means the theorem applies to every choice of these values. c is the
offset in p(c) = 2^128 - c, subject to valid_offset c. a0 and a1
are the low and high 64 bit limbs of the first input. b0 and b1 are the
limbs of the second input. pc is the address where the code is loaded.
The proof does not enumerate test cases. The theorem variables stand for all possible 64 bit words.
Initial machine state
The first function passed to ensures x86 is the precondition.
bytes_loaded s (word pc) code
This fixes the exact instruction bytes in memory at pc.
read RIP s = word body_pc
This says generic execution starts immediately after the fixture's literal
constant load. The precondition also puts symbolic c in r8. The separate
A7F7 corollary starts at the first instruction and proves that literal load.
The register equations place the four input limbs in the physical registers used by the object. The values are variables, but the register names are fixed because register numbers are encoded in x86 instructions.
Final machine state
The second function passed to ensures x86 is the postcondition. It states
where execution stops and what result registers contain.
For addition, the arithmetic part is an implication.
if a < p and b < p, then result = (a + b) mod p
This is the canonical input rule. If either input is not canonical, the theorem still proves that execution reaches the end while staying inside the frame condition. It makes no arithmetic claim about that result.
The field type must therefore preserve canonical values. Checked decoding and safe constructors enforce that rule in Rust. Internal field operations are designed and tested to preserve it. That type invariant is not yet a HOL Light theorem about the whole Rust program.
The right hand side uses MOD p, so it is in the range from zero through
p - 1. Equality to that expression proves both modular correctness and a
canonical result.
The frame condition
The third argument lists all state that may change.
MAYCHANGE [RIP; RAX; ...] ,,
MAYCHANGE SOME_FLAGS ,,
MAYCHANGE [events]
This is the theorem version of a changed register declaration. State not named by the frame must remain unchanged.
The events component records externally visible processor events in the
model. Allowing it to change does not prove a side channel claim. It lets the
functional theorem focus on the arithmetic result and ordinary machine state.
Body theorem and subroutine theorem
Each baseline kernel has three theorem levels.
The generic body theorem starts after the fixture's constant load and stops
before ret. It states the arithmetic result for every valid offset and the
exact state that the body may change. The A7F7 body corollary starts at the
literal load and specializes the generic theorem.
The subroutine theorem adds the procedure call convention. On x86-64 it states
that ret reads the return address from the stack, increases rsp by eight
bytes, and transfers control to that address. It permits only the registers and
flags that the System V convention allows a callee to change.
The BMI2 and ADX multiplication body already forms the result in rax:rdx.
Its subroutine theorem therefore covers the complete object with no result copy
between the proved arithmetic and the procedure return.
How the instruction proof works
The proof first symbolically executes the exact bytes. The x86 model produces equations for register results and flags.
For an addition with an incoming carry, one equation has this form.
2^64 * carry_out + output_word
= left_word + right_word + carry_in
For multiplication, the model gives low and high output words whose combined value equals the product of the input words.
adcx and adox use separate carry flags. adcx reads and writes the carry
flag. adox reads and writes the overflow flag. mulx leaves both flags
unchanged. The optimized multiplication proof follows both chains and proves
that the last carry from each chain is included in the top product limb.
The proof then combines the word equations into integer equations. It proves the field result in stages.
instruction equations
-> exact 256 bit product
-> first Solinas fold
-> bound on the remaining high limb
-> second Solinas fold
-> one final canonical correction
The proof never samples an input. Each algebra step uses the variables from the theorem statement.
Tactics and the HOL Light kernel
A tactic is an OCaml program that helps construct a proof. Tactics can be large. A tactic cannot create an accepted theorem on its own.
HOL Light represents a proved statement as a theorem value. A small logical kernel checks the primitive inference steps that create that value. If a tactic has a bug, it should fail, take too long, or build a theorem different from the one requested. It cannot bypass the abstract theorem type through the normal HOL Light interface.
We still trust the HOL Light kernel, its OCaml runtime, and the definitions of the x86 and AArch64 instructions. We also trust the host hardware and operating system that run the checker. The trust boundary page lists these assumptions in one place.
What to review in a theorem
A reviewer should read the statement before reading tactics.
Check these questions.
- Does the byte array name the intended object?
- Are the input registers and limb order correct?
- Is the canonical input condition explicit?
- Does the result use the intended operation and modulus?
- Is the result in the platform return registers?
- Does the frame list every changed register and flag?
- Does the subroutine theorem cover
retand the right procedure call convention?
Only after the statement is right should a reviewer inspect how the proof derives it.