Coverage for /pythoncovmergedfiles/medio/medio/src/onnx/onnx/fuzz/fuzz_compose.py: 70%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1###### Coverage stub
2import atexit
3import coverage
4cov = coverage.coverage(data_file='.coverage', cover_pylib=True)
5cov.start()
6# Register an exist handler that will print coverage
7def exit_handler():
8 cov.stop()
9 cov.save()
10atexit.register(exit_handler)
11####### End of coverage stub
12# Copyright (c) ONNX Project Contributors
13# SPDX-License-Identifier: Apache-2.0
14"""Atheris fuzz harness for onnx.compose.
16Two input paths are exercised per iteration, selected by a fuzzer-controlled
17toggle byte read from the *tail* of the input:
19* Raw -> a 4-byte big-endian length prefix splits the remaining bytes
20 into m1 | m2, each parsed via onnx.load_model_from_string. Catches
21 protobuf parser bugs and bugs reachable only through crafted serialized
22 models the structured builder will not produce.
24* Structured -> helper.make_model from FuzzedDataProvider builds two small
25 linear graphs with predictable input/output names, so merge_models
26 reaches its connect_io/add_prefix logic on most iterations rather than
27 only when the parser happens to accept a random byte string.
29Further bits of the toggle byte select prefix1/prefix2 (add_prefix
30collision-resolution path) and a randomized (vs. derived) io_map, so a
31single harness covers merge_models, merge_graphs, check_overlapping_names,
32the recursive connect_io subgraph rewrite, and add_prefix.
33"""
36import random
37import struct
38import sys
40import atheris
42with atheris.instrument_imports():
43 from onnx import (
44 TensorProto,
45 checker,
46 compose,
47 defs,
48 helper,
49 load_model_from_string,
50 )
52_UNARY = ("Relu", "Sigmoid", "Tanh", "Abs", "Neg", "Identity")
55def _value_info(name):
56 return helper.make_tensor_value_info(name, TensorProto.FLOAT, ["N"])
59def _build_model(fdp, tag, opset):
60 """Build a small linear graph with predictable input/output names.
62 Naming the sole input/output `In<tag>`/`Out<tag>` lets the derived
63 io_map (m1's outputs -> m2's inputs) connect on most iterations, so
64 merge_models' rewrite logic is reached without relying on the raw
65 protobuf parser to produce compatible names. `opset` is shared between
66 both models: merge_models rejects mismatched opset_import up front, so
67 picking it independently per model would make the structured path
68 rarely reach the merge/connect logic it exists to exercise.
69 """
70 n_ops = fdp.ConsumeIntInRange(0, 4)
71 last = f"In{tag}"
72 nodes = []
73 for i in range(n_ops):
74 op = _UNARY[fdp.ConsumeIntInRange(0, len(_UNARY) - 1)]
75 nxt = f"v{tag}_{i}"
76 nodes.append(helper.make_node(op, [last], [nxt]))
77 last = nxt
78 out_name = f"Out{tag}"
79 nodes.append(helper.make_node("Identity", [last], [out_name]))
80 graph = helper.make_graph(
81 nodes, f"g{tag}", [_value_info(f"In{tag}")], [_value_info(out_name)]
82 )
83 return helper.make_model(graph, opset_imports=[helper.make_opsetid("", opset)])
86def _derived_io_map(m1, m2):
87 outs = [o.name for o in m1.graph.output]
88 ins = [i.name for i in m2.graph.input]
89 return list(zip(outs, ins, strict=False))
92def _random_io_map(data, m1, m2):
93 outs = [o.name for o in m1.graph.output]
94 ins = [i.name for i in m2.graph.input]
95 if not outs or not ins:
96 return []
97 rng = random.Random(data)
98 rng.shuffle(outs)
99 rng.shuffle(ins)
100 n = rng.randint(0, min(len(outs), len(ins)))
101 return list(zip(outs[:n], ins[:n], strict=True))
104def TestOneInput(data):
105 # Toggles live in the trailing byte; see module docstring and
106 # onnx/fuzz/README.md's "fuzz_compose.py toggle byte" table.
107 if len(data) < 2:
108 return
109 toggles = data[-1]
110 body = data[:-1]
111 use_prefix = bool(toggles & 0x01)
112 use_structured = bool(toggles & 0x04)
113 use_random_io_map = bool(toggles & 0x08)
114 # bits 0x02, 0x10..0x80 are reserved for future toggles; mutations
115 # against them are harmless until claimed.
117 try:
118 if use_structured:
119 fdp = atheris.FuzzedDataProvider(body)
120 opset = fdp.ConsumeIntInRange(7, defs.onnx_opset_version())
121 m1 = _build_model(fdp, 1, opset)
122 m2 = _build_model(fdp, 2, opset)
123 else:
124 if len(body) < 4:
125 return
126 (n1,) = struct.unpack(">I", body[:4])
127 rest = body[4:]
128 if n1 > len(rest):
129 return
130 m1 = load_model_from_string(rest[:n1])
131 m2 = load_model_from_string(rest[n1:])
133 io_map = (
134 _random_io_map(body, m1, m2)
135 if use_random_io_map
136 else _derived_io_map(m1, m2)
137 )
139 prefix1 = "g1_" if use_prefix else None
140 prefix2 = "g2_" if use_prefix else None
141 merged = compose.merge_models(m1, m2, io_map, prefix1=prefix1, prefix2=prefix2)
142 checker.check_model(merged, full_check=True)
143 except Exception:
144 # Malformed fuzz inputs raise a broad set of expected exceptions
145 # (ValidationError, TypeError, ValueError, DecodeError, ...).
146 # Real bugs surface as crashes, hangs, or sanitizer reports.
147 return
150def main():
151 atheris.instrument_all()
152 atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True)
153 atheris.Fuzz()
156if __name__ == "__main__":
157 main()