~james/scope

scope/test/check.ml -rw-r--r-- 17.8 KiB
6e057d53 — james chore(import): snapshot james.dad.town/scope at 034e6b67d44c 14 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
open Scope
open Model

let count = ref 0
let check condition message = incr count; if not condition then failwith message
let virtual_address = Address.Virtual.of_int64
let physical_address = Address.Physical.of_int64
let supervisor access = { access; privilege = Supervisor; ac = false }
let user access = { access; privilege = User; ac = false }

let capture ?(context = Demo.context) pages =
  Capture.make context (List.map (fun (address, page) -> physical_address address, Ok page) pages)

let tables ?(upper = 7L) ?(middle = 7L) ?(parent = 7L) ?(leaf = 7L) () = [
  0x1000L, Demo.page [0, Int64.logor 0x2000L upper];
  0x2000L, Demo.page [0, Int64.logor 0x3000L middle];
  0x3000L, Demo.page [0, Int64.logor 0x4000L parent];
  0x4000L, Demo.page [0, Int64.logor 0x9000L leaf] ]

let explain capture ?(address = 0L) request =
  Paging.explain (Capture.context capture) (Capture.read capture) (virtual_address address) request

let expect_fault ?level vector code result = match result.outcome with
  | Fault f ->
      check (f.vector = vector) "fault vector";
      check (f.error_code = code) (Printf.sprintf "error code: expected %x got %x (%s)" code f.error_code f.reason);
      Option.iter (fun level -> check (f.level = Some level) "fault level") level
  | _ -> failwith "expected fault"

let expect_mapped result = match result.outcome with
  | Mapped m -> check true "mapped"; m
  | Fault f -> failwith ("unexpected fault: " ^ f.reason)
  | Incomplete reason -> failwith ("unexpected incomplete: " ^ reason)

let rejects action =
  check (try action (); false with Invalid_argument _ | Model.Limit _ -> true) "expected rejection"

let permissions () =
  for upper = 0 to 7 do
    for middle = 0 to 7 do
      for parent = 0 to 7 do
        for leaf = 0 to 7 do
          let flags x = Int64.of_int (1 lor ((x land 1) lsl 1) lor ((x land 2) lsl 1))
            |> fun bits -> if x land 4 <> 0 then Int64.logor bits Int64.min_int else bits in
          let capture = capture (tables ~upper:(flags upper) ~middle:(flags middle)
            ~parent:(flags parent) ~leaf:(flags leaf) ()) in
          let mapping = expect_mapped (Paging.walk (Capture.context capture) (Capture.read capture) (virtual_address 0L)) in
          check (mapping.permissions.writable = (upper land middle land parent land leaf land 1 <> 0)) "write accumulation";
          check (mapping.permissions.user = (upper land middle land parent land leaf land 2 <> 0)) "user accumulation";
          check (mapping.permissions.executable = ((upper lor middle lor parent lor leaf) land 4 = 0)) "nx accumulation"
        done
      done
    done
  done;
  let c = capture (tables ~parent:5L ()) in
  expect_fault ~level:2 14 3 (explain c (supervisor Write));
  expect_fault ~level:2 14 7 (explain c (user Write));
  let context = { Demo.context with registers = { Demo.context.registers with cr0 = 0x80000001L } } in
  ignore (expect_mapped (explain (capture ~context (tables ~parent:5L ())) (supervisor Write)));
  expect_fault 14 5 (explain (capture (tables ~upper:3L ())) (user Read));
  expect_fault 14 17 (explain (capture (tables ~parent:0x8000000000000007L ())) (supervisor Execute));
  let context = { Demo.context with registers = { Demo.context.registers with cr4 = 0x100020L } } in
  expect_fault 14 17 (explain (capture ~context (tables ())) (supervisor Execute));
  let context = { Demo.context with registers = { Demo.context.registers with cr4 = 0x200020L } } in
  let c = capture ~context (tables ()) in
  expect_fault 14 1 (explain c (supervisor Read));
  expect_fault 14 3 (explain c (supervisor Write));
  ignore (expect_mapped (explain c { access = Write; privilege = Supervisor; ac = true }));
  ignore (expect_mapped (explain c (supervisor Execute)))

