Coverage Report

Created: 2026-08-14 07:12

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libcoap/tests/oss-fuzz/proxy_target.c
Line
Count
Source
1
/* Proxy fuzzer for libcoap.
2
 *
3
 * Drives coap_proxy_forward_request() with a fuzzer-controlled request PDU.
4
 * The proxy is configured forward-dynamic so the upstream target comes from
5
 * the request's Proxy-Uri or Proxy-Scheme+Uri-Host+Uri-Port options — the
6
 * attacker-controllable parsing surface inside coap_proxy.c.
7
 *
8
 * This harness drives:
9
 *   - coap_get_uri_proxy_scheme_info  (Proxy-Scheme + Uri-Host + Uri-Port)
10
 *   - coap_split_proxy_uri            (full URI string in Proxy-Uri)
11
 *   - coap_verify_proxy_scheme_supported
12
 *   - coap_proxy_get_session / coap_proxy_get_add_list_entry
13
 *   - coap_proxy_get_ongoing_session  (address resolution, session spawn)
14
 *   - coap_proxy_forward_request_lkd  (option mangling, PDU rewrite, send)
15
 *   - coap_proxy_log_entry
16
 *   - coap_proxy_release / coap_proxy_remove_association on cleanup
17
 *
18
 * Outbound UDP sockets to 127.0.0.1:0 quietly drop frames; no listener is
19
 * needed because the proxy doesn't block waiting for the upstream response.
20
 */
21
22
#include "coap3/coap_internal.h"
23
24
#include <stdint.h>
25
#include <stdlib.h>
26
#include <string.h>
27
28
static int
29
0
proxy_fuzz_event_handler(coap_session_t *session, const coap_event_t event) {
30
0
  (void)session;
31
0
  (void)event;
32
0
  return 0;
33
0
}
34
35
/* Required by coap_resource_proxy_uri_init2; we never forward a real response
36
 * back to a client so the handler body just sets a code and returns. */
37
static void
38
proxy_fuzz_handler(coap_resource_t *resource, coap_session_t *session,
39
                   const coap_pdu_t *request, const coap_string_t *query,
40
0
                   coap_pdu_t *response) {
41
0
  (void)resource;
42
0
  (void)session;
43
0
  (void)request;
44
0
  (void)query;
45
0
  response->code = COAP_RESPONSE_CODE(205);
46
0
}
47
48
/* Bounded cursor for slicing option values out of the fuzz buffer. */
49
typedef struct {
50
  const uint8_t *data;
51
  size_t size;
52
  size_t pos;
53
} proxy_cursor_t;
54
55
/* Take up to `max` bytes off the cursor. Returns the actual length written to
56
 * *out_len. May return zero. */
57
static const uint8_t *
58
184
proxy_take(proxy_cursor_t *c, size_t max, size_t *out_len) {
59
184
  size_t avail = c->size > c->pos ? c->size - c->pos : 0;
60
184
  size_t take = avail < max ? avail : max;
61
184
  const uint8_t *p = c->data + c->pos;
62
184
  c->pos += take;
63
184
  *out_len = take;
64
184
  return p;
65
184
}
66
67
int
68
32
LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
69
  /* Need: mode byte + method byte + at least a few option-payload bytes. */
70
32
  if (size < 8)
71
4
    return 0;
72
73
28
  coap_context_t *ctx = NULL;
74
28
  coap_session_t *req_session = NULL;
75
28
  coap_resource_t *resource = NULL;
76
28
  coap_pdu_t *request = NULL;
77
28
  coap_pdu_t *response = NULL;
78
28
  coap_address_t addr;
79
80
28
  const uint8_t mode = data[0];
81
28
  const uint8_t method_byte = data[1];
82
83
28
  coap_startup();
84
28
  coap_set_log_level(COAP_LOG_EMERG);
85
28
  coap_dtls_set_log_level(COAP_LOG_EMERG);
86
28
  coap_debug_set_packet_loss("100%");
87
28
  coap_debug_set_packet_fail("100%");
88
89
28
  ctx = coap_new_context(NULL);
90
28
  if (!ctx)
91
0
    goto cleanup;
92
28
  coap_register_event_handler(ctx, proxy_fuzz_event_handler);
93
94
  /* Pick a proxy type from mode bits 2-4. Cover all three base types (static,
95
   * dynamic, reverse) crossed with the option-controlling bits that are
96
   * meaningful for each, per coap_proxy.h:
97
   *   0: FWD_STATIC                 — needs pre-populated server_list entry
98
   *   1: FWD_STATIC | STRIP         — static, strip proxy options on forward
99
   *   2: FWD_DYNAMIC               — pulls target from request's Proxy-* options
100
   *   3: FWD_DYNAMIC | STRIP        — same, strip proxy options (forces re-walk)
101
   *   4: FWD_DYNAMIC | MCAST        — allow multicast upstream resolution
102
   *   5: FWD_DYNAMIC | DYN_DEFINED  — no auto-added dynamic upstreams
103
   *   6: REV                        — reverse proxy, also uses server_list entry
104
   *   7: REV | STRIP                — reverse, strip proxy options
105
   * STRIP is the more interesting code path because it forces an option re-walk.
106
   */
