|
| 1 | +let rec random_path ?(tries = 10) fmt = |
| 2 | + if tries <= 0 then failwith "Impossible to generate an available random path"; |
| 3 | + let res = Bytes.create 6 in |
| 4 | + for i = 0 to Bytes.length res - 1 do |
| 5 | + let chr = |
| 6 | + match Random.int (10 + 26 + 26) with |
| 7 | + | n when n < 10 -> Char.chr (Char.code '0' + n) |
| 8 | + | n when n < 10 + 26 -> Char.chr (Char.code 'a' + n - 10) |
| 9 | + | n -> Char.chr (Char.code 'A' + n - 10 - 26) |
| 10 | + in |
| 11 | + Bytes.set res i chr |
| 12 | + done; |
| 13 | + let path = Fmt.str fmt (Bytes.unsafe_to_string res) in |
| 14 | + if Sys.file_exists path then random_path ~tries:(pred tries) fmt else path |
| 15 | + |
| 16 | +let unlink_if_exists path = |
| 17 | + try Unix.unlink path with Unix.Unix_error (Unix.ENOENT, _, _) -> () |
| 18 | + |
| 19 | +let bind_and_listen ?(backlog = 16) () = |
| 20 | + let tmp = random_path "socket-%s.socket" in |
| 21 | + unlink_if_exists tmp; |
| 22 | + let socket = Unix.socket ~cloexec:true Unix.PF_UNIX Unix.SOCK_STREAM 0 in |
| 23 | + let addr = Unix.ADDR_UNIX tmp in |
| 24 | + Unix.bind socket addr; |
| 25 | + Unix.listen socket backlog; |
| 26 | + (Miou_unix.of_file_descr ~non_blocking:true socket, addr, tmp) |
| 27 | + |
| 28 | +module Ca = struct |
| 29 | + open Rresult |
| 30 | + |
| 31 | + let prefix = |
| 32 | + X509.Distinguished_name. |
| 33 | + [ Relative_distinguished_name.singleton (CN "Fuzzer") ] |
| 34 | + |
| 35 | + let cacert_dn = |
| 36 | + X509.Distinguished_name.( |
| 37 | + prefix |
| 38 | + @ [ Relative_distinguished_name.singleton (CN "Ephemeral CA for fuzzer") ]) |
| 39 | + |
| 40 | + let cacert_lifetime = Ptime.Span.v (365, 0L) |
| 41 | + |
| 42 | + let make domain_name seed = |
| 43 | + Domain_name.of_string domain_name >>= Domain_name.host |
| 44 | + >>= fun domain_name -> |
| 45 | + let private_key = |
| 46 | + let seed = Base64.decode_exn ~pad:false seed in |
| 47 | + let g = Mirage_crypto_rng.(create ~seed (module Fortuna)) in |
| 48 | + Mirage_crypto_pk.Rsa.generate ~g ~bits:2048 () |
| 49 | + in |
| 50 | + let valid_from = Ptime.v (Ptime_clock.now_d_ps ()) in |
| 51 | + Ptime.add_span valid_from cacert_lifetime |
| 52 | + |> Option.to_result ~none:(R.msgf "End time out of range") |
| 53 | + >>= fun valid_until -> |
| 54 | + X509.Signing_request.create cacert_dn (`RSA private_key) >>= fun ca_csr -> |
| 55 | + let extensions = |
| 56 | + let open X509.Extension in |
| 57 | + let key_id = |
| 58 | + X509.Public_key.id X509.Signing_request.((info ca_csr).public_key) |
| 59 | + in |
| 60 | + empty |
| 61 | + |> add Subject_alt_name |
| 62 | + ( true, |
| 63 | + X509.General_name.( |
| 64 | + singleton DNS [ Domain_name.to_string domain_name ]) ) |
| 65 | + |> add Basic_constraints (true, (false, None)) |
| 66 | + |> add Key_usage |
| 67 | + (true, [ `Digital_signature; `Content_commitment; `Key_encipherment ]) |
| 68 | + |> add Subject_key_id (false, key_id) |
| 69 | + in |
| 70 | + X509.Signing_request.sign ~valid_from ~valid_until ~extensions |
| 71 | + ca_csr (`RSA private_key) cacert_dn |
| 72 | + |> R.reword_error (R.msgf "%a" X509.Validation.pp_signature_error) |
| 73 | + >>= fun certificate -> |
| 74 | + let fingerprint = X509.Certificate.fingerprint `SHA256 certificate in |
| 75 | + let time () = Some (Ptime_clock.now ()) in |
| 76 | + let authenticator = |
| 77 | + X509.Authenticator.cert_fingerprint ~time ~hash:`SHA256 |
| 78 | + ~fingerprint |
| 79 | + in |
| 80 | + Ok (certificate, `RSA private_key, authenticator) |
| 81 | +end |
| 82 | + |
| 83 | +let fuzz_coop = "fuzz.coop" |
| 84 | +let mutex = Miou.Mutex.create () |
| 85 | +let epr fmt = Miou.Mutex.protect mutex @@ fun () -> Fmt.epr fmt |
| 86 | + |
| 87 | +type operation = |
| 88 | + | Send of string |
| 89 | + | Recv of int |
| 90 | + | Shutdown of [ `read | `write ] |
| 91 | + | Close |
| 92 | + | Noop |
| 93 | + |
| 94 | +module Stop = struct |
| 95 | + type t = { |
| 96 | + mutex : Miou.Mutex.t; |
| 97 | + condition : Miou.Condition.t; |
| 98 | + mutable stop : bool; |
| 99 | + } |
| 100 | + |
| 101 | + let create () = |
| 102 | + let mutex = Miou.Mutex.create () in |
| 103 | + let condition = Miou.Condition.create () in |
| 104 | + { mutex; condition; stop = false } |
| 105 | + |
| 106 | + let stop t = |
| 107 | + Miou.Mutex.protect t.mutex @@ fun () -> |
| 108 | + t.stop <- true; |
| 109 | + Miou.Condition.broadcast t.condition |
| 110 | + |
| 111 | + let wait t = |
| 112 | + Miou.Mutex.protect t.mutex @@ fun () -> |
| 113 | + while t.stop = false do |
| 114 | + Miou.Condition.wait t.condition t.mutex |
| 115 | + done |
| 116 | +end |
| 117 | + |
| 118 | +let inhibit fn = try fn () with _exn -> () |
| 119 | + |
| 120 | +let run ~role:_ actions tls = |
| 121 | + let rec go buf tls = function |
| 122 | + | [] -> Buffer.contents buf |
| 123 | + | Noop :: actions -> |
| 124 | + Miou.yield (); |
| 125 | + go buf tls actions |
| 126 | + | Send str :: actions -> |
| 127 | + Tls_miou_unix.write tls str; |
| 128 | + go buf tls actions |
| 129 | + | Close :: actions -> |
| 130 | + Tls_miou_unix.close tls; |
| 131 | + go buf tls actions |
| 132 | + | Shutdown cmd :: actions -> |
| 133 | + Tls_miou_unix.shutdown tls (cmd :> [ `read | `write | `read_write ]); |
| 134 | + go buf tls actions |
| 135 | + | Recv len :: actions -> |
| 136 | + let tmp = Bytes.make len '\000' in |
| 137 | + Tls_miou_unix.really_read tls tmp; |
| 138 | + Buffer.add_subbytes buf tmp 0 len; |
| 139 | + go buf tls actions |
| 140 | + in |
| 141 | + let buf = Buffer.create 0x100 in |
| 142 | + try go buf tls actions with |
| 143 | + | End_of_file | Tls_miou_unix.Closed_by_peer | Tls_miou_unix.Tls_alert _ |
| 144 | + | Tls_miou_unix.Tls_failure _ -> |
| 145 | + inhibit (fun () -> Miou_unix.close (Tls_miou_unix.file_descr tls)); |
| 146 | + Buffer.contents buf |
| 147 | + | exn -> |
| 148 | + inhibit (fun () -> Miou_unix.close (Tls_miou_unix.file_descr tls)); |
| 149 | + raise exn |
| 150 | + |
| 151 | +let run_client ~to_client:actions cfg addr = |
| 152 | + let domain = Unix.domain_of_sockaddr addr in |
| 153 | + let socket = Unix.socket ~cloexec:true domain Unix.SOCK_STREAM 0 in |
| 154 | + Unix.connect socket addr; |
| 155 | + let fd = Miou_unix.of_file_descr ~non_blocking:true socket in |
| 156 | + let tls = Tls_miou_unix.client_of_fd cfg fd in |
| 157 | + let finally () = |
| 158 | + inhibit (fun () -> Unix.close socket) |
| 159 | + in |
| 160 | + Fun.protect ~finally @@ fun () -> run ~role:"client" actions tls |
| 161 | + |
| 162 | +let rec cleanup orphans clients = |
| 163 | + match Miou.care orphans with |
| 164 | + | None | Some None -> clients |
| 165 | + | Some (Some prm) -> |
| 166 | + let clients = Miou.await prm :: clients in |
| 167 | + cleanup orphans clients |
| 168 | + |
| 169 | +let rec terminate orphans clients = |
| 170 | + match Miou.care orphans with |
| 171 | + | None -> List.rev clients |
| 172 | + | Some None -> |
| 173 | + Miou.yield (); |
| 174 | + terminate orphans clients |
| 175 | + | Some (Some prm) -> |
| 176 | + let clients = Miou.await prm :: clients in |
| 177 | + terminate orphans clients |
| 178 | + |
| 179 | +exception Stop |
| 180 | + |
| 181 | +let run_server ~to_server:actions ~stop fd cfg = |
| 182 | + let rec go orphans clients = |
| 183 | + let clients = cleanup orphans clients in |
| 184 | + let accept = Miou.async @@ fun () -> Miou_unix.accept ~cloexec:true fd in |
| 185 | + let stop = |
| 186 | + Miou.async @@ fun () -> |
| 187 | + Stop.wait stop; |
| 188 | + raise Stop |
| 189 | + in |
| 190 | + match Miou.await_first [ accept; stop ] with |
| 191 | + | Error _ -> |
| 192 | + inhibit (fun () -> Miou_unix.close fd); |
| 193 | + terminate orphans clients |
| 194 | + | Ok (fd, _) -> |
| 195 | + ignore |
| 196 | + ( Miou.async ~orphans @@ fun () -> |
| 197 | + match Tls_miou_unix.server_of_fd cfg fd with |
| 198 | + | tls -> |
| 199 | + let str = run ~role:"server" actions tls in |
| 200 | + inhibit (fun () -> Miou_unix.close fd); str |
| 201 | + | exception _ -> |
| 202 | + Miou_unix.close fd; |
| 203 | + String.empty ); |
| 204 | + go orphans clients |
| 205 | + in |
| 206 | + go (Miou.orphans ()) [] |
| 207 | + |
| 208 | +let compile to_client to_server = |
| 209 | + let close_client close = function |
| 210 | + | Close -> close lor 0b1100 |
| 211 | + | Shutdown `read -> close lor 0b1000 |
| 212 | + | Shutdown `write -> close lor 0b0100 |
| 213 | + | _ -> close |
| 214 | + in |
| 215 | + let close_server close = function |
| 216 | + | Close -> close lor 0b0011 |
| 217 | + | Shutdown `read -> close lor 0b0010 |
| 218 | + | Shutdown `write -> close lor 0b0001 |
| 219 | + | _ -> close |
| 220 | + in |
| 221 | + let client = Buffer.create 0x100 in |
| 222 | + let server = Buffer.create 0x100 in |
| 223 | + let rec go close to_client to_server = |
| 224 | + match (close, to_client, to_server) with |
| 225 | + | _, [], _ | _, _, [] -> () |
| 226 | + | close, ((Shutdown _ | Close) as operation) :: to_client, _ -> |
| 227 | + go (close_client close operation) to_client to_server |
| 228 | + | close, _, ((Shutdown _ | Close) as operation) :: to_server -> |
| 229 | + go (close_server close operation) to_client to_server |
| 230 | + | close, Noop :: to_client, to_server | close, to_client, Noop :: to_server |
| 231 | + -> |
| 232 | + go close to_client to_server |
| 233 | + | close, Send str :: to_client, Recv n :: to_server -> |
| 234 | + assert (String.length str = n); |
| 235 | + if close land 0b0100 = 0 && close land 0b0010 = 0 then |
| 236 | + Buffer.add_string server str; |
| 237 | + if close land 0b0100 = 0 && close land 0b0010 = 0 then |
| 238 | + go close to_client to_server |
| 239 | + | close, Recv n :: to_client, Send str :: to_server -> |
| 240 | + assert (String.length str = n); |
| 241 | + if close land 0b1000 = 0 && close land 0b0001 = 0 then |
| 242 | + Buffer.add_string client str; |
| 243 | + if close land 0b1000 = 0 && close land 0b0001 = 0 then |
| 244 | + go close to_client to_server |
| 245 | + | _, Send _ :: _, Send _ :: _ | _, Recv _ :: _, Recv _ :: _ -> |
| 246 | + assert false (* GADT? *) |
| 247 | + in |
| 248 | + go 0x0 to_client to_server; |
| 249 | + (Buffer.contents client, Buffer.contents server) |
| 250 | + |
| 251 | +let pp_exn ppf exn = Fmt.string ppf (Printexc.to_string exn) |
| 252 | +let pp_str ppf str = Hxd_string.pp Hxd.default ppf str |
| 253 | + |
| 254 | +let run seed operations = |
| 255 | + Miou_unix.run ~domains:1 @@ fun () -> |
| 256 | + let rng = Mirage_crypto_rng_miou_unix.(initialize (module Pfortuna)) in |
| 257 | + let fd, addr, path = bind_and_listen () in |
| 258 | + let finally () = Unix.unlink path in |
| 259 | + Fun.protect ~finally @@ fun () -> |
| 260 | + let cert, pk, authenticator = |
| 261 | + Rresult.R.failwith_error_msg (Ca.make fuzz_coop seed) |
| 262 | + in |
| 263 | + let cfg_server = |
| 264 | + Result.get_ok (Tls.Config.server ~certificates:(`Single ([ cert ], pk)) ()) |
| 265 | + in |
| 266 | + let cfg_client = Result.get_ok (Tls.Config.client ~authenticator ()) in |
| 267 | + let to_client, to_server = List.split operations in |
| 268 | + let stop = Stop.create () in |
| 269 | + let prm0 = Miou.async @@ fun () -> run_server ~to_server ~stop fd cfg_server in |
| 270 | + let prm1 = |
| 271 | + Miou.async @@ fun () -> |
| 272 | + let finally () = Stop.stop stop in |
| 273 | + Fun.protect ~finally @@ fun () -> run_client ~to_client cfg_client addr |
| 274 | + in |
| 275 | + let send_to_client, send_to_server = compile to_client to_server in |
| 276 | + match (Miou.await prm0, Miou.await prm1) with |
| 277 | + | Ok [ Ok send_to_server' ], Ok send_to_client' -> |
| 278 | + Crowbar.check (String.equal send_to_client send_to_client'); |
| 279 | + Crowbar.check (String.equal send_to_server send_to_server'); |
| 280 | + let n = String.length send_to_client in |
| 281 | + let m = String.length send_to_server in |
| 282 | + Mirage_crypto_rng_miou_unix.kill rng; |
| 283 | + epr "[%a] %db %db transmitted\n%!" Fmt.(styled `Green string) "OK" n m |
| 284 | + | a, b -> |
| 285 | + Mirage_crypto_rng_miou_unix.kill rng; |
| 286 | + Crowbar.failf "[%a] Unexpected result: %a & %a\n%!" |
| 287 | + Fmt.(styled `Red string) "ERROR" |
| 288 | + Fmt.(Dump.result ~error:pp_exn ~ok:Fmt.(Dump.list (Dump.result ~error:pp_exn ~ok:pp_str))) a |
| 289 | + Fmt.(Dump.result ~error:pp_exn ~ok:pp_str) b |
| 290 | + |
| 291 | +let label name gen = Crowbar.with_printer Fmt.(const string name) gen |
| 292 | + |
| 293 | +let direction = |
| 294 | + let open Crowbar in |
| 295 | + choose |
| 296 | + [ |
| 297 | + label "server-to-client" (const `To_client); |
| 298 | + label "client-to-server" (const `To_server); |
| 299 | + ] |
| 300 | + |
| 301 | +let shutdown = |
| 302 | + let open Crowbar in |
| 303 | + choose |
| 304 | + [ |
| 305 | + label "close" (const Close); |
| 306 | + label "shutdown-recv" (const (Shutdown `read)); |
| 307 | + label "shutdown-send" (const (Shutdown `write)); |
| 308 | + label "noop" (const Noop); |
| 309 | + ] |
| 310 | + |
| 311 | +let operation = |
| 312 | + let open Crowbar in |
| 313 | + map [ direction; bytes ] @@ fun direction str -> |
| 314 | + match (direction, str) with |
| 315 | + | _, "" -> (Noop, Noop) |
| 316 | + | `To_server, str -> (Send str, Recv (String.length str)) |
| 317 | + | `To_client, str -> (Recv (String.length str), Send str) |
| 318 | + |
| 319 | +let counter = Atomic.make 0 |
| 320 | + |
| 321 | +let operations = |
| 322 | + let open Crowbar in |
| 323 | + fix @@ fun m -> |
| 324 | + let continue (to_client, to_server) = |
| 325 | + if Atomic.fetch_and_add counter 1 >= 4 then const [ (Close, Close) ] |
| 326 | + else map [ m ] @@ fun ops -> (to_client, to_server) :: ops |
| 327 | + in |
| 328 | + map |
| 329 | + [ list1 operation; dynamic_bind (pair shutdown shutdown) continue ] |
| 330 | + List.rev_append |
| 331 | + |
| 332 | +let seed = Crowbar.(map [ bytes ] Base64.encode_exn) |
| 333 | + |
| 334 | +let () = |
| 335 | + Sys.set_signal Sys.sigpipe Sys.Signal_ignore; |
| 336 | + Crowbar.add_test ~name:"run" Crowbar.[ seed; operations ] @@ fun seed operations -> |
| 337 | + run seed operations; |
| 338 | + Atomic.set counter 0 |
0 commit comments