Solo5 hvt ABI and MFT1/ABI1 note format as wire codecs
0

Configure Feed

Select the types of activity you want to include in your feed.

ocaml-solo5 / lib / tender / tender.ml
15 kB 334 lines
1(* The tender: lay out the boot info and manifest, initialise the CPU, then run 2 the guest, decoding each exit and servicing the Solo5 hypercalls. Network and 3 block hypercalls are serviced when their device is attached, and fail hard 4 otherwise. The backend-independent core; a {!Machine.S} backend supplies the 5 VM and the exit boundary, and {!Make} drives it. *) 6 7let i64 = Int64.of_int 8let fail fmt = Fmt.failwith fmt 9 10(* {1 Boot setup} *) 11 12(* The low-memory boot block, laid out exactly as Solo5's hvt_boot_info_init 13 (tenders/hvt/hvt_boot_info.c): the boot info, then the manifest, then the 14 command line, packed contiguously upward from [Solo5_abi.Abi.boot_info_base]. 15 It sits below the kernel load address [Solo5_abi.Abi.guest_min_base], in the 16 read-only region the guest never heaps into -- the guest's heap starts 17 page-rounded above the image's _end (Solo5's bindings/mem.c), far above the 18 kernel. A manifest left at or above the image end is overwritten by the 19 guest's first allocation, detaching its devices before solo5_block_acquire 20 reads them. *) 21type layout = { 22 boot_info : Solo5_abi.Abi.boot_info; 23 (** written at [Solo5_abi.Abi.boot_info_base] *) 24 mft_gpa : int; (** address the manifest is written at *) 25 manifest : string; 26 cmdline_gpa : int; 27 (** address the NUL-terminated command line is written at *) 28 cmdline : string; 29} 30 31let layout ~mem_size ~cpu_cycle_freq ~kernel_end ~manifest ~cmdline = 32 let mft_gpa = Solo5_abi.Abi.boot_info_base + Solo5_abi.Abi.boot_info_size in 33 let cmdline = cmdline ^ "\000" in 34 let cmdline_gpa = mft_gpa + String.length manifest in 35 let boot_info = 36 (* No ring I/O offered: a v0.11+ guest reads host_features = 0 and uses 37 hypercall I/O; the explicit zeros keep that independent of the guest RAM 38 being zero-initialised. *) 39 { 40 Solo5_abi.Abi.mem_size = i64 mem_size; 41 kernel_end = i64 kernel_end; 42 cpu_cycle_freq; 43 cmdline = i64 cmdline_gpa; 44 mft = i64 mft_gpa; 45 host_features = Optint.zero; 46 guest_features = Optint.zero; 47 net_ring = 0L; 48 } 49 in 50 { boot_info; mft_gpa; manifest; cmdline_gpa; cmdline } 51 52(* {1 Hypercall handlers} *) 53 54let now_ns () = int_of_float (Unix.gettimeofday () *. 1e9) 55let handle_walltime mem ~gpa = Hc.set_walltime_nsecs mem ~gpa (now_ns ()) 56 57(* A fixed buffer the guest's puts data is streamed through in bounded chunks. 58 Single-vCPU: one hypercall is serviced at a time, so the shared buffer is 59 race-free. *) 60let puts_scratch = Bytes.create 4096 61 62(* Escape the guest's console bytes when the tender's stdout is an interactive 63 terminal, so an untrusted guest cannot inject terminal control sequences (see 64 {!Console}); redirected output (a CI log, a file, a pipe) is left verbatim, 65 so structured output is not corrupted and that path stays allocation-free. *) 66let console_escapes = lazy (Unix.isatty Unix.stdout) 67 68(* Stream [remaining] bytes of guest memory at [data + off] to stdout in 69 [puts_scratch]-sized chunks. Top-level (not a closure nested in handle_puts) 70 so it captures nothing and a puts allocates none per call: [mem], [data], and 71 [escape] are passed as arguments. *) 72let rec puts_loop mem ~data ~escape off remaining = 73 if remaining > 0 then begin 74 let n = min remaining (Bytes.length puts_scratch) in 75 Guest.read_into mem ~gpa:(data + off) puts_scratch ~off:0 ~len:n; 76 if escape then Console.output stdout puts_scratch ~off:0 ~len:n 77 else output_substring stdout (Bytes.unsafe_to_string puts_scratch) 0 n; 78 puts_loop mem ~data ~escape (off + n) (remaining - n) 79 end 80 81let handle_puts mem ~gpa = 82 let data = Hc.puts_data mem ~gpa in 83 let len = Hc.puts_len mem ~gpa in 84 Guest.check mem ~gpa:data ~len; 85 let escape = Lazy.force console_escapes in 86 puts_loop mem ~data ~escape 0 len; 87 flush stdout 88 89(* Service a POLL: wait up to the deadline for a pollable device to become 90 readable and report which are ready in [ready_set] (bit i = device handle i). 91 Solo5 only polls net devices; [net_handle] is the net device's manifest-entry 92 index, the handle the guest acquired it under -- so the ready bit is set at 93 that handle, not a fixed bit 0. With no device, POLL is the bounded sleep the 94 deadline asks for. *) 95let handle_poll ~poll mem ~net ~net_handle ~gpa = 96 let timeout = Float.of_int (Hc.poll_timeout_nsecs mem ~gpa) /. 1e9 in 97 let ready_set = 98 match (net, net_handle) with 99 | Some dev, Some h -> 100 if Net.wait dev timeout then Int64.shift_left 1L h else 0L 101 | _ -> 102 (* No pollable device: honour the deadline as a bounded sleep through 103 the backend's wait ([poll]), which uses only allowlisted syscalls 104 (epoll_pwait on Linux, select on macOS) -- never a raw [Unix.select] 105 that the kvm sandbox would kill. *) 106 if timeout > 0. then ignore (poll timeout : bool); 107 0L 108 in 109 Hc.set_poll_ready_set mem ~gpa ready_set; 110 Hc.set_poll_ret mem ~gpa 0 111 112let handle_halt mem ~gpa = Hc.halt_exit_status mem ~gpa 113 114(* {1 Hypercall dispatch} *) 115 116(* The outcome of servicing one guest MMIO access: keep running ([Continue]) or 117 halt with a guest exit status ([Halt]). A load's result is delivered through 118 the [complete] callback {!service} is given, not carried in the variant, so 119 the common path returns the constant [Continue] and allocates nothing. *) 120type step = Continue | Halt of int 121 122(* Per-hypercall span for [--probe] captures. A single [nr] field, so passing it 123 allocates nothing on the hot path; emission is a no-op unless a capture is 124 active. The span end carries the body's minor_words delta, so a capture 125 attributes per-hypercall allocation by number. *) 126let hypercall_span = 127 Probe.span "solo5.hypercall" ~doc:"Service one guest hypercall" 128 Probe.Fields.( 129 obj (fun nr -> nr) |> field "nr" int ~enc:(fun nr -> nr) |> seal) 130 131(* Read and validate the device handle a hypercall carries as its first field: a 132 manifest-entry index, so it must fall in [0, MFT_MAX_ENTRIES). {!Guest.u64} 133 saturates a high-bit value to [max_int], which fails [h >= max_entries] here 134 rather than aliasing into a low, valid handle. *) 135let guest_handle mem ~gpa = 136 let h = Guest.u64 mem ~gpa in 137 if h < 0 || h >= Solo5_abi.Manifest.max_entries then 138 fail "guest used device handle 0x%x, outside the manifest" h; 139 h 140 141(* Select the block device bound to [handle], or fail. A direct walk returns the 142 device unwrapped, where [List.assoc_opt] would box a [Some] on every 143 hypercall. *) 144let rec block_by_handle handle = function 145 | [] -> fail "guest used block device handle %d, but none is attached" handle 146 | (h, dev) :: _ when h = handle -> dev 147 | _ :: rest -> block_by_handle handle rest 148 149(* A block hypercall carries the device handle (the manifest-entry index) as its 150 first field; select the bound device by it. *) 151let block_dispatch blocks mem ~gpa handler = 152 let handle = guest_handle mem ~gpa in 153 handler (block_by_handle handle blocks) mem ~gpa; 154 Continue 155 156(* A net hypercall carries the device handle (the manifest-entry index) as its 157 first field, the same handle the guest acquired the net device under. Service 158 it only for the attached net device's handle; any other handle is a guest the 159 tender never granted that device. *) 160let net_dispatch net net_handle mem ~gpa handler = 161 match (net, net_handle) with 162 | Some dev, Some h -> 163 let handle = guest_handle mem ~gpa in 164 if handle = h then begin 165 handler dev mem ~gpa; 166 Continue 167 end 168 else 169 fail "guest used net device handle %d, but the net device is handle %d" 170 handle h 171 | _ -> fail "guest used the network, but no net device is attached" 172 173(* Decode and service one guest hypercall. Network and block hypercalls need 174 their device configured; a guest that uses one with no device attached fails 175 hard. [blocks] maps each block-device handle to its device. *) 176let dispatch_body ~poll mem ~net ~net_handle ~blocks ~nr ~gpa = 177 match Solo5_abi.Abi.hypercall_of_int nr with 178 | Some Solo5_abi.Abi.Halt -> Halt (handle_halt mem ~gpa) 179 | Some Solo5_abi.Abi.Walltime -> 180 handle_walltime mem ~gpa; 181 Continue 182 | Some Solo5_abi.Abi.Puts -> 183 handle_puts mem ~gpa; 184 Continue 185 | Some Solo5_abi.Abi.Poll -> 186 handle_poll ~poll mem ~net ~net_handle ~gpa; 187 Continue 188 | Some Solo5_abi.Abi.Net_write -> 189 net_dispatch net net_handle mem ~gpa Net.handle_write 190 | Some Solo5_abi.Abi.Net_read -> 191 net_dispatch net net_handle mem ~gpa Net.handle_read 192 | Some Solo5_abi.Abi.Block_write -> 193 block_dispatch blocks mem ~gpa Block.handle_write 194 | Some Solo5_abi.Abi.Block_read -> 195 block_dispatch blocks mem ~gpa Block.handle_read 196 | None -> fail "invalid hypercall %d" nr 197 198let dispatch_body_args poll mem net net_handle blocks nr gpa = 199 dispatch_body ~poll mem ~net ~net_handle ~blocks ~nr ~gpa 200 201let dispatch ~poll mem ~net ~net_handle ~blocks ~nr ~gpa = 202 if Probe.active () then 203 Probe.with_span hypercall_span nr (fun () -> 204 dispatch_body_args poll mem net net_handle blocks nr gpa) 205 else dispatch_body_args poll mem net net_handle blocks nr gpa 206 207(* Service a guest MMIO access: a virtio-mmio register access (the guest polls 208 the device through these) or a hypercall store in the MMIO window; anything 209 outside both windows is a guest the tender never mapped. A load's result is 210 handed to [complete ~read] ([~read:(-1)] for a store), rather than carried in 211 the return, so the common outcome is the constant [Continue]. *) 212let service ~complete ~poll mem ~net ~net_handle ~blocks ~virtio ~addr ~write = 213 if Solo5_abi.Abi.in_virtio addr then begin 214 (* A virtio-mmio register access: a store carries its value in [write] (>= 215 0); a load ([write = -1]) is answered by [Virtio_net.read]. The guest 216 polls the device through these accesses, so first deliver any host frames 217 into the receive queue (a no-op until the guest has set that queue up); a 218 read of InterruptStatus then reflects them. *) 219 ignore (Virtio_net.deliver virtio mem : bool); 220 let off = Solo5_abi.Abi.virtio_offset addr in 221 let read = 222 if write >= 0 then ( 223 Virtio_net.write virtio mem off write; 224 -1) 225 else Virtio_net.read virtio off 226 in 227 complete ~read; 228 Continue 229 end 230 else if 231 addr < Solo5_abi.Abi.mmio_base 232 || addr >= Solo5_abi.Abi.hypercall_address Solo5_abi.Abi.hypercall_max 233 then fail "invalid guest MMIO access at 0x%x" addr 234 else 235 let nr = Solo5_abi.Abi.hypercall_nr addr in 236 (* A hypercall is always a store; the argument-struct gpa is the value 237 written (the low 32 bits; a store of the zero register is a null gpa). 238 [write = -1] means a load, which is never a valid hypercall. *) 239 if write < 0 then 240 fail "guest issued hypercall %d via a load at 0x%x" nr addr; 241 match dispatch ~poll mem ~net ~net_handle ~blocks ~nr ~gpa:write with 242 | Halt _ as h -> h 243 | Continue -> 244 complete ~read:(-1); 245 Continue 246 247(* {1 Run loop} *) 248 249module Make (M : Machine.S) = struct 250 module Cpu = Cpu.Make (M) 251 252 let setup mem ~mem_size ~(loaded : Loader.t) ~cmdline ~net ~blocks = 253 (* Attach the tender's devices into the manifest so the guest's 254 solo5_*_acquire succeed, then place the boot block in low memory and 255 initialise the CPU. The block bindings (handle -> device) drive the block 256 hypercall dispatch. *) 257 let attached = Manifest.attach loaded.manifest ~net ~blocks in 258 let l = 259 layout ~mem_size ~cpu_cycle_freq:(M.counter_frequency ()) 260 ~kernel_end:loaded.kernel_end ~manifest:attached.manifest ~cmdline 261 in 262 Guest.write_string mem ~gpa:l.mft_gpa l.manifest; 263 Guest.write_string mem ~gpa:l.cmdline_gpa l.cmdline; 264 Guest.encode mem ~gpa:Solo5_abi.Abi.boot_info_base 265 Solo5_abi.Abi.boot_info_codec l.boot_info; 266 (* W^X: code segments (executable, not writable) are mapped read-only so a 267 guest cannot rewrite its own code; data segments (writable, not 268 executable) are mapped no-execute so it cannot run from its writable 269 memory. *) 270 let range (s : Loader.segment) = (s.addr, s.addr + s.size) in 271 let code_ranges = 272 List.filter_map 273 (fun s -> 274 if s.Loader.executable && not s.writable then Some (range s) else None) 275 loaded.segments 276 in 277 let data_ranges = 278 List.filter_map 279 (fun s -> 280 if s.Loader.writable && not s.executable then Some (range s) else None) 281 loaded.segments 282 in 283 Cpu.init mem ~mem_size ~entry:loaded.entry ~code_ranges ~data_ranges; 284 (attached.net, attached.blocks) 285 286 let run ?(watchdog = 0.) mem ~mem_size ~loaded ~cmdline ~net ~blocks = 287 let net_handle, blocks = 288 setup mem ~mem_size ~loaded ~cmdline ~net ~blocks 289 in 290 (* The virtio-mmio net device backing the side-channel region. With no host 291 net backend it loops back; the guest discovers it via the manifest. *) 292 let virtio = 293 Virtio_net.v ~net:(match net with Some n -> n | None -> Net.loopback ()) 294 in 295 (* Probe the environment for anything lazy that does a one-off syscall, now, 296 before the sandbox: the console's isatty check does an ioctl(TCGETS), 297 which the kvm sandbox's ioctl=KVM_RUN-only filter would otherwise SIGSYS 298 at the first PUTS. Forcing it here caches the result, so the run loop 299 makes only allowlisted syscalls. *) 300 ignore (Lazy.force console_escapes : bool); 301 (* Arm the runaway-guest watchdog before the sandbox: the backend spawns 302 its watchdog thread here, which the sandbox would deny later. *) 303 if watchdog > 0. then M.watchdog watchdog; 304 (* Drop privileges before entering the guest: with the VM and devices 305 already set up, the tender needs no new files, no process spawning, and 306 no host network of its own -- a slirp gateway, when used, runs in a 307 separate process and the tender only reads and writes its socketpair end. 308 The sandbox denies all three, fail-closed. *) 309 M.sandbox (); 310 let rec loop () = 311 match M.run () with 312 | Machine.Vtimer -> 313 M.mask_vtimer true; 314 loop () 315 | Machine.Canceled -> 316 (* A cancel is either the watchdog powering off a guest that made no 317 VM exit within its budget, or a stray interruption to resume. *) 318 if M.watchdog_fired () then 319 fail "guest watchdog: no VM exit within %gs (runaway guest)" 320 watchdog 321 else loop () 322 | Machine.Fatal -> fail "%s" (M.fatal_message ()) 323 | Machine.Mmio -> ( 324 (* [service] delivers a load's result through [M.complete] itself, so 325 the common outcome is the constant [Continue] with no allocation. *) 326 match 327 service ~complete:M.complete ~poll:M.poll_wait mem ~net ~net_handle 328 ~blocks ~virtio ~addr:(M.mmio_addr ()) ~write:(M.mmio_write ()) 329 with 330 | Continue -> loop () 331 | Halt status -> status) 332 in 333 loop () 334end