107
28
  static const coap_proxy_t types[8] = {
108
28
    COAP_PROXY_FWD_STATIC,
109
28
    COAP_PROXY_FWD_STATIC | COAP_PROXY_BIT_STRIP,
110
28
    COAP_PROXY_FWD_DYNAMIC,
111
28
    COAP_PROXY_FWD_DYNAMIC | COAP_PROXY_BIT_STRIP,
112
28
    COAP_PROXY_FWD_DYNAMIC | COAP_PROXY_BIT_MCAST,
113
28
    COAP_PROXY_FWD_DYNAMIC | COAP_PROXY_DYN_DEFINED,
114
28
    COAP_PROXY_REV,
115
28
    COAP_PROXY_REV | COAP_PROXY_BIT_STRIP,
116
28
  };
117
28
  coap_proxy_t selected_type = types[(mode >> 2) & 0x07];
118
119
  /* Backing storage for a static server_list entry. coap_proxy_get_session
120
   * memcpys the entry into server_use, so stack storage is fine. */
121
28
  coap_proxy_server_t static_entry;
122
28
  memset(&static_entry, 0, sizeof(static_entry));
123
28
  static_entry.uri.scheme = COAP_URI_SCHEME_COAP;
124
28
  static_entry.uri.host.s = (const uint8_t *)"127.0.0.1";
125
28
  static_entry.uri.host.length = 9;
126
28
  static_entry.uri.port = 0; /* Picks something the OS will quietly drop. */
127
128
28
  coap_proxy_server_list_t server_list;
129
28
  memset(&server_list, 0, sizeof(server_list));
130
28
  server_list.type = selected_type;
131
  /* Match on the base type only (COAP_PROXY_NEW_MASK), so the STRIP/MCAST/
132
   * DYN_DEFINED option bits don't defeat the comparison — both FWD_STATIC and
133
   * REV (with or without those bits) need a pre-populated upstream entry. */
134
28
  coap_proxy_t base_type = selected_type & COAP_PROXY_NEW_MASK;
135
28
  if (base_type == COAP_PROXY_FWD_STATIC || base_type == COAP_PROXY_REV) {
136
14
    server_list.entry = &static_entry;
137
14
    server_list.entry_count = 1;
138
14
  }
139
140
  /* Build the "incoming client" session. We use UDP-to-loopback because UDP
141
   * doesn't connect — coap_new_client_session returns a fully-formed session
142
   * with no real network handshake. We won't actually transmit on it. */
143
28
  coap_address_init(&addr);
144
28
  addr.addr.sin.sin_family = AF_INET;
145
28
  addr.addr.sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
146
28
  addr.addr.sin.sin_port = htons(5683);
147
28
  req_session = coap_new_client_session(ctx, NULL, &addr, COAP_PROTO_UDP);
148
28
  if (!req_session)
149
0
    goto cleanup;
150
28
  req_session->state = COAP_SESSION_STATE_ESTABLISHED;
151
152
  /* The proxy code uses `resource` to track which proxy URI resource the
153
   * request came in on. Use the official proxy-URI resource builder so flags
154
   * like `is_proxy_uri` are set, but with no host_name_list — we don't enforce
155
   * the host filter here. */
156
28
  resource = coap_resource_proxy_uri_init2(proxy_fuzz_handler, 0, NULL, 0);
157
28
  if (!resource)
158
0
    goto cleanup;
159
28
  coap_add_resource(ctx, resource);
160
161
  /* Build the request PDU. */
162
28
  const coap_pdu_code_t methods[8] = {
163
28
    COAP_REQUEST_CODE_GET,   COAP_REQUEST_CODE_POST,
164
28
    COAP_REQUEST_CODE_PUT,   COAP_REQUEST_CODE_DELETE,
165
28
    COAP_REQUEST_CODE_FETCH, COAP_REQUEST_CODE_PATCH,
166
28
    COAP_REQUEST_CODE_IPATCH, COAP_REQUEST_CODE_GET,
167
28
  };
168
28
  coap_pdu_type_t pdu_type = (mode & 0x01) ? COAP_MESSAGE_NON : COAP_MESSAGE_CON;
169
28
  size_t max_pdu = coap_session_max_pdu_size(req_session);
170
28
  request = coap_pdu_init(pdu_type, methods[method_byte & 0x07],
171
28
                          coap_new_message_id(req_session), max_pdu);
172
28
  if (!request)
173
0
    goto cleanup;
174
175
  /* Deterministic 4-byte token derived from the input keeps option-tracking
176
   * predictable across iterations without consuming the cursor's payload. */
177
28
  const uint8_t token[4] = { data[2], data[3], data[4], data[5] };
