~james/remy

feat(checker): validate native stack and register effects

commit
31d281ada88d2822fbbece49ea5e511b0ad3ada3 browse tree
parent
5ccdadce3e9d6b55c645dcb7057b9d5c747f501a
author
james <j@cpl0.zip>
signature
Verified ~james signed with a key registered by ~james
date
2026-10-21 11:00 UTC

7 files changed, +88 −3

ROOT modified

@@ -13,6 +13,7 @@ session Remy in src = Remybase +
1313 Compiler
1414 Native
1515 Encoding
16+ Checker
1617 Frontend
1718 Examples
1819 Export

cli/driver.ML modified

@@ -36,9 +36,10 @@ fun main () =
3636 | ["eval", source, x, y, t] => output (Remy.reference (read source) (chars x) (chars y) (chars t))
3737 | ["limits", depth, nodes, stack, source, destination] =>
3838 emit destination (Remy.configured (chars depth) (chars nodes) (chars stack) (read source))
39+ | ["check", binary] => output (Remy.validate (read binary))
3940 | ["disassemble", binary] => output (Remy.disassemble (read binary))
4041 | [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")
4243 handle exn =>
4344 (TextIO.output (TextIO.stdErr, "remy: " ^ General.exnMessage exn ^ "\n");
4445 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"
66
77 definition ordinal :: "char \<Rightarrow> integer" where "ordinal = of_char"
88
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
1010
1111 end

src/Frontend.thy modified

@@ -1,5 +1,5 @@
11 theory Frontend
2- imports Encoding
2+ imports Checker
33 begin
44
55 fun finish :: "string \<Rightarrow> string list \<Rightarrow> string list" where
@@ -208,6 +208,13 @@ definition disassemble :: "string \<Rightarrow> bool \<times> string" where
208208 None \<Rightarrow> (False,''invalid native block stream'' @ newline)
209209 | Some code \<Rightarrow> (True,listing 0 code))"
210210
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+
211218 datatype problem = Unexpected nat | Exhausted nat
212219
213220 datatype 'a outcome = Failed problem | Parsed 'a

tests/cli.py modified

@@ -55,6 +55,14 @@ def main():
5555 assert 'invalid native' in invoke('disassemble',binary,ok=False)
5656 binary.write_bytes(bytes([72,184,42,0,0,0,0,0,0,0,195]))
5757 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)
5866 source.write_text('é')
5967 assert 'ASCII' in invoke(source,binary,ok=False)
6068 for text in ['0x','0x10000000000000000','0xfg','0xffextra','-0x1']:

tests/native.py modified

@@ -80,6 +80,8 @@ def main():
8080 path.write_text(source(case))
8181 result = subprocess.run([ROOT / ".build/remy", path, binary], capture_output=True, text=True, timeout=30)
8282 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)
8385 function, image, thunk = native(binary.read_bytes())
8486 vectors = [(a, b, a ^ b) for a in (0, 1, MASK, 1 << 63) for b in (0, 1, 63, 64, 65, MASK)]
8587 vectors += [tuple(rng.getrandbits(64) for _ in range(3)) for _ in range(24)]