feat(checker): validate native stack and register effects
7 files changed, +88 −3
- ROOT modified +1 −0
- cli/driver.ML modified +2 −1
- src/Checker.thy added +66 −0
- src/Export.thy modified +1 −1
- src/Frontend.thy modified +8 −1
- tests/cli.py modified +8 −0
- tests/native.py modified +2 −0
ROOT modified
| @@ -13,6 +13,7 @@ session Remy in src = Remybase + | ||
| 13 | 13 | Compiler |
| 14 | 14 | Native |
| 15 | 15 | Encoding |
| 16 | + Checker | |
| 16 | 17 | Frontend |
| 17 | 18 | Examples |
| 18 | 19 | Export |
cli/driver.ML modified
| @@ -36,9 +36,10 @@ fun main () = | ||
| 36 | 36 | | ["eval", source, x, y, t] => output (Remy.reference (read source) (chars x) (chars y) (chars t)) |
| 37 | 37 | | ["limits", depth, nodes, stack, source, destination] => |
| 38 | 38 | emit destination (Remy.configured (chars depth) (chars nodes) (chars stack) (read source)) |
| 39 | + | ["check", binary] => output (Remy.validate (read binary)) | |
| 39 | 40 | | ["disassemble", binary] => output (Remy.disassemble (read binary)) |
| 40 | 41 | | [source, destination] => emit destination (Remy.artifact (read source)) |
| 41 | - | _ => raise Fail "usage: remy disassemble binary | remy limits depth nodes stack source binary | remy source binary | remy vectors source data | remy eval source x y t") | |
| 42 | + | _ => raise Fail "usage: remy check binary | remy disassemble binary | remy limits depth nodes stack source binary | remy source binary | remy vectors source data | remy eval source x y t") | |
| 42 | 43 | handle exn => |
| 43 | 44 | (TextIO.output (TextIO.stdErr, "remy: " ^ General.exnMessage exn ^ "\n"); |
| 44 | 45 | OS.Process.exit OS.Process.failure); |
src/Checker.thy added
| @@ -0,0 +1,66 @@ | ||
| 1 | +theory Checker | |
| 2 | + imports Encoding | |
| 3 | +begin | |
| 4 | + | |
| 5 | +fun effect :: "opcode \<Rightarrow> nat \<Rightarrow> nat option" where | |
| 6 | + "effect (Push r) n = Some (Suc n)" | |
| 7 | +| "effect (Pop r) n = (if n = 0 then None else Some (n - 1))" | |
| 8 | +| "effect (Peek i) n = (if i < n then Some n else None)" | |
| 9 | +| "effect _ n = Some n" | |
| 10 | + | |
| 11 | +fun audit :: "opcode list \<Rightarrow> nat \<Rightarrow> nat option" where | |
| 12 | + "audit [] n = Some n" | |
| 13 | +| "audit (op # code) n = (if writes op \<subseteq> {Rax,Rcx,Rbx} then | |
| 14 | + Option.bind (effect op n) (\<lambda>m. if m \<le> 512 then audit code m else None) else None)" | |
| 15 | + | |
| 16 | +lemma effect_sound: | |
| 17 | + "effect op (length (stack s)) = Some n \<Longrightarrow> | |
| 18 | + \<exists>t. transition op s = Some t \<and> length (stack t) = n" | |
| 19 | + by (cases op; cases "stack s") (auto split: if_splits) | |
| 20 | + | |
| 21 | +theorem audit_sound: | |
| 22 | + "audit code (length (stack s)) = Some n \<Longrightarrow> | |
| 23 | + \<exists>t. run code s = Some t \<and> length (stack t) = n" | |
| 24 | +proof (induction code arbitrary: s) | |
| 25 | + case Nil | |
| 26 | + then show ?case by simp | |
| 27 | +next | |
| 28 | + case (Cons op code) | |
| 29 | + then show ?case | |
| 30 | + by (auto dest!: effect_sound split: if_splits bind_splits; fastforce) | |
| 31 | +qed | |
| 32 | + | |
| 33 | +lemma audit_scratch: | |
| 34 | + "audit code n = Some m \<Longrightarrow> list_all (\<lambda>op. writes op \<subseteq> {Rax,Rcx,Rbx}) code" | |
| 35 | + by (induction code arbitrary: n) (auto split: if_splits bind_splits) | |
| 36 | + | |
| 37 | +theorem audit_inputs: | |
| 38 | + "audit code n = Some m \<Longrightarrow> run code s = Some t \<Longrightarrow> inputs t = inputs s" | |
| 39 | +proof - | |
| 40 | + assume audited: "audit code n = Some m" and ran: "run code s = Some t" | |
| 41 | + have scratch: "list_all (\<lambda>op. writes op \<subseteq> {Rax,Rcx,Rbx}) code" | |
| 42 | + using audit_scratch[OF audited] . | |
| 43 | + show ?thesis | |
| 44 | + proof (rule ext) | |
| 45 | + fix i | |
| 46 | + have keep: "list_all (\<lambda>op. input_register i \<notin> writes op) code" | |
| 47 | + unfolding list_all_iff | |
| 48 | + proof (rule ballI) | |
| 49 | + fix op | |
| 50 | + assume member: "op \<in> set code" | |
| 51 | + have bound: "writes op \<subseteq> {Rax,Rcx,Rbx}" | |
| 52 | + using scratch member by (auto simp: list_all_iff) | |
| 53 | + show "input_register i \<notin> writes op" | |
| 54 | + proof | |
| 55 | + assume written: "input_register i \<in> writes op" | |
| 56 | + have "input_register i \<in> {Rax,Rcx,Rbx}" | |
| 57 | + using subsetD[OF bound written] . | |
| 58 | + then show False by simp | |
| 59 | + qed | |
| 60 | + qed | |
| 61 | + show "inputs t i = inputs s i" | |
| 62 | + unfolding inputs_def using run_preserves[OF ran keep] . | |
| 63 | + qed | |
| 64 | +qed | |
| 65 | + | |
| 66 | +end | |
src/Export.thy modified
| @@ -6,6 +6,6 @@ definition ascii :: "integer \<Rightarrow> char" where "ascii = char_of" | ||
| 6 | 6 | |
| 7 | 7 | definition ordinal :: "char \<Rightarrow> integer" where "ordinal = of_char" |
| 8 | 8 | |
| 9 | -export_code disassemble configured artifact vectors reference ascii ordinal in SML module_name Remy file_prefix remy | |
| 9 | +export_code validate disassemble configured artifact vectors reference ascii ordinal in SML module_name Remy file_prefix remy | |
| 10 | 10 | |
| 11 | 11 | end |
src/Frontend.thy modified
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | theory Frontend |
| 2 | - imports Encoding | |
| 2 | + imports Checker | |
| 3 | 3 | begin |
| 4 | 4 | |
| 5 | 5 | fun finish :: "string \<Rightarrow> string list \<Rightarrow> string list" where |
| @@ -208,6 +208,13 @@ definition disassemble :: "string \<Rightarrow> bool \<times> string" where | ||
| 208 | 208 | None \<Rightarrow> (False,''invalid native block stream'' @ newline) |
| 209 | 209 | | Some code \<Rightarrow> (True,listing 0 code))" |
| 210 | 210 | |
| 211 | +definition validate :: "string \<Rightarrow> bool \<times> string" where | |
| 212 | + "validate bytes = (case blocks (map nat_of_char bytes) of | |
| 213 | + None \<Rightarrow> (False,''invalid native block stream'' @ newline) | |
| 214 | + | Some code \<Rightarrow> if audit (map snd code) 0 = Some 0 | |
| 215 | + then (True,''native stack and register effects verified'' @ newline) | |
| 216 | + else (False,''invalid native stack or register effects'' @ newline))" | |
| 217 | + | |
| 211 | 218 | datatype problem = Unexpected nat | Exhausted nat |
| 212 | 219 | |
| 213 | 220 | datatype 'a outcome = Failed problem | Parsed 'a |
tests/cli.py modified
| @@ -55,6 +55,14 @@ def main(): | ||
| 55 | 55 | assert 'invalid native' in invoke('disassemble',binary,ok=False) |
| 56 | 56 | binary.write_bytes(bytes([72,184,42,0,0,0,0,0,0,0,195])) |
| 57 | 57 | assert '10 ret' in invoke('disassemble',binary) |
| 58 | + source.write_text('(+ x y)') | |
| 59 | + invoke(source,binary) | |
| 60 | + assert 'verified' in invoke('check',binary) | |
| 61 | + for code in [[88,195],[80,195],[65,88,195],[73,137,192,195],[72,139,68,36,0,195]]: | |
| 62 | + binary.write_bytes(bytes(code)) | |
| 63 | + invoke('check',binary,ok=False) | |
| 64 | + binary.write_bytes(bytes([72,199,192,42,0,0,0,195])) | |
| 65 | + invoke('check',binary) | |
| 58 | 66 | source.write_text('é') |
| 59 | 67 | assert 'ASCII' in invoke(source,binary,ok=False) |
| 60 | 68 | for text in ['0x','0x10000000000000000','0xfg','0xffextra','-0x1']: |
tests/native.py modified
| @@ -80,6 +80,8 @@ def main(): | ||
| 80 | 80 | path.write_text(source(case)) |
| 81 | 81 | result = subprocess.run([ROOT / ".build/remy", path, binary], capture_output=True, text=True, timeout=30) |
| 82 | 82 | assert result.returncode == 0, (case, result.stderr) |
| 83 | + checked = subprocess.run([ROOT / ".build/remy", "check", binary],capture_output=True,text=True,timeout=30) | |
| 84 | + assert checked.returncode == 0, (case,checked.stderr) | |
| 83 | 85 | function, image, thunk = native(binary.read_bytes()) |
| 84 | 86 | vectors = [(a, b, a ^ b) for a in (0, 1, MASK, 1 << 63) for b in (0, 1, 63, 64, 65, MASK)] |
| 85 | 87 | vectors += [tuple(rng.getrandbits(64) for _ in range(3)) for _ in range(24)] |