Protocol Buffers codec for hand-written schemas
0
fork

Configure Feed

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

protobuf: rename Wire.wire_type to Wire.t; inline hostile tests

Two merlint-driven structural cleanups:

- [Wire.wire_type] -> [Wire.t], [Wire.wire_type_to_int] -> [Wire.to_int],
[Wire.wire_type_of_int] -> [Wire.of_int], [Wire.pp_wire_type] ->
[Wire.pp]. Merlint's E330 rule flags the redundant module prefix;
callers using [Wire.Fixed32]/etc. already disambiguate the sort by
module path, so the type can be the idiomatic [Wire.t]. Labeled
argument [~wire_type:] on [write_tag] / [read_tag] stays.

- Merge [test_hostile.ml] into [test_protobuf.ml] as a
[hostile_cases : unit Alcotest.test_case list] appended onto the
main suite. Matches the user's established convention -- hostile
inputs are tested alongside the happy-path cases, not in a separate
test_<nonexistent-library>.ml that merlint E610 objects to.

All 49 unit + 17 fuzz + 2 protoc interop tests pass.

Remaining merlint items: long identifier names (CVE-numbered tests
with > 4 underscores), a [Fmt] usage hint, and a small set of doc
tags. Low-signal nits; deferred.

+397 -444
+21 -37
lib/protobuf.ml
··· 134 134 | Message _ -> Fmt.string ppf "message" 135 135 | Rec _ -> Fmt.string ppf "rec message" 136 136 137 - let wire_type_of : type a. a t -> Wire.wire_type = function 137 + let wire_type_of : type a. a t -> Wire.t = function 138 138 | Varint _ -> Wire.Varint 139 139 | Fixed32 _ -> Wire.Fixed32 140 140 | Fixed64 _ -> Wire.Fixed64 ··· 170 170 let type_error ~sort expected got = 171 171 raise 172 172 (Wire.Decode_error 173 - (Fmt.str "%a: expected wire type %a, got %a" Sort.pp sort 174 - Wire.pp_wire_type expected Wire.pp_wire_type (wire_value_type got))) 173 + (Fmt.str "%a: expected wire type %a, got %a" Sort.pp sort Wire.pp 174 + expected Wire.pp (wire_value_type got))) 175 175 176 176 let varint_of ~sort = function 177 177 | WV_varint v -> v ··· 242 242 243 243 let int32 : int32 t = 244 244 Varint 245 - { 246 - sort = Int32; 247 - dec = Int64.to_int32; 248 - enc = Int64.of_int32; 249 - default = 0l; 250 - } 245 + { sort = Int32; dec = Int64.to_int32; enc = Int64.of_int32; default = 0l } 251 246 252 247 let int64 : int64 t = 253 248 Varint { sort = Int64; dec = (fun x -> x); enc = (fun x -> x); default = 0L } ··· 262 257 } 263 258 264 259 let uint64 : int64 t = 265 - Varint 266 - { sort = Uint64; dec = (fun x -> x); enc = (fun x -> x); default = 0L } 260 + Varint { sort = Uint64; dec = (fun x -> x); enc = (fun x -> x); default = 0L } 267 261 268 262 let sint32 : int32 t = 269 263 Varint ··· 400 394 let packed tag get codec = 401 395 Repeated { tag; get; codec; packed = true; cont = (fun x -> Return x) } 402 396 403 - let rec ( let* ) : 404 - type o a b. (o, a) field -> (a -> (o, b) field) -> (o, b) field = 397 + let rec ( let* ) : type o a b. 398 + (o, a) field -> (a -> (o, b) field) -> (o, b) field = 405 399 fun m f -> 406 400 match m with 407 401 | Return a -> f a ··· 523 517 if !pos <> end_ then 524 518 raise 525 519 (Wire.Decode_error 526 - (Fmt.str "overran message boundary: at %d, expected end %d" !pos 527 - end_)); 520 + (Fmt.str "overran message boundary: at %d, expected end %d" !pos end_)); 528 521 table 529 522 530 523 (* [take_*] consume the matched entries out of the table so that after ··· 574 567 tags; 575 568 Buffer.contents buf 576 569 577 - let decode_packed_or_repeated : type a. 578 - a t -> wire_value list -> a list = 570 + let decode_packed_or_repeated : type a. a t -> wire_value list -> a list = 579 571 fun codec values -> 580 572 (* For a repeated field, each element in [values] can be either a 581 573 scalar wire value (non-packed) or a length-delimited blob ··· 633 625 let* / return chain. *) 634 626 let encode_body buf (k, v) = 635 627 if k <> default_of key_codec then begin 636 - Wire.write_tag buf ~field_number:1 637 - ~wire_type:(wire_type_of key_codec); 628 + Wire.write_tag buf ~field_number:1 ~wire_type:(wire_type_of key_codec); 638 629 write_value buf key_codec k 639 630 end; 640 631 if v <> default_of value_codec then begin 641 - Wire.write_tag buf ~field_number:2 642 - ~wire_type:(wire_type_of value_codec); 632 + Wire.write_tag buf ~field_number:2 ~wire_type:(wire_type_of value_codec); 643 633 write_value buf value_codec v 644 634 end 645 635 in ··· 663 653 (decode_body s start end_, "") 664 654 in 665 655 let msg_default = (default_of key_codec, default_of value_codec) in 666 - Message 667 - { encode_body; decode_body; decode_body_with_unknowns; msg_default } 656 + Message { encode_body; decode_body; decode_body_with_unknowns; msg_default } 668 657 669 658 let map tag get key_codec value_codec = 670 659 Repeated ··· 691 680 (* A message with no fields populated: all scalars take their 692 681 default, repeated fields are empty, optionals are [None]. *) 693 682 let msg_default = decode_body "" 0 0 in 694 - Message 695 - { encode_body; decode_body; decode_body_with_unknowns; msg_default } 683 + Message { encode_body; decode_body; decode_body_with_unknowns; msg_default } 696 684 end 697 685 698 686 (* -- Top-level encode / decode. ··· 726 714 let v, off = decode_bytes codec s 0 in 727 715 if off <> String.length s then 728 716 Error 729 - (Fmt.str "trailing %d bytes after scalar" 730 - (String.length s - off)) 717 + (Fmt.str "trailing %d bytes after scalar" (String.length s - off)) 731 718 else Ok v 732 719 | Rec c -> ( 733 720 match Lazy.force c with ··· 768 755 - Only works for Message codecs. Calling on a bare scalar returns 769 756 [Error]. *) 770 757 771 - let decode_with_unknowns_string : 772 - type a. a t -> string -> (a * string, string) result = 758 + let decode_with_unknowns_string : type a. 759 + a t -> string -> (a * string, string) result = 773 760 fun codec s -> 774 761 depth := 0; 775 762 try ··· 778 765 | Rec c -> ( 779 766 match Lazy.force c with 780 767 | Message m -> Ok (m.decode_body_with_unknowns s 0 (String.length s)) 781 - | _ -> 782 - Error "decode_with_unknowns_string: codec is not a message") 768 + | _ -> Error "decode_with_unknowns_string: codec is not a message") 783 769 | _ -> Error "decode_with_unknowns_string: codec is not a message" 784 770 with Wire.Decode_error msg -> Error msg 785 771 786 - let encode_with_unknowns_string : type a. a t -> unknowns:string -> a -> string = 772 + let encode_with_unknowns_string : type a. a t -> unknowns:string -> a -> string 773 + = 787 774 fun codec ~unknowns v -> 788 775 let buf = Buffer.create 64 in 789 776 (match codec with ··· 795 782 | Message m -> 796 783 m.encode_body buf v; 797 784 Buffer.add_string buf unknowns 798 - | _ -> 799 - invalid_arg 800 - "encode_with_unknowns_string: codec is not a message") 801 - | _ -> 802 - invalid_arg "encode_with_unknowns_string: codec is not a message"); 785 + | _ -> invalid_arg "encode_with_unknowns_string: codec is not a message") 786 + | _ -> invalid_arg "encode_with_unknowns_string: codec is not a message"); 803 787 Buffer.contents buf
+26 -31
lib/protobuf.mli
··· 31 31 (** A codec for protobuf messages or scalar values of type ['a]. *) 32 32 33 33 val pp : Format.formatter -> 'a t -> unit 34 - (** [pp ppf c] prints a short human-readable sort tag for the codec 35 - (e.g. ["int32"], ["fixed64"], ["message"]). Intended for debugging; 36 - not a round-trippable representation of the codec structure. *) 34 + (** [pp ppf c] prints a short human-readable sort tag for the codec (e.g. 35 + ["int32"], ["fixed64"], ["message"]). Intended for debugging; not a 36 + round-trippable representation of the codec structure. *) 37 37 38 38 (** {1 Scalar codecs} 39 39 ··· 136 136 (required by the protobuf spec for compatibility). *) 137 137 138 138 val map : 139 - int -> ('o -> ('k * 'v) list) -> 'k t -> 'v t -> 140 - ('o, ('k * 'v) list) field 141 - (** [map tag get key_codec value_codec] declares a [map<K, V>] field. On 142 - the wire this is sugar for a repeated nested message with two fields: 143 - [key] at tag 1 (encoded by [key_codec]) and [value] at tag 2 (encoded 144 - by [value_codec]). Proto3 map ordering is unspecified on the wire; the 139 + int -> ('o -> ('k * 'v) list) -> 'k t -> 'v t -> ('o, ('k * 'v) list) field 140 + (** [map tag get key_codec value_codec] declares a [map<K, V>] field. On the 141 + wire this is sugar for a repeated nested message with two fields: [key] at 142 + tag 1 (encoded by [key_codec]) and [value] at tag 2 (encoded by 143 + [value_codec]). Proto3 map ordering is unspecified on the wire; the 145 144 decoder preserves wire order. 146 145 147 - Protobuf restricts map keys to the integer/bool/string scalars; this 148 - API does not enforce that — use a valid key codec. *) 146 + Protobuf restricts map keys to the integer/bool/string scalars; this API 147 + does not enforce that — use a valid key codec. *) 149 148 end 150 149 151 150 (** {1 Recursive codecs} *) 152 151 153 152 val fix : default:'a -> ('a t -> 'a t) -> 'a t 154 - (** [fix ~default f] builds a self-referential codec. [f] is invoked 155 - once with a forwarding placeholder; any occurrences of the 156 - placeholder inside [f]'s body resolve to the final codec at 157 - decode/encode time. [~default] is the value used when a 158 - [required] field declared with this codec is absent from the 153 + (** [fix ~default f] builds a self-referential codec. [f] is invoked once with a 154 + forwarding placeholder; any occurrences of the placeholder inside [f]'s body 155 + resolve to the final codec at decode/encode time. [~default] is the value 156 + used when a [required] field declared with this codec is absent from the 159 157 wire (typically the "empty" record for a tree-shaped type). *) 160 158 161 159 (** {1 Encode / Decode} *) ··· 177 175 178 176 (** {1 Unknown field preservation} 179 177 180 - Standard decoders drop fields whose tag is not in the schema. 181 - These variants preserve them so a decoded-then-re-encoded message 182 - round-trips even when intermediate tooling runs an older schema. *) 178 + Standard decoders drop fields whose tag is not in the schema. These variants 179 + preserve them so a decoded-then-re-encoded message round-trips even when 180 + intermediate tooling runs an older schema. *) 183 181 184 - val decode_with_unknowns_string : 185 - 'a t -> string -> ('a * string, string) result 186 - (** [decode_with_unknowns_string c s] returns [Ok (value, unknown_wire)] 187 - where [unknown_wire] is a byte string holding the wire bytes of every 188 - tag that was not in the schema, re-serialized in canonical form and 189 - sorted by tag. Pair with {!encode_with_unknowns_string} on the way 190 - back out. 182 + val decode_with_unknowns_string : 'a t -> string -> ('a * string, string) result 183 + (** [decode_with_unknowns_string c s] returns [Ok (value, unknown_wire)] where 184 + [unknown_wire] is a byte string holding the wire bytes of every tag that was 185 + not in the schema, re-serialized in canonical form and sorted by tag. Pair 186 + with {!encode_with_unknowns_string} on the way back out. 191 187 192 188 Returns [Error _] if [c] is not a message codec. *) 193 189 194 - val encode_with_unknowns_string : 195 - 'a t -> unknowns:string -> 'a -> string 196 - (** [encode_with_unknowns_string c ~unknowns v] encodes [v] as a message 197 - body, then appends [unknowns] verbatim. [unknowns] is typically the 198 - byte string returned by an earlier {!decode_with_unknowns_string}. 190 + val encode_with_unknowns_string : 'a t -> unknowns:string -> 'a -> string 191 + (** [encode_with_unknowns_string c ~unknowns v] encodes [v] as a message body, 192 + then appends [unknowns] verbatim. [unknowns] is typically the byte string 193 + returned by an earlier {!decode_with_unknowns_string}. 199 194 200 195 Raises [Invalid_argument] if [c] is not a message codec. *)
+6 -6
lib/wire.ml
··· 1 - type wire_type = Varint | Fixed64 | Length_delimited | Fixed32 1 + type t = Varint | Fixed64 | Length_delimited | Fixed32 2 2 3 - let pp_wire_type ppf = function 3 + let pp ppf = function 4 4 | Varint -> Fmt.string ppf "varint" 5 5 | Fixed64 -> Fmt.string ppf "fixed64" 6 6 | Length_delimited -> Fmt.string ppf "length-delimited" 7 7 | Fixed32 -> Fmt.string ppf "fixed32" 8 8 9 - let wire_type_to_int = function 9 + let to_int = function 10 10 | Varint -> 0 11 11 | Fixed64 -> 1 12 12 | Length_delimited -> 2 13 13 | Fixed32 -> 5 14 14 15 - let wire_type_of_int = function 15 + let of_int = function 16 16 | 0 -> Some Varint 17 17 | 1 -> Some Fixed64 18 18 | 2 -> Some Length_delimited ··· 26 26 (* -- Tags -- *) 27 27 28 28 let write_tag buf ~field_number ~wire_type = 29 - let tag = (field_number lsl 3) lor wire_type_to_int wire_type in 29 + let tag = (field_number lsl 3) lor to_int wire_type in 30 30 Leb128.add_u63_to_buffer buf tag 31 31 32 32 let read_tag s off = ··· 39 39 let field_number = tag lsr 3 in 40 40 let wt = tag land 0x7 in 41 41 if field_number = 0 then decode_error "tag: field number 0 is reserved"; 42 - match wire_type_of_int wt with 42 + match of_int wt with 43 43 | Some w -> (field_number, w, off') 44 44 | None -> decode_error "tag: unsupported wire type %d" wt 45 45
+10 -9
lib/wire.mli
··· 10 10 11 11 (** The four wire types of Protocol Buffers (wire types 3/4 for groups are 12 12 deprecated and not supported). *) 13 - type wire_type = 13 + type t = 14 14 | Varint 15 15 (** Wire type 0: int32, int64, uint32, uint64, sint32, sint64, bool, enum. 16 16 Length-prefixed by continuation bits. *) ··· 20 20 Varint length + bytes. *) 21 21 | Fixed32 (** Wire type 5: fixed32, sfixed32, float. 4 bytes LE. *) 22 22 23 - val pp_wire_type : Format.formatter -> wire_type -> unit 24 - val wire_type_to_int : wire_type -> int 23 + val pp : Format.formatter -> t -> unit 24 + val to_int : t -> int 25 25 26 - val wire_type_of_int : int -> wire_type option 27 - (** [wire_type_of_int n] returns [Some w] when [n] is 0, 1, 2, or 5; [None] 28 - otherwise. *) 26 + val of_int : int -> t option 27 + (** [of_int n] returns [Some w] when [n] is 0, 1, 2, or 5; [None] otherwise. *) 29 28 30 29 (** {1 Decode errors} 31 30 ··· 37 36 38 37 A tag is a varint encoding [(field_number lsl 3) lor wire_type]. *) 39 38 40 - val write_tag : Buffer.t -> field_number:int -> wire_type:wire_type -> unit 39 + val write_tag : Buffer.t -> field_number:int -> wire_type:t -> unit 40 + (** [write_tag buf ~field_number ~wire_type] writes the tag varint 41 + [(field_number lsl 3) lor wire_type]. *) 41 42 42 - val read_tag : string -> int -> int * wire_type * int 43 + val read_tag : string -> int -> int * t * int 43 44 (** [read_tag s off] returns [(field_number, wire_type, new_offset)]. Raises 44 45 {!Decode_error} if the wire-type bits are not 0/1/2/5 or if the field number 45 46 is 0. *) ··· 122 123 123 124 (** {1 Skipping} *) 124 125 125 - val skip_field : string -> int -> wire_type -> int 126 + val skip_field : string -> int -> t -> int 126 127 (** [skip_field s off w] returns the offset past an unknown field of wire type 127 128 [w] starting at [off]. Used by the message decoder to discard fields whose 128 129 tag is not in the schema. *)
+1 -3
test/test.ml
··· 1 - let () = 2 - Alcotest.run "protobuf" 3 - [ Test_wire.suite; Test_protobuf.suite; Test_hostile.suite ] 1 + let () = Alcotest.run "protobuf" [ Test_wire.suite; Test_protobuf.suite ]
-350
test/test_hostile.ml
··· 1 - (** Hostile-input tests covering known protobuf decoder CVE classes. 2 - 3 - Each test cites the upstream CVE (where one exists) or the generic 4 - vulnerability class it guards against. Tests are short and 5 - targeted — they cover the boundary conditions where real 6 - decoders have historically failed, not the full fuzzing 7 - distribution (which lives in the [fuzz/] sibling). 8 - 9 - References drawn from the NIST CVE database and the protobuf 10 - security advisory archive on GitHub. *) 11 - 12 - (* -- Simple message schema used throughout. -- *) 13 - 14 - type test1 = { a : int32 } 15 - 16 - let test1_codec : test1 Protobuf.t = 17 - let open Protobuf.Message in 18 - finish 19 - (let* a = required 1 (fun r -> r.a) Protobuf.int32 in 20 - return { a }) 21 - 22 - (* A schema that declares no fields: every input is unknown. *) 23 - 24 - type empty_msg = unit 25 - 26 - let empty_codec : empty_msg Protobuf.t = 27 - let open Protobuf.Message in 28 - finish (return ()) 29 - 30 - (* ================================================================= 31 - CVE-2015-5237 (protobuf-c++, 2015): integer overflow in varint parser 32 - when a maliciously large length prefix is claimed. 33 - ================================================================= *) 34 - 35 - let test_cve_2015_5237_huge_length () = 36 - (* Tag 14 (str, length-delim), length 0xFFFFFFF (268 MiB), but only 37 - two payload bytes follow. A naive decoder allocates 268 MiB before 38 - realising the buffer is too short. *) 39 - let bad = 40 - "\x72" (* tag 14, wire type 2 *) ^ "\xff\xff\xff\x7f" 41 - (* varint 0x0FFFFFFF = 268435455 *) ^ "ab" 42 - in 43 - match Protobuf.decode_string test1_codec bad with 44 - | Error _ -> () 45 - | Ok _ -> Alcotest.fail "CVE-2015-5237: huge length prefix must be rejected" 46 - 47 - let test_cve_2015_5237_overlong_varint () = 48 - (* Varint with 11 continuation bytes. 64-bit values fit in 10. *) 49 - let bad = "\x08" ^ "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01" in 50 - match Protobuf.decode_string test1_codec bad with 51 - | Error _ -> () 52 - | Ok _ -> Alcotest.fail "CVE-2015-5237: over-long varint must be rejected" 53 - 54 - let test_cve_2015_5237_truncated_tag () = 55 - match Protobuf.decode_string test1_codec "\x80\x80\x80" with 56 - | Error _ -> () 57 - | Ok _ -> Alcotest.fail "CVE-2015-5237: truncated tag must be rejected" 58 - 59 - (* ================================================================= 60 - CVE-2021-22569 (protobuf-java, 2021): malicious input with many 61 - small groups causes unbounded `ByteString` allocations, amplifying 62 - memory pressure. 63 - ================================================================= *) 64 - 65 - let test_cve_2021_22569_many_small_groups () = 66 - (* Many repeated small fields should decode in linear memory, not 67 - amplified. 10k unknown fields at unique tags. *) 68 - let buf = Buffer.create (6 * 10_000) in 69 - for tag = 100 to 10_099 do 70 - Protobuf.Wire.write_tag buf ~field_number:tag ~wire_type:Protobuf.Wire.Varint; 71 - Protobuf.Wire.write_int32 buf 1l 72 - done; 73 - let wire = Buffer.contents buf in 74 - match Protobuf.decode_string empty_codec wire with 75 - | Ok () -> () 76 - | Error msg -> Alcotest.failf "10k unknown fields rejected: %s" msg 77 - 78 - (* ================================================================= 79 - CVE-2022-1941 (protobuf-c++, 2022): null-pointer dereference when 80 - parsing an all-unknown message through a schema with no declared 81 - fields. 82 - ================================================================= *) 83 - 84 - let test_cve_2022_1941_all_unknown () = 85 - (* Emit a full [Test1] message; decode through [empty_codec] whose 86 - schema has no fields. Every field is unknown. Decoder must not 87 - crash. *) 88 - let wire = Protobuf.encode_string test1_codec { a = 42l } in 89 - match Protobuf.decode_string empty_codec wire with 90 - | Ok () -> () 91 - | Error msg -> Alcotest.failf "empty schema, all unknowns: %s" msg 92 - 93 - (* ================================================================= 94 - CVE-2022-3171 (protobuf-java, 2022): repeated group wire type 95 - (deprecated 3/4) triggers long GC pauses. 96 - ================================================================= *) 97 - 98 - let test_cve_2022_3171_group_wire_type () = 99 - (* Wire type 3 is the deprecated `SGROUP` form. Modern protobuf 100 - rejects it. *) 101 - match Protobuf.decode_string test1_codec "\x0b\x00" with 102 - | Error _ -> () 103 - | Ok _ -> Alcotest.fail "CVE-2022-3171: wire type 3 must be rejected" 104 - 105 - let test_cve_2022_3171_group_wire_type_4 () = 106 - (* Wire type 4 is the deprecated `EGROUP` form. Must be rejected at 107 - tag-parse time regardless of field number. *) 108 - match Protobuf.decode_string test1_codec "\x0c" with 109 - | Error _ -> () 110 - | Ok _ -> Alcotest.fail "CVE-2022-3171: wire type 4 must be rejected" 111 - 112 - (* ================================================================= 113 - CVE-2024-7254 (protobuf-go, 2024): deeply nested unknown groups 114 - cause stack overflow in the parser. 115 - ================================================================= *) 116 - 117 - let test_cve_2024_7254_deep_nesting_known () = 118 - (* 200 levels of declared nested messages exceeds the 100-level 119 - [max_depth] bound baked into the decoder. *) 120 - let nest_codec : unit Protobuf.t = 121 - Protobuf.fix ~default:() (fun self -> 122 - let open Protobuf.Message in 123 - finish 124 - (let* () = 125 - (* [optional 1] with payload type that is [self] (a message 126 - codec). When absent, yields [None]; we collapse to unit 127 - to keep the test type trivial. *) 128 - optional 1 (fun () -> None) self 129 - |> fun f -> 130 - let* _v = f in 131 - return () 132 - in 133 - return ())) 134 - in 135 - let rec build_wire n = 136 - if n = 0 then "" 137 - else 138 - let inner = build_wire (n - 1) in 139 - "\x0a" ^ Leb128.encode_u63_string (String.length inner) ^ inner 140 - in 141 - match Protobuf.decode_string nest_codec (build_wire 200) with 142 - | Error _ -> () 143 - | Ok () -> Alcotest.fail "CVE-2024-7254: 200-level nesting must be rejected" 144 - 145 - let test_cve_2024_7254_deep_nesting_unknown () = 146 - (* Deeply nested length-delim fields that are UNKNOWN to the schema 147 - are skipped at the outer level via [Wire.skip_field], which 148 - advances past the body without recursing. No depth bound needed; 149 - the cost is O(N) in wire length. *) 150 - let rec build_wire n = 151 - if n = 0 then "" 152 - else 153 - let inner = build_wire (n - 1) in 154 - "\x7a" (* tag 15, wire type 2 — unknown to [test1_codec] *) 155 - ^ Leb128.encode_u63_string (String.length inner) 156 - ^ inner 157 - in 158 - let wire = build_wire 500 in 159 - (* Must decode without stack overflow. *) 160 - match Protobuf.decode_string test1_codec wire with 161 - | Ok _ | Error _ -> () 162 - 163 - (* ================================================================= 164 - CVE-2024-47554 (rust prost, 2024): length-prefix overflow / DoS 165 - through crafted length fields. 166 - ================================================================= *) 167 - 168 - let test_cve_2024_47554_length_past_end () = 169 - (* Tag 14 (str), length 100, but only 2 bytes follow. *) 170 - let bad = "\x72\x64ab" in 171 - match Protobuf.decode_string test1_codec bad with 172 - | Error _ -> () 173 - | Ok _ -> Alcotest.fail "CVE-2024-47554: length past end must be rejected" 174 - 175 - let test_cve_2024_47554_packed_corrupt_body () = 176 - let packed_codec = 177 - let open Protobuf.Message in 178 - finish 179 - (let* nums = 180 - Protobuf.Message.packed 1 181 - (fun (nums : int32 list) -> nums) 182 - Protobuf.int32 183 - in 184 - return nums) 185 - in 186 - (* Tag 1, wire 2, length 2, then \x80\x80 (unterminated varint inside 187 - the packed blob). *) 188 - let bad = "\x0a\x02\x80\x80" in 189 - match Protobuf.decode_string packed_codec bad with 190 - | Error _ -> () 191 - | Ok _ -> Alcotest.fail "CVE-2024-47554: corrupt packed body must be rejected" 192 - 193 - (* ================================================================= 194 - Generic vulnerability class: reserved tag 0 195 - (protobuf spec §3 forbids field number 0). 196 - ================================================================= *) 197 - 198 - let test_reserved_tag_zero () = 199 - match Protobuf.decode_string test1_codec "\x00" with 200 - | Error _ -> () 201 - | Ok _ -> Alcotest.fail "tag field=0 must be rejected" 202 - 203 - (* ================================================================= 204 - Wire type mismatch: the schema declares varint for tag 1, the wire 205 - carries length-delim. Decoder must reject instead of silently 206 - coercing. 207 - ================================================================= *) 208 - 209 - let test_wire_type_mismatch () = 210 - let bad = "\x0a\x00" (* tag 1 wire type 2, length 0 *) in 211 - match Protobuf.decode_string test1_codec bad with 212 - | Error _ -> () 213 - | Ok _ -> Alcotest.fail "wire type mismatch must be rejected" 214 - 215 - (* ================================================================= 216 - Empty input: proto3 defaults must be returned for every field. 217 - ================================================================= *) 218 - 219 - let test_empty_input () = 220 - match Protobuf.decode_string test1_codec "" with 221 - | Error msg -> Alcotest.failf "empty input should use defaults: %s" msg 222 - | Ok r -> Alcotest.(check int32) "a defaults to 0" 0l r.a 223 - 224 - (* ================================================================= 225 - Trailing unused bytes inside the message body: parse_wire's 226 - boundary check rejects. 227 - ================================================================= *) 228 - 229 - let test_overrun_rejected () = 230 - let good_prefix = Protobuf.encode_string test1_codec { a = 1l } in 231 - let with_trailer = good_prefix ^ "\x80" (* truncated continuation *) in 232 - match Protobuf.decode_string test1_codec with_trailer with 233 - | Error _ -> () 234 - | Ok _ -> Alcotest.fail "trailing truncated varint must be rejected" 235 - 236 - (* ================================================================= 237 - Malformed UTF-8 in a string field: protobuf spec §5 says strings 238 - and bytes share the wire representation, and decoders must accept 239 - non-UTF-8 byte content. No validation must be performed. 240 - ================================================================= *) 241 - 242 - type with_str = { s : string } 243 - 244 - let with_str_codec : with_str Protobuf.t = 245 - let open Protobuf.Message in 246 - finish 247 - (let* s = required 1 (fun r -> r.s) Protobuf.string in 248 - return { s }) 249 - 250 - let test_non_utf8_string_accepted () = 251 - let raw = "\xff\xfe\xfd" in 252 - (* Not valid UTF-8 *) 253 - let wire = Protobuf.encode_string with_str_codec { s = raw } in 254 - match Protobuf.decode_string with_str_codec wire with 255 - | Error msg -> Alcotest.failf "non-UTF-8 string must decode: %s" msg 256 - | Ok r -> 257 - Alcotest.(check string) "roundtrip" raw r.s 258 - 259 - (* ================================================================= 260 - Map with duplicate key: protobuf spec says last-wins. A malicious 261 - sender can pad the wire with many duplicate keys; decoder must 262 - handle this in linear memory. 263 - ================================================================= *) 264 - 265 - type dict = { entries : (string * int32) list } 266 - 267 - let dict_codec : dict Protobuf.t = 268 - let open Protobuf.Message in 269 - finish 270 - (let* entries = 271 - Protobuf.Message.map 1 (fun r -> r.entries) Protobuf.string Protobuf.int32 272 - in 273 - return { entries }) 274 - 275 - let test_map_duplicate_keys_accepted () = 276 - (* Encode three entries, two with the same key. Decoder preserves 277 - wire order in the output list; semantic last-wins is the user's 278 - responsibility. *) 279 - let v = 280 - { entries = [ ("k", 1l); ("k", 2l); ("x", 99l); ("k", 3l) ] } 281 - in 282 - let wire = Protobuf.encode_string dict_codec v in 283 - match Protobuf.decode_string dict_codec wire with 284 - | Error msg -> Alcotest.fail msg 285 - | Ok r -> 286 - Alcotest.(check int) "entry count preserved" 4 (List.length r.entries) 287 - 288 - (* ================================================================= 289 - Many repeated fields: 10 000 tags at the same field, each small. 290 - Must not scale super-linearly. 291 - ================================================================= *) 292 - 293 - type rep = { tags : string list } 294 - 295 - let rep_codec : rep Protobuf.t = 296 - let open Protobuf.Message in 297 - finish 298 - (let* tags = repeated 1 (fun r -> r.tags) Protobuf.string in 299 - return { tags }) 300 - 301 - let test_many_repeated () = 302 - let n = 10_000 in 303 - let buf = Buffer.create (3 * n) in 304 - for _ = 1 to n do 305 - Protobuf.Wire.write_tag buf ~field_number:1 306 - ~wire_type:Protobuf.Wire.Length_delimited; 307 - Protobuf.Wire.write_string buf "x" 308 - done; 309 - let wire = Buffer.contents buf in 310 - match Protobuf.decode_string rep_codec wire with 311 - | Error msg -> Alcotest.failf "many-repeated rejected: %s" msg 312 - | Ok r -> 313 - Alcotest.(check int) "count" n (List.length r.tags); 314 - Alcotest.(check string) "first" "x" (List.hd r.tags) 315 - 316 - let suite = 317 - ( "hostile", 318 - [ 319 - Alcotest.test_case "CVE-2015-5237 huge length prefix" `Quick 320 - test_cve_2015_5237_huge_length; 321 - Alcotest.test_case "CVE-2015-5237 over-long varint" `Quick 322 - test_cve_2015_5237_overlong_varint; 323 - Alcotest.test_case "CVE-2015-5237 truncated tag" `Quick 324 - test_cve_2015_5237_truncated_tag; 325 - Alcotest.test_case "CVE-2021-22569 many small groups" `Quick 326 - test_cve_2021_22569_many_small_groups; 327 - Alcotest.test_case "CVE-2022-1941 all-unknown schema" `Quick 328 - test_cve_2022_1941_all_unknown; 329 - Alcotest.test_case "CVE-2022-3171 group wire type 3" `Quick 330 - test_cve_2022_3171_group_wire_type; 331 - Alcotest.test_case "CVE-2022-3171 group wire type 4" `Quick 332 - test_cve_2022_3171_group_wire_type_4; 333 - Alcotest.test_case "CVE-2024-7254 deep known nesting" `Quick 334 - test_cve_2024_7254_deep_nesting_known; 335 - Alcotest.test_case "CVE-2024-7254 deep unknown nesting" `Quick 336 - test_cve_2024_7254_deep_nesting_unknown; 337 - Alcotest.test_case "CVE-2024-47554 length past end" `Quick 338 - test_cve_2024_47554_length_past_end; 339 - Alcotest.test_case "CVE-2024-47554 packed corrupt body" `Quick 340 - test_cve_2024_47554_packed_corrupt_body; 341 - Alcotest.test_case "reserved tag 0" `Quick test_reserved_tag_zero; 342 - Alcotest.test_case "wire type mismatch" `Quick test_wire_type_mismatch; 343 - Alcotest.test_case "empty input -> defaults" `Quick test_empty_input; 344 - Alcotest.test_case "overrun rejected" `Quick test_overrun_rejected; 345 - Alcotest.test_case "non-UTF-8 string accepted" `Quick 346 - test_non_utf8_string_accepted; 347 - Alcotest.test_case "map duplicate keys accepted" `Quick 348 - test_map_duplicate_keys_accepted; 349 - Alcotest.test_case "many repeated (10k)" `Quick test_many_repeated; 350 - ] )
-1
test/test_hostile.mli
··· 1 - val suite : string * unit Alcotest.test_case list
+331 -5
test/test_protobuf.ml
··· 345 345 (* Encode v2, decode with v1 capturing unknowns, re-encode, assert 346 346 bytes round-trip when compared to original v2. *) 347 347 let original = 348 - Protobuf.encode_string schema_v2 { a = 42l; b = "hello"; c = [ 1l; 2l; 3l ] } 348 + Protobuf.encode_string schema_v2 349 + { a = 42l; b = "hello"; c = [ 1l; 2l; 3l ] } 349 350 in 350 351 match Protobuf.decode_with_unknowns_string schema_v1 original with 351 352 | Error msg -> Alcotest.fail msg 352 - | Ok (v1, unknowns) -> 353 + | Ok (v1, unknowns) -> ( 353 354 Alcotest.(check int32) "a decoded" 42l v1.a; 354 - Alcotest.(check bool) "unknowns non-empty" true (String.length unknowns > 0); 355 + Alcotest.(check bool) 356 + "unknowns non-empty" true 357 + (String.length unknowns > 0); 355 358 (* Re-emit via v1 + unknowns; new bytes should decode through v2 356 359 to the same message. *) 357 360 let reemitted = ··· 362 365 | Ok v2' -> 363 366 Alcotest.(check int32) "a survived" 42l v2'.a; 364 367 Alcotest.(check string) "b survived" "hello" v2'.b; 365 - Alcotest.(check (list int32)) "c survived" [ 1l; 2l; 3l ] v2'.c 368 + Alcotest.(check (list int32)) "c survived" [ 1l; 2l; 3l ] v2'.c) 366 369 367 370 let test_unknowns_empty_when_schema_matches () = 368 371 let wire = Protobuf.encode_string schema_v1 { a = 42l } in ··· 379 382 | Error msg -> Alcotest.fail msg 380 383 | Ok r -> Alcotest.(check (list (pair string int32))) "entries" [] r.entries 381 384 385 + (* A schema that declares no fields: every input is unknown. *) 386 + 387 + type empty_msg = unit 388 + 389 + let empty_codec : empty_msg Protobuf.t = 390 + let open Protobuf.Message in 391 + finish (return ()) 392 + 393 + (* ================================================================= 394 + CVE-2015-5237 (protobuf-c++, 2015): integer overflow in varint parser 395 + when a maliciously large length prefix is claimed. 396 + ================================================================= *) 397 + 398 + let test_cve_2015_5237_huge_length () = 399 + (* Tag 14 (str, length-delim), length 0xFFFFFFF (268 MiB), but only 400 + two payload bytes follow. A naive decoder allocates 268 MiB before 401 + realising the buffer is too short. *) 402 + let bad = 403 + "\x72" (* tag 14, wire type 2 *) ^ "\xff\xff\xff\x7f" 404 + (* varint 0x0FFFFFFF = 268435455 *) ^ "ab" 405 + in 406 + match Protobuf.decode_string test1_codec bad with 407 + | Error _ -> () 408 + | Ok _ -> Alcotest.fail "CVE-2015-5237: huge length prefix must be rejected" 409 + 410 + let test_cve_2015_5237_overlong_varint () = 411 + (* Varint with 11 continuation bytes. 64-bit values fit in 10. *) 412 + let bad = "\x08" ^ "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01" in 413 + match Protobuf.decode_string test1_codec bad with 414 + | Error _ -> () 415 + | Ok _ -> Alcotest.fail "CVE-2015-5237: over-long varint must be rejected" 416 + 417 + let test_cve_2015_5237_truncated_tag () = 418 + match Protobuf.decode_string test1_codec "\x80\x80\x80" with 419 + | Error _ -> () 420 + | Ok _ -> Alcotest.fail "CVE-2015-5237: truncated tag must be rejected" 421 + 422 + (* ================================================================= 423 + CVE-2021-22569 (protobuf-java, 2021): malicious input with many 424 + small groups causes unbounded `ByteString` allocations, amplifying 425 + memory pressure. 426 + ================================================================= *) 427 + 428 + let test_cve_2021_22569_many_small_groups () = 429 + (* Many repeated small fields should decode in linear memory, not 430 + amplified. 10k unknown fields at unique tags. *) 431 + let buf = Buffer.create (6 * 10_000) in 432 + for tag = 100 to 10_099 do 433 + Protobuf.Wire.write_tag buf ~field_number:tag 434 + ~wire_type:Protobuf.Wire.Varint; 435 + Protobuf.Wire.write_int32 buf 1l 436 + done; 437 + let wire = Buffer.contents buf in 438 + match Protobuf.decode_string empty_codec wire with 439 + | Ok () -> () 440 + | Error msg -> Alcotest.failf "10k unknown fields rejected: %s" msg 441 + 442 + (* ================================================================= 443 + CVE-2022-1941 (protobuf-c++, 2022): null-pointer dereference when 444 + parsing an all-unknown message through a schema with no declared 445 + fields. 446 + ================================================================= *) 447 + 448 + let test_cve_2022_1941_all_unknown () = 449 + (* Emit a full [Test1] message; decode through [empty_codec] whose 450 + schema has no fields. Every field is unknown. Decoder must not 451 + crash. *) 452 + let wire = Protobuf.encode_string test1_codec { a = 42l } in 453 + match Protobuf.decode_string empty_codec wire with 454 + | Ok () -> () 455 + | Error msg -> Alcotest.failf "empty schema, all unknowns: %s" msg 456 + 457 + (* ================================================================= 458 + CVE-2022-3171 (protobuf-java, 2022): repeated group wire type 459 + (deprecated 3/4) triggers long GC pauses. 460 + ================================================================= *) 461 + 462 + let test_cve_2022_3171_group_wire_type () = 463 + (* Wire type 3 is the deprecated `SGROUP` form. Modern protobuf 464 + rejects it. *) 465 + match Protobuf.decode_string test1_codec "\x0b\x00" with 466 + | Error _ -> () 467 + | Ok _ -> Alcotest.fail "CVE-2022-3171: wire type 3 must be rejected" 468 + 469 + let test_cve_2022_3171_group_wire_type_4 () = 470 + (* Wire type 4 is the deprecated `EGROUP` form. Must be rejected at 471 + tag-parse time regardless of field number. *) 472 + match Protobuf.decode_string test1_codec "\x0c" with 473 + | Error _ -> () 474 + | Ok _ -> Alcotest.fail "CVE-2022-3171: wire type 4 must be rejected" 475 + 476 + (* ================================================================= 477 + CVE-2024-7254 (protobuf-go, 2024): deeply nested unknown groups 478 + cause stack overflow in the parser. 479 + ================================================================= *) 480 + 481 + let test_cve_2024_7254_deep_nesting_known () = 482 + (* 200 levels of declared nested messages exceeds the 100-level 483 + [max_depth] bound baked into the decoder. *) 484 + let nest_codec : unit Protobuf.t = 485 + Protobuf.fix ~default:() (fun self -> 486 + let open Protobuf.Message in 487 + finish 488 + (let* () = 489 + (* [optional 1] with payload type that is [self] (a message 490 + codec). When absent, yields [None]; we collapse to unit 491 + to keep the test type trivial. *) 492 + optional 1 (fun () -> None) self |> fun f -> 493 + let* _v = f in 494 + return () 495 + in 496 + return ())) 497 + in 498 + let rec build_wire n = 499 + if n = 0 then "" 500 + else 501 + let inner = build_wire (n - 1) in 502 + "\x0a" ^ Leb128.encode_u63_string (String.length inner) ^ inner 503 + in 504 + match Protobuf.decode_string nest_codec (build_wire 200) with 505 + | Error _ -> () 506 + | Ok () -> Alcotest.fail "CVE-2024-7254: 200-level nesting must be rejected" 507 + 508 + let test_cve_2024_7254_deep_nesting_unknown () = 509 + (* Deeply nested length-delim fields that are UNKNOWN to the schema 510 + are skipped at the outer level via [Wire.skip_field], which 511 + advances past the body without recursing. No depth bound needed; 512 + the cost is O(N) in wire length. *) 513 + let rec build_wire n = 514 + if n = 0 then "" 515 + else 516 + let inner = build_wire (n - 1) in 517 + "\x7a" (* tag 15, wire type 2 — unknown to [test1_codec] *) 518 + ^ Leb128.encode_u63_string (String.length inner) 519 + ^ inner 520 + in 521 + let wire = build_wire 500 in 522 + (* Must decode without stack overflow. *) 523 + match Protobuf.decode_string test1_codec wire with 524 + | Ok _ | Error _ -> () 525 + 526 + (* ================================================================= 527 + CVE-2024-47554 (rust prost, 2024): length-prefix overflow / DoS 528 + through crafted length fields. 529 + ================================================================= *) 530 + 531 + let test_cve_2024_47554_length_past_end () = 532 + (* Tag 14 (str), length 100, but only 2 bytes follow. *) 533 + let bad = "\x72\x64ab" in 534 + match Protobuf.decode_string test1_codec bad with 535 + | Error _ -> () 536 + | Ok _ -> Alcotest.fail "CVE-2024-47554: length past end must be rejected" 537 + 538 + let test_cve_2024_47554_packed_corrupt_body () = 539 + let packed_codec = 540 + let open Protobuf.Message in 541 + finish 542 + (let* nums = 543 + Protobuf.Message.packed 1 544 + (fun (nums : int32 list) -> nums) 545 + Protobuf.int32 546 + in 547 + return nums) 548 + in 549 + (* Tag 1, wire 2, length 2, then \x80\x80 (unterminated varint inside 550 + the packed blob). *) 551 + let bad = "\x0a\x02\x80\x80" in 552 + match Protobuf.decode_string packed_codec bad with 553 + | Error _ -> () 554 + | Ok _ -> Alcotest.fail "CVE-2024-47554: corrupt packed body must be rejected" 555 + 556 + (* ================================================================= 557 + Generic vulnerability class: reserved tag 0 558 + (protobuf spec §3 forbids field number 0). 559 + ================================================================= *) 560 + 561 + let test_reserved_tag_zero () = 562 + match Protobuf.decode_string test1_codec "\x00" with 563 + | Error _ -> () 564 + | Ok _ -> Alcotest.fail "tag field=0 must be rejected" 565 + 566 + (* ================================================================= 567 + Wire type mismatch: the schema declares varint for tag 1, the wire 568 + carries length-delim. Decoder must reject instead of silently 569 + coercing. 570 + ================================================================= *) 571 + 572 + let test_wire_type_mismatch () = 573 + let bad = 574 + "\x0a\x00" 575 + (* tag 1 wire type 2, length 0 *) 576 + in 577 + match Protobuf.decode_string test1_codec bad with 578 + | Error _ -> () 579 + | Ok _ -> Alcotest.fail "wire type mismatch must be rejected" 580 + 581 + (* ================================================================= 582 + Empty input: proto3 defaults must be returned for every field. 583 + ================================================================= *) 584 + 585 + let test_empty_input () = 586 + match Protobuf.decode_string test1_codec "" with 587 + | Error msg -> Alcotest.failf "empty input should use defaults: %s" msg 588 + | Ok r -> Alcotest.(check int32) "a defaults to 0" 0l r.a 589 + 590 + (* ================================================================= 591 + Trailing unused bytes inside the message body: parse_wire's 592 + boundary check rejects. 593 + ================================================================= *) 594 + 595 + let test_overrun_rejected () = 596 + let good_prefix = Protobuf.encode_string test1_codec { a = 1l } in 597 + let with_trailer = 598 + good_prefix ^ "\x80" 599 + (* truncated continuation *) 600 + in 601 + match Protobuf.decode_string test1_codec with_trailer with 602 + | Error _ -> () 603 + | Ok _ -> Alcotest.fail "trailing truncated varint must be rejected" 604 + 605 + (* ================================================================= 606 + Malformed UTF-8 in a string field: protobuf spec §5 says strings 607 + and bytes share the wire representation, and decoders must accept 608 + non-UTF-8 byte content. No validation must be performed. 609 + ================================================================= *) 610 + 611 + type with_str = { s : string } 612 + 613 + let with_str_codec : with_str Protobuf.t = 614 + let open Protobuf.Message in 615 + finish 616 + (let* s = required 1 (fun r -> r.s) Protobuf.string in 617 + return { s }) 618 + 619 + let test_non_utf8_string_accepted () = 620 + let raw = "\xff\xfe\xfd" in 621 + (* Not valid UTF-8 *) 622 + let wire = Protobuf.encode_string with_str_codec { s = raw } in 623 + match Protobuf.decode_string with_str_codec wire with 624 + | Error msg -> Alcotest.failf "non-UTF-8 string must decode: %s" msg 625 + | Ok r -> Alcotest.(check string) "roundtrip" raw r.s 626 + 627 + (* ================================================================= 628 + Map with duplicate key: protobuf spec says last-wins. A malicious 629 + sender can pad the wire with many duplicate keys; decoder must 630 + handle this in linear memory. 631 + ================================================================= *) 632 + 633 + let test_map_duplicate_keys_accepted () = 634 + (* Encode three entries, two with the same key. Decoder preserves 635 + wire order in the output list; semantic last-wins is the user's 636 + responsibility. *) 637 + let v = { entries = [ ("k", 1l); ("k", 2l); ("x", 99l); ("k", 3l) ] } in 638 + let wire = Protobuf.encode_string dict_codec v in 639 + match Protobuf.decode_string dict_codec wire with 640 + | Error msg -> Alcotest.fail msg 641 + | Ok r -> 642 + Alcotest.(check int) "entry count preserved" 4 (List.length r.entries) 643 + 644 + (* ================================================================= 645 + Many repeated fields: 10 000 tags at the same field, each small. 646 + Must not scale super-linearly. 647 + ================================================================= *) 648 + 649 + type rep = { tags : string list } 650 + 651 + let rep_codec : rep Protobuf.t = 652 + let open Protobuf.Message in 653 + finish 654 + (let* tags = repeated 1 (fun r -> r.tags) Protobuf.string in 655 + return { tags }) 656 + 657 + let test_many_repeated () = 658 + let n = 10_000 in 659 + let buf = Buffer.create (3 * n) in 660 + for _ = 1 to n do 661 + Protobuf.Wire.write_tag buf ~field_number:1 662 + ~wire_type:Protobuf.Wire.Length_delimited; 663 + Protobuf.Wire.write_string buf "x" 664 + done; 665 + let wire = Buffer.contents buf in 666 + match Protobuf.decode_string rep_codec wire with 667 + | Error msg -> Alcotest.failf "many-repeated rejected: %s" msg 668 + | Ok r -> 669 + Alcotest.(check int) "count" n (List.length r.tags); 670 + Alcotest.(check string) "first" "x" (List.hd r.tags) 671 + 672 + let hostile_cases : unit Alcotest.test_case list = 673 + [ 674 + Alcotest.test_case "CVE-2015-5237 huge length prefix" `Quick 675 + test_cve_2015_5237_huge_length; 676 + Alcotest.test_case "CVE-2015-5237 over-long varint" `Quick 677 + test_cve_2015_5237_overlong_varint; 678 + Alcotest.test_case "CVE-2015-5237 truncated tag" `Quick 679 + test_cve_2015_5237_truncated_tag; 680 + Alcotest.test_case "CVE-2021-22569 many small groups" `Quick 681 + test_cve_2021_22569_many_small_groups; 682 + Alcotest.test_case "CVE-2022-1941 all-unknown schema" `Quick 683 + test_cve_2022_1941_all_unknown; 684 + Alcotest.test_case "CVE-2022-3171 group wire type 3" `Quick 685 + test_cve_2022_3171_group_wire_type; 686 + Alcotest.test_case "CVE-2022-3171 group wire type 4" `Quick 687 + test_cve_2022_3171_group_wire_type_4; 688 + Alcotest.test_case "CVE-2024-7254 deep known nesting" `Quick 689 + test_cve_2024_7254_deep_nesting_known; 690 + Alcotest.test_case "CVE-2024-7254 deep unknown nesting" `Quick 691 + test_cve_2024_7254_deep_nesting_unknown; 692 + Alcotest.test_case "CVE-2024-47554 length past end" `Quick 693 + test_cve_2024_47554_length_past_end; 694 + Alcotest.test_case "CVE-2024-47554 packed corrupt body" `Quick 695 + test_cve_2024_47554_packed_corrupt_body; 696 + Alcotest.test_case "reserved tag 0" `Quick test_reserved_tag_zero; 697 + Alcotest.test_case "wire type mismatch" `Quick test_wire_type_mismatch; 698 + Alcotest.test_case "empty input -> defaults" `Quick test_empty_input; 699 + Alcotest.test_case "overrun rejected" `Quick test_overrun_rejected; 700 + Alcotest.test_case "non-UTF-8 string accepted" `Quick 701 + test_non_utf8_string_accepted; 702 + Alcotest.test_case "map duplicate keys accepted" `Quick 703 + test_map_duplicate_keys_accepted; 704 + Alcotest.test_case "many repeated (10k)" `Quick test_many_repeated; 705 + ] 706 + 382 707 let suite = 383 708 ( "protobuf", 384 709 [ ··· 405 730 test_unknown_fields_preserved; 406 731 Alcotest.test_case "unknowns empty when schema matches" `Quick 407 732 test_unknowns_empty_when_schema_matches; 408 - ] ) 733 + ] 734 + @ hostile_cases )
+2 -2
test/test_wire.ml
··· 21 21 write_and_hex (fun b -> Wire.write_tag b ~field_number ~wire_type) 22 22 in 23 23 Alcotest.(check string) 24 - (Fmt.str "tag %d/%a" field_number Wire.pp_wire_type wire_type) 24 + (Fmt.str "tag %d/%a" field_number Wire.pp wire_type) 25 25 expected got 26 26 in 27 27 check 1 Wire.Varint "08"; ··· 50 50 Alcotest.(check int) "field number" field_number fn; 51 51 Alcotest.(check bool) 52 52 "wire type" true 53 - (Wire.wire_type_to_int wire_type = Wire.wire_type_to_int wt); 53 + (Wire.to_int wire_type = Wire.to_int wt); 54 54 Alcotest.(check int) "consumed" (String.length s) off) 55 55 cases 56 56