| @@ -0,0 +1,228 @@ |
| 1 | +module Frames = Map.Make (struct |
| 2 | + type t = int64 |
| 3 | + let compare = Address.compare |
| 4 | +end) |
| 5 | + |
| 6 | +type t = { |
| 7 | + context : Model.context; |
| 8 | + frames : (string, string) result Frames.t; |
| 9 | +} |
| 10 | + |
| 11 | +let maximum_pages = 65536 |
| 12 | +let maximum_bytes = 280_000_000 |
| 13 | +let context capture = capture.context |
| 14 | +let pages capture = Frames.cardinal capture.frames |
| 15 | + |
| 16 | +let make context frames = |
| 17 | + Model.validate context; |
| 18 | + if List.length frames > maximum_pages then invalid_arg "too many capture pages"; |
| 19 | + let frames = List.fold_left (fun map (address, page) -> |
| 20 | + let raw = Address.Physical.to_int64 address in |
| 21 | + if Int64.logand raw 0xfffL <> 0L || Address.compare raw (Address.mask context.features.phys_bits) > 0 then |
| 22 | + invalid_arg "unaligned or out-of-range table frame"; |
| 23 | + if Frames.mem raw map then invalid_arg "duplicate table frame"; |
| 24 | + (match page with |
| 25 | + | Ok bytes when String.length bytes <> 4096 -> invalid_arg "table must contain 4096 bytes" |
| 26 | + | Error reason when String.length reason > 4096 -> invalid_arg "table error is too long" |
| 27 | + | _ -> ()); |
| 28 | + Frames.add raw page map) Frames.empty frames |
| 29 | + in |
| 30 | + { context; frames } |
| 31 | + |
| 32 | +let read capture address = |
| 33 | + let raw = Address.Physical.to_int64 address in |
| 34 | + match Frames.find_opt raw capture.frames with |
| 35 | + | Some page -> page |
| 36 | + | None -> Error ("table not captured at " ^ Address.hex raw) |
| 37 | + |
| 38 | +let with_root capture root = |
| 39 | + let context = { capture.context with root_override = Some root } in |
| 40 | + Model.validate context; |
| 41 | + { capture with context } |
| 42 | + |
| 43 | +let collect ?(max_pages = maximum_pages) context reader = |
| 44 | + Model.validate context; |
| 45 | + if max_pages < 1 || max_pages > maximum_pages then invalid_arg "invalid capture page budget"; |
| 46 | + let frames = ref Frames.empty and visited = Hashtbl.create 64 in |
| 47 | + let rec go level address = |
| 48 | + let raw = Address.Physical.to_int64 address in |
| 49 | + if not (Hashtbl.mem visited (level, raw)) then begin |
| 50 | + Hashtbl.add visited (level, raw) (); |
| 51 | + let page = match Frames.find_opt raw !frames with |
| 52 | + | Some page -> page |
| 53 | + | None -> |
| 54 | + if Frames.cardinal !frames >= max_pages then raise (Model.Limit "capture page budget exceeded"); |
| 55 | + let page = reader address in |
| 56 | + (match page with Ok bytes when String.length bytes <> 4096 -> invalid_arg "short table read" | _ -> ()); |
| 57 | + frames := Frames.add raw page !frames; |
| 58 | + page |
| 59 | + in |
| 60 | + match page with |
| 61 | + | Error _ -> () |
| 62 | + | Ok bytes -> |
| 63 | + for index = 0 to 511 do |
| 64 | + let entry = String.get_int64_le bytes (index * 8) in |
| 65 | + match Paging.decode context.features ~nxe:(Address.bit context.registers.efer 11) ~level entry with |
| 66 | + | Paging.Table next -> go (level - 1) next |
| 67 | + | _ -> () |
| 68 | + done |
| 69 | + end |
| 70 | + in |
| 71 | + go 4 (Model.root context); |
| 72 | + { context; frames = !frames } |
| 73 | + |
| 74 | +let complete capture = |
| 75 | + let closure = collect capture.context (read capture) in |
| 76 | + Frames.for_all (fun _ -> function Ok _ -> true | Error _ -> false) closure.frames |
| 77 | + |
| 78 | +let add_u32 buffer n = |
| 79 | + let bytes = Bytes.create 4 in |
| 80 | + Bytes.set_int32_le bytes 0 (Int32.of_int n); |
| 81 | + Buffer.add_bytes buffer bytes |
| 82 | + |
| 83 | +let add_u64 buffer n = |
| 84 | + let bytes = Bytes.create 8 in |
| 85 | + Bytes.set_int64_le bytes 0 n; |
| 86 | + Buffer.add_bytes buffer bytes |
| 87 | + |
| 88 | +let add_string buffer s = |
| 89 | + if String.length s > 4096 then invalid_arg "capture string exceeds 4096 bytes"; |
| 90 | + add_u32 buffer (String.length s); |
| 91 | + Buffer.add_string buffer s |
| 92 | + |
| 93 | +let encode capture = |
| 94 | + let buffer = Buffer.create (128 + pages capture * 4112) in |
| 95 | + Buffer.add_string buffer "SCOPE\000\001\n"; |
| 96 | + let c = capture.context in |
| 97 | + add_u32 buffer c.domid; |
| 98 | + add_u32 buffer c.vcpu; |
| 99 | + add_u32 buffer c.features.phys_bits; |
| 100 | + add_u32 buffer ((if c.features.nx then 1 else 0) lor (if c.features.gb_pages then 2 else 0)); |
| 101 | + let r = c.registers in |
| 102 | + List.iter (add_u64 buffer) [r.cr0; r.cr2; r.cr3; r.cr4; r.efer; r.rflags; r.rip]; |
| 103 | + add_u32 buffer (if c.root_override = None then 0 else 1); |
| 104 | + add_u64 buffer (Option.value c.root_override ~default:0L); |
| 105 | + List.iter (add_string buffer) [c.source; c.consistency; c.identity]; |
| 106 | + add_u32 buffer (pages capture); |
| 107 | + Frames.iter (fun address page -> |
| 108 | + add_u64 buffer address; |
| 109 | + match page with |
| 110 | + | Ok bytes -> add_u32 buffer 0; Buffer.add_string buffer bytes |
| 111 | + | Error reason -> add_u32 buffer 1; add_string buffer reason) capture.frames; |
| 112 | + let payload = Buffer.contents buffer in |
| 113 | + payload ^ Digest.string payload |
| 114 | + |
| 115 | +let save path capture = |
| 116 | + let bytes = encode capture in |
| 117 | + let temporary, channel = Filename.open_temp_file ~temp_dir:(Filename.dirname path) ".scope" ".tmp" in |
| 118 | + Fun.protect ~finally:(fun () -> close_out_noerr channel; if Sys.file_exists temporary then Sys.remove temporary) |
| 119 | + (fun () -> |
| 120 | + output_string channel bytes; |
| 121 | + flush channel; |
| 122 | + Unix.fsync (Unix.descr_of_out_channel channel); |
| 123 | + close_out channel; |
| 124 | + Unix.rename temporary path) |
| 125 | + |
| 126 | +let load path = |
| 127 | + let channel = open_in_bin path in |
| 128 | + let bytes = Fun.protect ~finally:(fun () -> close_in_noerr channel) (fun () -> |
| 129 | + let length = in_channel_length channel in |
| 130 | + if length > maximum_bytes || length < 24 then invalid_arg "capture size outside format bounds"; |
| 131 | + really_input_string channel length) |
| 132 | + in |
| 133 | + let limit = String.length bytes - 16 in |
| 134 | + if Digest.string (String.sub bytes 0 limit) <> String.sub bytes limit 16 then |
| 135 | + invalid_arg "capture checksum mismatch"; |
| 136 | + let position = ref 0 in |
| 137 | + let take n = |
| 138 | + if n < 0 || n > limit - !position then invalid_arg "truncated capture"; |
| 139 | + let s = String.sub bytes !position n in |
| 140 | + position := !position + n; |
| 141 | + s |
| 142 | + in |
| 143 | + let u32 () = |
| 144 | + let n = Int64.logand (Int64.of_int32 (String.get_int32_le (take 4) 0)) 0xffffffffL in |
| 145 | + if n > Int64.of_int max_int then invalid_arg "capture integer overflow"; |
| 146 | + Int64.to_int n |
| 147 | + in |
| 148 | + let u64 () = String.get_int64_le (take 8) 0 in |
| 149 | + let string () = let n = u32 () in if n > 4096 then invalid_arg "capture string too long"; take n in |
| 150 | + if take 8 <> "SCOPE\000\001\n" then invalid_arg "unsupported capture format"; |
| 151 | + let domid = u32 () in |
| 152 | + let vcpu = u32 () in |
| 153 | + let phys_bits = u32 () in |
| 154 | + let flags = u32 () in |
| 155 | + if flags land (lnot 3) <> 0 then invalid_arg "unsupported feature flags"; |
| 156 | + let cr0 = u64 () in |
| 157 | + let cr2 = u64 () in |
| 158 | + let cr3 = u64 () in |
| 159 | + let cr4 = u64 () in |
| 160 | + let efer = u64 () in |
| 161 | + let rflags = u64 () in |
| 162 | + let rip = u64 () in |
| 163 | + let root_tag = u32 () in |
| 164 | + let root_value = u64 () in |
| 165 | + let root_override = match root_tag, root_value with |
| 166 | + | 0, 0L -> None | 1, value -> Some value | _ -> invalid_arg "invalid root override record" |
| 167 | + in |
| 168 | + let source = string () in |
| 169 | + let consistency = string () in |
| 170 | + let identity = string () in |
| 171 | + let count = u32 () in |
| 172 | + if count > maximum_pages then invalid_arg "too many capture pages"; |
| 173 | + let frames = List.init count (fun _ -> |
| 174 | + let address = Address.Physical.of_int64 (u64 ()) in |
| 175 | + let page = match u32 () with |
| 176 | + | 0 -> Ok (take 4096) |
| 177 | + | 1 -> Error (string ()) |
| 178 | + | _ -> invalid_arg "invalid table record tag" |
| 179 | + in |
| 180 | + address, page) |
| 181 | + in |
| 182 | + if !position <> limit then invalid_arg "trailing capture data"; |
| 183 | + make { registers = { cr0; cr2; cr3; cr4; efer; rflags; rip }; root_override; |
| 184 | + features = { phys_bits; nx = flags land 1 <> 0; gb_pages = flags land 2 <> 0 }; |
| 185 | + source; consistency; identity; domid; vcpu } frames |
| 186 | + |
| 187 | +let import ~source path = |
| 188 | + let channel = open_in path in |
| 189 | + Fun.protect ~finally:(fun () -> close_in_noerr channel) (fun () -> |
| 190 | + if in_channel_length channel > maximum_bytes * 3 then invalid_arg "import exceeds format size budget"; |
| 191 | + let context = ref None and frames = ref [] and count = ref 0 in |
| 192 | + let number = ref 0 in |
| 193 | + (try while true do |
| 194 | + let line = input_line channel in |
| 195 | + incr number; |
| 196 | + if String.length line > 16384 then invalid_arg "import line exceeds 16384 bytes"; |
| 197 | + let fields = String.split_on_char ' ' line |> List.filter ((<>) "") in |
| 198 | + match fields with |
| 199 | + | [] -> () |
| 200 | + | ["context"; cr0; cr2; cr3; cr4; efer; rflags; rip; phys_bits; nx; gb_pages] -> |
| 201 | + if !context <> None then invalid_arg "duplicate import context"; |
| 202 | + let boolean = function "0" -> false | "1" -> true | _ -> invalid_arg "invalid feature boolean" in |
| 203 | + let open Model in |
| 204 | + context := Some { registers = { cr0 = Address.parse cr0; cr2 = Address.parse cr2; |
| 205 | + cr3 = Address.parse cr3; cr4 = Address.parse cr4; efer = Address.parse efer; |
| 206 | + rflags = Address.parse rflags; rip = Address.parse rip }; |
| 207 | + features = { phys_bits = int_of_string phys_bits; nx = boolean nx; gb_pages = boolean gb_pages }; |
| 208 | + root_override = None; source; consistency = "offline import; producer defines consistency"; |
| 209 | + identity = "import"; domid = 0; vcpu = 0 } |
| 210 | + | ["page"; address; hex] -> |
| 211 | + incr count; |
| 212 | + if !count > maximum_pages then invalid_arg "too many imported pages"; |
| 213 | + if String.length hex <> 8192 then invalid_arg "page hex must contain 8192 digits"; |
| 214 | + let digit = function |
| 215 | + | '0' .. '9' as c -> Char.code c - Char.code '0' |
| 216 | + | 'a' .. 'f' as c -> Char.code c - Char.code 'a' + 10 |
| 217 | + | 'A' .. 'F' as c -> Char.code c - Char.code 'A' + 10 |
| 218 | + | _ -> invalid_arg "invalid page hex" |
| 219 | + in |
| 220 | + let bytes = String.init 4096 (fun i -> Char.chr ((digit hex.[2*i] lsl 4) lor digit hex.[2*i+1])) in |
| 221 | + frames := (Address.Physical.of_int64 (Address.parse address), Ok bytes) :: !frames |
| 222 | + | "missing" :: address :: reason -> |
| 223 | + incr count; |
| 224 | + if !count > maximum_pages then invalid_arg "too many imported pages"; |
| 225 | + frames := (Address.Physical.of_int64 (Address.parse address), Error (String.concat " " reason)) :: !frames |
| 226 | + | _ -> invalid_arg (Printf.sprintf "invalid import record on line %d" !number) |
| 227 | + done with End_of_file -> ()); |
| 228 | + match !context with None -> invalid_arg "missing import context" | Some context -> make context !frames) |