let translation () =
  let c = capture (tables ()) in
  let m = expect_mapped (explain c ~address:0xabcL (supervisor Read)) in
  check (Address.Physical.to_int64 m.physical_address = 0x9abcL) "4k offset";
  expect_fault 13 0 (explain c ~address:0x0000800000000000L (supervisor Read));
  expect_fault 13 0 (explain c ~address:0xffff7fffffffffffL (supervisor Read));
  let c = capture [0x1000L, Demo.page [511, 0x2007L]; 0x2000L, Demo.page [511, 0x3007L];
    0x3000L, Demo.page [511, 0x4007L]; 0x4000L, Demo.page [511, 0x9007L]] in
  let m = expect_mapped (explain c ~address:(-1L) (supervisor Read)) in
  check (Address.Physical.to_int64 m.physical_address = 0x9fffL) "upper canonical address";
  let regions = Paging.enumerate (Capture.context c) (Capture.read c) in
  check (List.length regions = 1 && (List.hd regions).start = 0xfffffffffffff000L) "upper enumeration";
  let c = capture [0x1000L, Demo.page [0, 0x2007L]; 0x2000L, Demo.page [0, 0x3007L];
    0x3000L, Demo.page [0, 0x201087L]] in
  let m = expect_mapped (explain c ~address:0x1fffffL (supervisor Read)) in
  check (m.size = 0x200000L && Address.Physical.to_int64 m.physical_address = 0x3fffffL) "2m pat and offset";
  let c = capture [0x1000L, Demo.page [0, 0x2007L]; 0x2000L, Demo.page [0, 0x40001087L]] in
  let m = expect_mapped (explain c ~address:0x3fffffffL (supervisor Read)) in
  check (m.size = 0x40000000L && Address.Physical.to_int64 m.physical_address = 0x7fffffffL) "1g pat and offset";
  let c = capture (tables ~leaf:0x87L ()) in
  ignore (expect_mapped (explain c (supervisor Read)));
  let context = { Demo.context with registers = { Demo.context.registers with cr3 = 0x1abcL; cr4 = 0x20020L } } in
  ignore (expect_mapped (explain (capture ~context (tables ())) (supervisor Read)))

let invalid () =
  expect_fault ~level:4 14 9 (explain (capture (tables ~upper:0x87L ())) (supervisor Read));
  expect_fault ~level:1 14 9 (explain (capture (tables ~leaf:0x1000000000007L ())) (supervisor Read));
  let context = { Demo.context with registers = { Demo.context.registers with efer = 0x500L } } in
  expect_fault 14 9 (explain (capture ~context (tables ~leaf:0x8000000000000007L ())) (supervisor Read));
  let context = { Demo.context with features = { Demo.context.features with gb_pages = false } } in
  let c = capture ~context [0x1000L, Demo.page [0, 0x2007L]; 0x2000L, Demo.page [0, 0x87L]] in
  expect_fault 14 9 (explain c (supervisor Read));
  let c = capture [0x1000L, Demo.page [0, 0x2007L]; 0x2000L, Demo.page [0, 0x3007L]; 0x3000L, Demo.page [0, 0x2087L]] in
  expect_fault 14 9 (explain c (supervisor Read));
  let c = capture (tables ~parent:0L ()) in
  expect_fault ~level:2 14 0 (explain c (supervisor Read));
  let c = capture (tables ~parent:0L ~upper:1L ()) in
  expect_fault ~level:2 14 6 (explain c (user Write));
  List.iter (fun bit -> rejects (fun () ->
    Model.validate { Demo.context with registers = { Demo.context.registers with cr4 = Int64.logor 0x20L (Int64.shift_left 1L bit) } })) [12; 22; 23; 24];
  let context = { Demo.context with registers = { Demo.context.registers with cr3 = 0x1abcL } } in
  ignore (expect_mapped (explain (capture ~context (tables ())) (supervisor Read)));
  rejects (fun () -> ignore (Address.parse "-1"));
  rejects (fun () -> ignore (Address.parse "18446744073709551616"));
  check (Address.parse "18446744073709551615" = -1L) "unsigned decimal parsing"

