CliRun.lean
Declarations on this page (17)
@[expose] public sectionset_option autoImplicit falsenamespace CryptoVerification.SignalSessionopen Arm64 Arm64.Process ProofLibPaths
def readPath
The path argument (ptr, len) of an I/O function as an FsState. key: one character per byte (Char.), as the engine reads a path of openat (readCString, ISA/).
def pathComps
The components of path p: its characters split at / (List.). On a PathOk path they are the segments of the engine's canonical form (Arm64.).
def pathComps (p : String) : List (List Char) := p.toList.splitOn '/'def isUnmodelledPathComponent
A component the specifications do not model: empty (a leading, trailing or doubled /, or the empty path), . or ...
def isUnmodelledPathComponent (c : List Char) : Bool := c.isEmpty || c == ['.'] || c == ['.', '.']def PathOk
The paths the I/O specifications model: relative and canonical (every component non-empty and neither . nor ..), no NUL byte (Rust's CString would reject it), and no component that fsView hides (stdioPaths, Run.lean), so every file a hook reads or writes is observable. The standard streams themselves are not in the file system (FsState.): the loader opens file descriptors 0, 1, 2 as .stdio descriptors, and stdin's bytes are FsState.. On such paths the engine's canonical lookup (FsState.) agrees with the way Linux resolves them.
def PathOk (p : String) : Bool :=
(pathComps p).all fun c =>
!isUnmodelledPathComponent c && !c.contains '\x00' && !(stdioPaths.contains (String.ofList c))def ancestors
The proper ancestors of p, shallowest first: "a/ gives ["a", "a/; a single component has none (its parent is the party's directory, which exists).
def ancestors (p : String) : List String :=
let cs := pathComps p
(List.range (cs.length - 1)).map fun i => String.ofList (['/'].intercalate (cs.take (i + 1)))def blockingAncestor?
3 declarations not shown (lines 233–267): theorems, which Lean checks, or definitions that no headline statement uses
CryptoVerification., CryptoVerification., CryptoVerification.
def unitResult
Return Ok(()) (x0 := 0) or Err(IoError) (x0 := 1) of a Result<(), IoError> (measured on the compiled write_file / write_stdout: cset w0, ne).
def unitResult (ok : Bool) : State → State :=
retHook fun s => s.writeReg64 .x0 (if ok then 0#64 else 1#64)def unmodelled
Stop with an .Exception (outcomeOf is then none): the hook's inputs are outside what it models.
def unmodelled (msg : String) (s : State) : State := s.setStatus (.Exception msg)1 declaration not shown (lines 281–308): theorems, which Lean checks, or definitions that no headline statement uses
CryptoVerification.
def writeFileHook
libsignal_cli_io::write_file(path: &str = (x0, x1), bytes: &[u8] = (x2, x3)) -> Result<(), IoError> (x0): 0-allocation C-ABI musl open/write/close/__errno_location loop (O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, mode 0o666, < 4096-byte NUL-free stack path copy, EINTR retry on open and write, n == 0 treated as Err, close on every exit after open), i.e. create or truncate, then write all of bytes.
-
.
Exceptionif the path or the bytes are not readable, or (after the length / interior-NUL check) the path is notPathOk; -
path byte length ≥
cliPathMaxBytes(4096,PATH_MAX) or containing an interior0byte:Err(unitResultfalse, beforeopenatand before emittingTraceEvent.);fileChanged -
a proper ancestor is not a directory (a regular file:
ENOTDIR; missing:ENOENT, the CLI never creates a directory):Err, nothing changes infs.;files -
fs.is a directory:files path Err(EISDIR), nothing changes infs.;files -
otherwise (a regular file, or nothing):
fs.,files path := file bytes Ok. In the threePathOk< 4096-byte cases it first appends the eventTraceEvent.p to the trace (not output; module docstring).fileChanged
def writeFileHook (s : State) : State :=
let ptr := s.readReg64 .x0
let len := s.readReg64 .x1
let buf := s.readReg64 .x2
let n := s.readReg64 .x3
if !(s.mem.canRead ptr len.toNat && s.mem.canRead buf n.toNat) then
unmodelled "write_file: unreadable path or bytes" s
else
let raw := s.mem.readBytes ptr len.toNat
if cliPathMaxBytes ≤ len.toNat || raw.contains 0#8 then
unitResult false s
else
let p := readPath s.mem ptr len
if !PathOk p then unmodelled "write_file: path outside the model" s
else
let s := s.appendTrace [.fileChanged p]
match blockingAncestor? s.fs p, s.fs.files p with
| some _, _ => unitResult false s
| none, some .dir => unitResult false s
| none, _ =>
unitResult true (s.setFs (s.fs.setFile p (some (.file (s.mem.readBytes buf n.toNat)))))1 declaration not shown (lines 347–364): theorems, which Lean checks, or definitions that no headline statement uses
CryptoVerification.
def writeStdoutHook
libsignal_cli_io::write_stdout(bytes: &[u8] = (x0, x1)) -> Result<(), IoError> (x0): 0-allocation C-ABI musl write(1, …) / __errno_location loop (EINTR retry, n == 0 treated as Err, and EBADF on a closed file descriptor 1 treated as Ok(()) matching std's handle_ebadf and read_stdin; in processInputs file descriptor 1 is open .stdio stdout), i.e. the engine's write(1, …), which appends the bytes to the trace as stdout events (TraceEvent. .stdout) and leaves the file system unchanged. Ok. .Exception if the bytes are not readable.
def writeStdoutHook (s : State) : State :=
let buf := s.readReg64 .x0
let n := s.readReg64 .x1
if !s.mem.canRead buf n.toNat then unmodelled "write_stdout: unreadable bytes" s
else unitResult true (s.appendTrace (TraceEvent.ofBytes .stdout (s.mem.readBytes buf n.toNat)))CLI-specific extern hooks
def strlenHook
def memrchrHook
core::slice::memchr::memrchr(x: u8 = w0, text: &[u8] = (x1, x2)) -> Option<usize> (x0 = tag, x1 = value): Some(i) (x0 := 1, x1 := i) for the largest i < len with text[i] = x (x: the low byte of x0), else None (x0 := 0, x1 unchanged). .Exception if the bytes are not readable. Caller: rsplit_once('.') in address (cli/), which tests x0 = 1.
def memrchrHook (s : State) : State :=
let x := (s.readReg64 .x0).setWidth 8
let ptr := s.readReg64 .x1
let len := s.readReg64 .x2
if !s.mem.canRead ptr len.toNat then unmodelled "memrchr: unreadable bytes" s
else
let bs := s.mem.readBytes ptr len.toNat
match ((List.range bs.length).filter fun i => bs[i]? == some x).getLast? with
| some i => retHook (fun s => (s.writeReg64 .x0 1#64).writeReg64 .x1 (BitVec.ofNat 64 i)) s
| none => retHook (fun s => s.writeReg64 .x0 0#64) sdef Utf8Ok
bs is well-formed UTF-8: the UTF-8 encoding (String., shortest form) of some list of Unicode scalar values (Char: no surrogates, at most U+10FFFF). This is Rust's definition (core::str::from_utf8); equivalently ∃ s : String, s. up to UInt8..
def Utf8Ok (bs : List (BitVec 8)) : Prop :=
∃ cs : List Char, (cs.flatMap String.utf8EncodeChar).map UInt8.toBitVec = bsdef fromUtf8Hook
core::str::converts::from_utf8(v: &[u8] = (x0, x1)) -> Result<&str, Utf8Error> (sret x8, 24 bytes): if the bytes are well-formed UTF-8 (Utf8Ok), Ok(v): tag 0 @0, x0 @8, x1 @16. Otherwise Err: tag 1 @0, then err bs = (valid_up_to, error_len), a free choice, as the word @8 and the 16-bit field @16. .Exception if the bytes are not readable or the sret not writable. (Measured on the compiled from_utf8: str xzr, [x8]; stp x0, x1, [x8, #8] and str x9(=1), [x8]; str x9, [x8, #8]; strb, strb [x8, #16..17].)
open Classical in
noncomputable def fromUtf8Hook (err : List (BitVec 8) → BitVec 64 × BitVec 16) (s : State) :
State :=
let ptr := s.readReg64 .x0
let len := s.readReg64 .x1
let r := s.readReg64 .x8
if !(s.mem.canRead ptr len.toNat && s.mem.canWrite r 24) then
unmodelled "from_utf8: unreadable bytes or unwritable result" s
else
let bs := s.mem.readBytes ptr len.toNat
if Utf8Ok bs then
retHook (fun s => s.setMem (((s.mem.write64 r 0#64).write64 (r + 8#64) ptr).write64
(r + 16#64) len)) s
else
retHook (fun s => s.setMem (((s.mem.write64 r 1#64).write64 (r + 8#64) (err bs).1).write16
(r + 16#64) (err bs).2)) sdef durationSinceExactHook
<SystemTime>::duration_since(&self = x0, earlier = (x1 = tv_sec, w2 = tv_nsec)) -> Result<Duration, SystemTimeError> (sret x8), exactly std's Timespec::sub_timespec. With t1 = self (tv_sec: i64 @0, tv_nsec: u32 @8) and t2 = earlier, each read as tv_sec · 10^9 + tv_nsec nanoseconds (an integer), and d = |t1 - t2|: tag @0 = 0 (Ok) if t1 ≥ t2, else 1 (Err); the Duration (d / 10^9, d % 10^9) as secs: u64 @8 and nanos: u32 @16. .Exception if self's 12 bytes are not readable, the 24 sret bytes are not writable, or a tv_nsec is ≥ 10^9 (not a Timespec: undefined behaviour in Rust). (Measured on the compiled duration_since / sub_timespec: ldr x9, [x1]; ldr w12, [x1, #8] with self in x1 after mov x1, x9(=x0), earlier stored from x1 / w2; stp xzr, x9, [x19]; str w8, [x19, #16] for Ok, the tag XOR-ed with 1 for Err.) Every call site passes UNIX_EPOCH as earlier.
def durationSinceExactHook (s : State) : State :=
let a := s.readReg64 .x0
let r := s.readReg64 .x8
if !(s.mem.canRead a 12 && s.mem.canWrite r 24) then
unmodelled "duration_since: unreadable self or unwritable result" s
else
let ns1 := (s.mem.read32 (a + 8#64)).toNat
let ns2 := ((s.readReg64 .x2).setWidth 32).toNat
if ns1 < 1000000000 ∧ ns2 < 1000000000 then
let t1 : Int := (s.mem.read64 a).toInt * 1000000000 + ns1
let t2 : Int := (s.readReg64 .x1).toInt * 1000000000 + ns2
let d := (t1 - t2).natAbs
let tag : BitVec 64 := if t2 ≤ t1 then 0#64 else 1#64
retHook (fun s => s.setMem (((s.mem.write64 r tag).write64 (r + 8#64)
(BitVec.ofNat 64 (d / 1000000000))).write32 (r + 16#64)
(BitVec.ofNat 32 (d % 1000000000)))) s
else unmodelled "duration_since: tv_nsec out of range" sThe free choices and the table
structure CliHooks
What a CLI run is quantified over (the W of cliRunOf elf W and cliRunAbortOnPanic elf W): the abstract primitives and two results the CLI cannot observe.
structure CliHooks where
/-- SHA-256 compression, AES-256-CBC, X25519 (`CryptoGames.LibsignalSession.Crypto`). -/
C : CryptoGames.LibsignalSession.Crypto
/-- SHA-512, the Ed25519 curve core, Kyber1024, SPQR's ML-KEM-768 (`E2E.PqxdhPrims`). -/
Q : E2E.PqxdhPrims
/-- `bcmp`'s `x0` on the two compared byte strings. -/
bcmp : List (BitVec 8) → List (BitVec 8) → BitVec 64
/-- On equal lengths, the low 32 bits are zero exactly when the strings are equal (C's
contract). -/
bcmp_ok : ∀ a b, a.length = b.length → ((bcmp a b).setWidth 32 = 0#32 ↔ a = b)
/-- `from_utf8`'s `Err` payload `(valid_up_to, error_len)` on ill-formed bytes. -/
utf8Err : List (BitVec 8) → BitVec 64 × BitVec 16def cliBcmpHook
bcmp(a = x0, b = x1, n = x2) -> int: x0 := bcmp (n bytes at a) (n bytes at b), return: the function-level (C-ABI) bcmp contract with the value function bcmp. Unlike the CLI-specific hooks above it checks no permission (it does not fail closed): on unreadable bytes it over-approximates, reading them anyway, as the function-level contract does, so that the function-level proofs' bcmp facts transfer unchanged.
end CryptoVerification.SignalSession