/src/glaze/fuzzing/repe_registry.cpp
Line | Count | Source |
1 | | // Fuzzes the REPE registry over untrusted wire bytes. |
2 | | // |
3 | | // The registry parses bytes it does not own: a span handed to registry::call, and the same span |
4 | | // handed across an FFI boundary by repe::plugin_call. Neither carries a null terminator, and both |
5 | | // are reachable by anyone who can send a message. Every buffer here is heap allocated at exactly |
6 | | // the size of its contents so ASAN reports a read one byte past the end. |
7 | | // |
8 | | // Most random bytes fail header validation, which would leave the registry itself barely exercised, |
9 | | // so the input is also replayed wrapped in a well formed header. That splits the work: the raw pass |
10 | | // fuzzes the header parser, the wrapped pass fuzzes the query dispatch and body reader behind it. |
11 | | |
12 | | #include <cstddef> |
13 | | #include <cstdint> |
14 | | #include <cstring> |
15 | | #include <functional> |
16 | | #include <glaze/glaze.hpp> |
17 | | #include <glaze/rpc/registry.hpp> |
18 | | #include <glaze/rpc/repe/plugin_helper.hpp> |
19 | | #include <map> |
20 | | #include <optional> |
21 | | #include <string> |
22 | | #include <variant> |
23 | | #include <vector> |
24 | | |
25 | | // Reflection needs external linkage, so these cannot live in an anonymous namespace. |
26 | | namespace repe_fuzz |
27 | | { |
28 | | struct point |
29 | | { |
30 | | double x{}; |
31 | | double y{}; |
32 | | }; |
33 | | |
34 | | struct amount |
35 | | { |
36 | | double value{}; |
37 | | }; |
38 | | |
39 | | struct nested |
40 | | { |
41 | | int32_t depth{}; |
42 | | std::string label{}; |
43 | | }; |
44 | | |
45 | | // Wide enough that the dispatch has somewhere to go and the reader has more than one shape to |
46 | | // resolve against. Three surfaces matter here and each is represented: |
47 | | // - plain members, where the read target is a fixed type |
48 | | // - std::function members, which is how a call with typed parameters is registered, and so |
49 | | // the only way to reach the reader that parses arguments |
50 | | // - a variant, whose resolution re-parses speculatively -- the path where a reader is most |
51 | | // likely to walk off the end of a buffer it does not own |
52 | | struct fuzz_api |
53 | | { |
54 | | int32_t value{}; |
55 | | std::string name{}; |
56 | | point position{}; |
57 | | nested inner{}; |
58 | | std::vector<int32_t> series{}; |
59 | | std::map<std::string, int32_t> table{}; |
60 | | std::optional<double> maybe{}; |
61 | | std::variant<std::string, amount> measure{}; |
62 | | |
63 | | // The fuzzer supplies these arguments, so every one of them arrives at its own extremes. |
64 | | // Signed overflow in the body of a target is undefined behavior in the target, not a finding |
65 | | // about the registry, and UBSan stops the run on it either way -- so the arithmetic here goes |
66 | | // through unsigned, where wrapping is defined and the result is still a function of the input. |
67 | 403 | std::function<int32_t(int32_t)> doubled = [](int32_t v) { return int32_t(uint32_t(v) * 2u); }; |
68 | 461 | std::function<std::string(const std::string&)> greet = [](const std::string& who) { return "hi " + who; }; |
69 | 76 | std::function<point(const point&)> midpoint = [](const point& p) -> point { return {p.x / 2, p.y / 2}; }; |
70 | 467 | std::function<double(std::vector<double>&)> total = [](std::vector<double>& v) { |
71 | 467 | double sum{}; |
72 | 818 | for (auto d : v) sum += d; |
73 | 467 | return sum; |
74 | 467 | }; |
75 | 13.6k | std::function<void()> reset = [] {}; |
76 | | }; |
77 | | |
78 | | // Member functions listed in a glz::meta reach different endpoint registrations than the |
79 | | // std::function members above -- register_member_function_endpoint and its with_params variant. |
80 | | // The with_params one parks its argument in a `static thread_local`, which is the kind of state |
81 | | // that survives between inputs and turns a crash into one that will not reproduce, so it is |
82 | | // worth having under the fuzzer rather than only under the unit tests. |
83 | | struct member_api |
84 | | { |
85 | | int32_t counter{}; |
86 | | |
87 | | // Wraps rather than overflows, for the reason given on `doubled` above. `counter` accumulates |
88 | | // across inputs, so it reaches its extremes on its own even when no single argument is large. |
89 | | int32_t scale(int32_t v) |
90 | 499 | { |
91 | 499 | counter = int32_t(uint32_t(counter) + uint32_t(v)); |
92 | 499 | return int32_t(uint32_t(v) * 3u); |
93 | 499 | } |
94 | 13.6k | void bump() { counter = int32_t(uint32_t(counter) + 1u); } |
95 | 177 | point shift(const point& p) { return {p.x + 1, p.y + 1}; } |
96 | 326 | std::string tag(const nested& n) { return n.label; } |
97 | | }; |
98 | | } |
99 | | |
100 | | template <> |
101 | | struct glz::meta<repe_fuzz::member_api> |
102 | | { |
103 | | using T = repe_fuzz::member_api; |
104 | | static constexpr auto value = object(&T::counter, &T::scale, &T::bump, &T::shift, &T::tag); |
105 | | }; |
106 | | |
107 | | namespace |
108 | | { |
109 | | using repe_fuzz::fuzz_api; |
110 | | |
111 | | // Exactly sized, so there is no slack after the message for an over-read to land in harmlessly. |
112 | | std::vector<char> exact_buffer(std::string_view bytes) |
113 | 508k | { |
114 | 508k | std::vector<char> buf(bytes.size()); |
115 | 508k | if (!bytes.empty()) { |
116 | 508k | std::memcpy(buf.data(), bytes.data(), bytes.size()); |
117 | 508k | } |
118 | 508k | return buf; |
119 | 508k | } |
120 | | |
121 | | std::string framed_message(std::string_view query, std::string_view body, uint8_t flags) |
122 | 301k | { |
123 | 301k | glz::repe::header hdr{}; |
124 | 301k | hdr.spec = glz::repe::repe_magic; |
125 | 301k | hdr.version = 1; |
126 | 301k | hdr.id = 1; |
127 | 301k | hdr.notify = (flags & 0x1) ? 1 : 0; |
128 | 301k | hdr.query_length = query.size(); |
129 | 301k | hdr.body_length = body.size(); |
130 | 301k | hdr.length = sizeof(glz::repe::header) + query.size() + body.size(); |
131 | 301k | hdr.query_format = glz::repe::query_format::JSON_POINTER; |
132 | 301k | hdr.body_format = glz::repe::body_format::JSON; |
133 | | // The registry echoes a request that already carries an error back to the sender without |
134 | | // dispatching it. Nothing else in this target sets ec, so without this that branch is dead. |
135 | 301k | hdr.ec = (flags & 0x2) ? glz::error_code::invalid_query : glz::error_code::none; |
136 | | |
137 | 301k | std::string msg; |
138 | 301k | msg.resize(hdr.length); |
139 | 301k | std::memcpy(msg.data(), &hdr, sizeof(hdr)); |
140 | 301k | std::memcpy(msg.data() + sizeof(hdr), query.data(), query.size()); |
141 | 301k | std::memcpy(msg.data() + sizeof(hdr) + query.size(), body.data(), body.size()); |
142 | 301k | return msg; |
143 | 301k | } |
144 | | |
145 | | void drive(std::span<const char> request) |
146 | 315k | { |
147 | 315k | glz::registry<> registry; |
148 | 315k | fuzz_api api{}; |
149 | 315k | repe_fuzz::member_api members{}; |
150 | | // members first, so the last registration -- and therefore the root endpoint -- is the wider |
151 | | // of the two objects. Root reads the whole object in one document, so it should be the one |
152 | | // with the nested struct, containers, optional and variant in it. |
153 | 315k | registry.on(members); |
154 | 315k | registry.on(api); |
155 | | |
156 | 315k | std::string response; |
157 | 315k | registry.call(request, response); |
158 | 315k | if (!response.empty()) { |
159 | | // The response is a message in its own right, so parsing it back exercises the header |
160 | | // reader on bytes the registry itself produced. Copy it out at exactly its own size first: |
161 | | // a std::string keeps an implicit '\0' at data()[size()] and allocator slack behind it, |
162 | | // which is the very thing this target exists to not have. |
163 | 193k | const auto buf = exact_buffer(response); |
164 | 193k | [[maybe_unused]] auto parsed = glz::repe::parse_request({buf.data(), buf.size()}); |
165 | 193k | } |
166 | 315k | } |
167 | | |
168 | | // plugin_call forwards to the same registry::call overload drive() already uses, so running it |
169 | | // per drive doubled the work for one extra line of coverage: the pointer-and-length wrapper and |
170 | | // its thread_local response buffer. Once per input is enough to keep that line exercised. |
171 | | void drive_plugin(std::span<const char> request) |
172 | 13.7k | { |
173 | 13.7k | glz::registry<> registry; |
174 | 13.7k | fuzz_api api{}; |
175 | 13.7k | registry.on(api); |
176 | 13.7k | [[maybe_unused]] auto out = glz::repe::plugin_call(registry, request.data(), request.size()); |
177 | 13.7k | } |
178 | | } |
179 | | |
180 | | extern "C" int LLVMFuzzerTestOneInput(const uint8_t* Data, size_t Size) |
181 | 13.7k | { |
182 | 13.7k | const std::string_view input{reinterpret_cast<const char*>(Data), Size}; |
183 | | |
184 | | // Raw bytes: header validation, length arithmetic, and the query/body views it hands out. |
185 | 13.7k | { |
186 | 13.7k | const auto buf = exact_buffer(input); |
187 | 13.7k | drive({buf.data(), buf.size()}); |
188 | 13.7k | drive_plugin({buf.data(), buf.size()}); |
189 | 13.7k | } |
190 | | |
191 | 13.7k | if (Size < 2) { |
192 | 2 | return 0; |
193 | 2 | } |
194 | | |
195 | | // Wrapped: the first byte picks the framing, the second says how much of the rest is the query. |
196 | | // The query is what the dispatch walks and the body is what the reader parses, so splitting the |
197 | | // input between them lets a single corpus entry drive both. |
198 | 13.7k | const uint8_t flags = Data[0]; |
199 | 13.7k | const std::string_view rest = input.substr(2); |
200 | 13.7k | const size_t query_size = rest.empty() ? 0 : size_t(Data[1]) % (rest.size() + 1); |
201 | 13.7k | const std::string_view query = rest.substr(0, query_size); |
202 | 13.7k | const std::string_view body = rest.substr(query_size); |
203 | | |
204 | 13.7k | { |
205 | 13.7k | const auto framed = framed_message(query, body, flags); |
206 | 13.7k | const auto buf = exact_buffer(framed); |
207 | 13.7k | drive({buf.data(), buf.size()}); |
208 | 13.7k | } |
209 | | |
210 | | // The same body against a fixed query that is known to resolve, so the body reader is reached |
211 | | // even when the fuzzer has not yet learned what a valid JSON pointer looks like. |
212 | | // "" is the root endpoint, which reads the whole registered object in one document -- every |
213 | | // nested struct, container, optional and variant at once, and the deepest reader nesting on |
214 | | // offer. It is the widest single target here, so it must not be left out of the list. |
215 | 13.7k | for (std::string_view known : {"", "/value", "/name", "/position", "/position/x", "/inner", "/inner/label", |
216 | 13.7k | "/series", "/table", "/maybe", "/measure", "/doubled", "/greet", "/midpoint", |
217 | 287k | "/total", "/reset", "/counter", "/scale", "/bump", "/shift", "/tag"}) { |
218 | 287k | const auto framed = framed_message(known, body, flags); |
219 | 287k | const auto buf = exact_buffer(framed); |
220 | 287k | drive({buf.data(), buf.size()}); |
221 | 287k | } |
222 | | |
223 | 13.7k | return 0; |
224 | 13.7k | } |