let incomplete () =
  let c = capture [0x1000L, Demo.page [0, 0x2007L]] in
  check (match (explain c (supervisor Read)).outcome with Incomplete _ -> true | _ -> false) "missing table is incomplete";
  let regions = Paging.enumerate (Capture.context c) (Capture.read c) in
  check (List.length regions = 1 && (List.hd regions).length = 0x8000000000L) "missing subtree range";
  let c = Capture.make Demo.context [] in
  let regions = Paging.enumerate Demo.context (Capture.read c) in
  check (List.length regions = 2) "missing root covers both canonical halves";
  let c = Demo.capture ~after:false in
  rejects (fun () -> ignore (Paging.enumerate ~max_entries:1 Demo.context (Capture.read c)));
  rejects (fun () -> ignore (Capture.collect ~max_pages:1 Demo.context (Capture.read c)));
  let reads = ref 0 in
  let collected = Capture.collect Demo.context (fun address -> incr reads; Capture.read c address) in
  check (!reads = 5 && Capture.pages collected = 5) "capture memoises table frames"

let regions capture = Paging.enumerate (Capture.context capture) (Capture.read capture)

let diff () =
  let a = Demo.capture ~after:false and b = Demo.capture ~after:true in
  let changes = Analysis.diff (regions a) (regions b) in
  check (List.length changes = 2) "demo diff count";
  check ((List.hd changes).kinds = ["permissions"]) "parent permission diff";
  check ((List.nth changes 1).kinds = ["backing"]) "backing diff";
  check (Analysis.diff (regions a) (regions a) = []) "identity diff";
  let a = capture (tables ()) in
  let b = capture (tables ~leaf:0x67L ()) in
  check (Analysis.diff (regions a) (regions b) = []) "ignore ad by default";
  check ((List.hd (Analysis.diff ~include_ad:true (regions a) (regions b))).kinds = ["accessed_dirty"]) "include ad";
  let huge = capture [0x1000L, Demo.page [0, 0x2007L]; 0x2000L, Demo.page [0, 0x3007L]; 0x3000L, Demo.page [0, 0x87L]] in
  let small = capture [0x1000L, Demo.page [0, 0x2007L]; 0x2000L, Demo.page [0, 0x3007L];
    0x3000L, Demo.page [0, 0x4007L]; 0x4000L, Demo.page (List.init 512 (fun index -> index, Int64.logor 7L (Int64.of_int (index * 4096))))] in
  let changes = Analysis.diff (regions huge) (regions small) in
  check (List.length changes = 512 && List.for_all (fun (c : Analysis.change) -> c.kinds = ["page_size"]) changes) "huge split preserves backing";
  let partial = Capture.make Demo.context [] in
  let changes = Analysis.diff (regions partial) (regions a) in
  check (List.for_all (fun (c : Analysis.change) -> c.kinds = ["incomplete"]) changes) "incomplete is not removal"

let aliases () =
  let pairs = Analysis.aliases (regions (Demo.capture ~after:false)) in
  check (List.length pairs = 1) "demo alias";
  let pair = List.hd pairs in
  check (pair.physical = 0x9000L && pair.length = 4096L) "alias overlap";
  check (Analysis.aliases (regions (Demo.capture ~after:true)) = []) "demo resolved alias";
  let make_mapping va pa size permissions = {
    virtual_address = virtual_address va; physical_address = physical_address pa;
    size; permissions; leaf = 0L; trace = [] } in
  let writable = { unrestricted with executable = false } in
  let executable = { unrestricted with writable = false } in
  let a = make_mapping 0L 0x200000L 0x200000L executable in
  let b = make_mapping 0x800000L 0x201000L 4096L writable in
  let pairs = Analysis.aliases [
    { Paging.start = 0L; length = a.size; state = Paging.Present a };
    { Paging.start = 0x800000L; length = b.size; state = Paging.Present b } ] in
  check (List.length pairs = 1 && (List.hd pairs).physical = 0x201000L && (List.hd pairs).length = 4096L) "partial huge alias"