178
28
  coap_add_token(request, sizeof(token), token);
179
180
  /* Add options in monotonically increasing number order, since
181
   * coap_add_option doesn't reorder. Numbers below from coap_option.h:
182
   *   IF_MATCH=1  URI_HOST=3  URI_PORT=7  URI_PATH=11  PROXY_URI=35
183
   *   PROXY_SCHEME=39  SIZE1=60  ...
184
   */
185
28
  proxy_cursor_t c = { data + 6, size - 6, 0 };
186
187
  /* URI-Host (option 3). Pull a small slice — the proxy uses this together
188
   * with Proxy-Scheme to compute the upstream target. */
189
28
  {
190
28
    size_t n;
191
28
    const uint8_t *p = proxy_take(&c, 32, &n);
192
28
    if (n)
193
28
      coap_add_option(request, COAP_OPTION_URI_HOST, n, p);
194
28
  }
195
196
  /* URI-Port (option 7). 2 bytes max, encodes as a CoAP varint internally. */
197
28
  {
198
28
    size_t n;
199
28
    const uint8_t *p = proxy_take(&c, 2, &n);
200
28
    if (n)
201
17
      coap_add_option(request, COAP_OPTION_URI_PORT, n, p);
202
28
  }
203
204
  /* URI-Path (option 11) — a couple of segments to drive option-walk code. */
205
84
  for (int i = 0; i < 2; i++) {
206
56
    size_t n;
207
56
    const uint8_t *p = proxy_take(&c, 16, &n);
208
56
    if (n)
209
33
      coap_add_option(request, COAP_OPTION_URI_PATH, n, p);
210
56
  }
211
212
  /* Submode (bits 5-6 of mode) selects which proxy target option(s) we set:
213
   *   0: Proxy-Scheme only  — drives coap_get_uri_proxy_scheme_info
214
   *   1: Proxy-Uri only     — drives coap_split_proxy_uri
215
   *   2: both               — proxy code prefers Proxy-Uri but parses both
216
   *   3: neither            — exercises the "404 no proxy info" path
217
   */
218
28
  uint8_t submode = (mode >> 5) & 0x03;
219
220
28
  if (submode == 0 || submode == 2) {
221
    /* Proxy-Scheme value (option 39). The parser at coap_proxy.c:222-249
222
     * branches on opt_len 4/5/7/8/9 to identify "coap" / "coaps" / "coap+ws"
223
     * / "coap+tcp" / "coaps+tcp" — we want the fuzzer to discover those. */
224
14
    size_t n;
225
14
    const uint8_t *p = proxy_take(&c, 9, &n);
226
14
    if (n)
227
6
      coap_add_option(request, COAP_OPTION_PROXY_SCHEME, n, p);
228
14
  }
229
28
  if (submode == 1 || submode == 2) {
230
    /* Proxy-Uri value (option 35). Up to 1034 bytes per RFC 7252 — we cap at
231
     * 256 to leave room for payload. The parser is coap_split_proxy_uri. */
232
19
    size_t n;
233
19
    const uint8_t *p = proxy_take(&c, 256, &n);
234
19
    if (n)
235
12
      coap_add_option(request, COAP_OPTION_PROXY_URI, n, p);
236
19
  }
237
238
  /* Optional Observe option (bit 7 of mode, 4 bytes max). Triggers the proxy's
239
   * observe-cache logic if a proxy_response_cb were registered. */
240
28
  if ((mode >> 7) & 0x01) {
241
11
    size_t n;
242
11
    const uint8_t *p = proxy_take(&c, 4, &n);
243
11
    if (n)
244
6
      coap_add_option(request, COAP_OPTION_OBSERVE, n, p);
245
11
  }
246
247
  /* Remaining bytes become the request body. */
248
28
  {
249
28
    size_t n;
250
28
    const uint8_t *p = proxy_take(&c, c.size > c.pos ? c.size - c.pos : 0, &n);
251
28
    if (n)
252
14
      coap_add_data(request, n, p);
253
28
  }
254
255
  /* Response PDU the proxy fills in (with an error code on failure). */
256
28
  response = coap_pdu_init(COAP_MESSAGE_ACK, 0,
257
28
                           coap_new_message_id(req_session), max_pdu);
258
28
  if (!response)
259
0
    goto cleanup;
260
261
  /* The single API call we're here to fuzz. */
262
28
  coap_proxy_forward_request(req_session, request, response, resource, NULL,
263
28
                             &server_list);
264
265
28
cleanup:
266
28
  if (request)
267
28
    coap_delete_pdu(request);
268
28
  if (response)
269
28
    coap_delete_pdu(response);
270
  /* Don't release req_session manually — coap_free_context iterates
271
   * ctx->sessions and releases each (same idiom as the other harnesses). */
272
28
  if (ctx)
273
28
    coap_free_context(ctx);
274
28
  coap_cleanup();
275
28
  return 0;
276
28
}