Coverage Report

Created: 2026-08-31 06:18

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}