let codec () =
  let path = Filename.temp_file "scope" ".capture" in
  Fun.protect ~finally:(fun () -> Sys.remove path) (fun () ->
    let original = Demo.capture ~after:false in
    Capture.save path original;
    let loaded = Capture.load path in
    check (Capture.context original = Capture.context loaded) "context roundtrip";
    check (regions original = regions loaded) "table roundtrip";
    let channel = open_out_gen [Open_wronly; Open_binary] 0o600 path in
    seek_out channel 32;
    output_char channel '\255';
    close_out channel;
    rejects (fun () -> ignore (Capture.load path)));
  rejects (fun () -> ignore (Capture.make Demo.context [physical_address 0x1000L, Ok "short"]));
  rejects (fun () -> ignore (Capture.make Demo.context [physical_address 0x1001L, Ok (Demo.page [])]));
  rejects (fun () -> ignore (capture [0x1000L, Demo.page []; 0x1000L, Demo.page []]))

let generated () =
  let random = Random.State.make [|0x5c0; 0x2026|] in
  let generate () =
    let entries = List.init 64 (fun index ->
      let frame = Random.State.int random 16 in
      let flags = Int64.of_int (1 lor (Random.State.int random 4 lsl 1)) in
      let flags = if Random.State.bool random then Int64.logor flags Int64.min_int else flags in
      index, if Random.State.int random 4 = 0 then 0L else Int64.logor (Int64.of_int (frame * 4096)) flags) in
    capture [0x1000L, Demo.page [0, 0x2007L]; 0x2000L, Demo.page [0, 0x3007L];
      0x3000L, Demo.page [0, 0x4007L]; 0x4000L, Demo.page entries]
  in
  for _ = 1 to 64 do
    let a = generate () and b = generate () in
    let changes = Analysis.diff (regions a) (regions b) in
    for index = 0 to 63 do
      let address = Int64.of_int (index * 4096) in
      let state capture = match (explain capture ~address (supervisor Read)).outcome with
        | Mapped m -> Some (Paging.Present m)
        | Fault _ -> None
        | Incomplete _ -> failwith "generated graph unexpectedly incomplete"
      in
      let expected = Analysis.differences ~include_ad:false (state a) (state b) in
      let observed = List.find_opt (fun (c : Analysis.change) ->
        Address.compare address c.start >= 0 && Address.compare address (Int64.pred (Int64.add c.start c.length)) <= 0) changes
        |> Option.map (fun (c : Analysis.change) -> c.kinds) |> Option.value ~default:[] in
      check (expected = observed) "interval diff agrees with point translations"
    done;
    let mappings = regions a |> List.filter_map (fun r -> match r.Paging.state with Paging.Present m -> Some m | _ -> None) in
    let physical m = Address.Physical.to_int64 m.physical_address in
    let virtual_raw m = Address.Virtual.to_int64 m.virtual_address in
    let expected = List.concat_map (fun w -> List.filter_map (fun x ->
      if w.permissions.writable && x.permissions.executable && virtual_raw w <> virtual_raw x && physical w = physical x then
        Some (physical w, 4096L, virtual_raw w, virtual_raw x) else None) mappings) mappings |> List.sort compare in
    let actual = Analysis.aliases (regions a) |> List.map (fun (a : Analysis.alias) ->
      a.physical, a.length, virtual_raw a.writable, virtual_raw a.executable) |> List.sort compare in
    check (expected = actual) "alias sweep agrees with pairwise overlap oracle"
  done;
  let self = capture [0x1000L, Demo.page [0, 0x1007L]] in
  let reads = ref 0 in
  let closed = Capture.collect Demo.context (fun address -> incr reads; Capture.read self address) in
  check (!reads = 1 && Capture.pages closed = 1) "recursive mapping is read once across levels";
  ignore (expect_mapped (explain closed (supervisor Read)));
  let context = { Demo.context with registers = { Demo.context.registers with cr3 = 0x1000L } } in
  let exploding = capture ~context [0x1000L, Demo.page (List.init 512 (fun index -> index, 0x1007L))] in
  rejects (fun () -> ignore (Paging.enumerate ~max_entries:4096 context (Capture.read exploding)))

let imports () =
  let path = Filename.temp_file "scopeimport" "" in
  Fun.protect ~finally:(fun () -> Sys.remove path) (fun () ->
    let context = "context 0x80010001 0 0x1000 0x20 0xd00 2 0 48 1 1" in
    let write text =
      let channel = open_out_bin path in
      Fun.protect ~finally:(fun () -> close_out_noerr channel) (fun () -> output_string channel text)
    in
    List.iter (fun ending ->
      write (context ^ ending);
      let imported = Capture.import ~source:"test" path in
      check (Capture.pages imported = 0) "import accepts context at eof") [""; "\n"];
    List.iter (fun ending ->
      write (context ^ "\n" ^ String.make 16384 ' ' ^ ending);
      check (Capture.pages (Capture.import ~source:"test" path) = 0)
        "import accepts maximum line length") [""; "\n"];
    List.iter (fun ending ->
      write (context ^ "\n" ^ String.make 16385 ' ' ^ ending);
      rejects (fun () -> ignore (Capture.import ~source:"test" path))) [""; "\n"];
    write (context ^ "\n" ^ String.make (2 * 1024 * 1024) 'x');
    let allocated = Gc.allocated_bytes () in
    let rejected = try ignore (Capture.import ~source:"test" path); false with
      Invalid_argument reason -> reason = "import line exceeds 16384 bytes"
    in
    let allocated = Gc.allocated_bytes () -. allocated in
    check rejected "import rejects oversized record before parsing";
    check (allocated < 1048576.) "import rejects oversized line with bounded allocation")

let watch () =
  let base = Filename.temp_file "scopewatch" "" in
  Sys.remove base;
  let a = Demo.capture ~after:false and b = Demo.capture ~after:true in
  let switched = capture ~context:{ Demo.context with registers = { Demo.context.registers with cr3 = 0x6000L } }
    ((0x6000L, Demo.page [0, 0x2007L]) :: Demo.frames ~after:true) in
  let samples = ref [a; b; b; Capture.make Demo.context []; b; switched] in
  let events = ref [] in
  let capture () = match !samples with x :: xs -> samples := xs; x | [] -> failwith "sample exhaustion" in
  let status = Watch.run ~capture ~wait:(fun _ -> ()) ~emit:(fun event -> events := event :: !events)
    ~directory:base ~interval:0.1 ~count:6 ~retain:2 ~max_entries:10000 ~max_pairs:100
    ~include_ad:false ~scan_aliases:true ~selected:(fun _ -> true) () in
  check (status = 2) "watch remembers incomplete samples";
  let events = List.rev !events in
  check (List.map (fun (e : Watch.event) -> e.kind) events =
    ["baseline"; "change"; "sample"; "incomplete"; "sample"; "address_space"]) "watch transitions";
  check (List.length (List.nth events 1).changes = 2) "watch semantic changes";
  check ((List.hd events).mappings = 2 && (List.hd events).bytes = 8192L) "watch statistics";
  check (List.length (List.hd events).aliases = 1) "watch alias baseline";
  check (Array.length (Sys.readdir base) = 2) "watch bounded retention";
  let pair = Filename.concat base "00000006" in
  check (Capture.context (Capture.load (Filename.concat pair "before")) = Capture.context b) "watch preserves last complete baseline";
  check ((List.nth events 5).changes = []) "watch does not diff unrelated roots";
  Array.iter (fun name -> let path = Filename.concat base name in
    Array.iter (fun child -> Sys.remove (Filename.concat path child)) (Sys.readdir path);
    Unix.rmdir path) (Sys.readdir base);
  Unix.rmdir base

let () =
  permissions ();
  translation ();
  invalid ();
  incomplete ();
  diff ();
  aliases ();
  codec ();
  generated ();
  imports ();
  watch ();
  Printf.printf "%d checks passed\n" !count