Coverage Report

Created: 2026-11-12 06:42

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/freeradius-server/src/fuzzer/fuzzer_tmpl.c
Line
Count
Source
1
/*
2
 *   This program is free software; you can redistribute it and/or modify
3
 *   it under the terms of the GNU General Public License as published by
4
 *   the Free Software Foundation; either version 2 of the License, or
5
 *   (at your option) any later version.
6
 *
7
 *   This program is distributed in the hope that it will be useful,
8
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
9
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10
 *   GNU General Public License for more details.
11
 */
12
13
/**
14
 * @file src/fuzzer/fuzzer_tmpl.c
15
 * @brief Fuzz the tmpl tokenize -> resolve pipeline.
16
 *
17
 * Drives the two public tmpl parsers:
18
 *
19
 *   tmpl_afrom_substr()      - the general parser, dispatches by quote.
20
 *                              Reads from an fr_sbuff_t with explicit length,
21
 *                              i.e. the input is NOT required to be
22
 *                              NUL-terminated.  Network-attacker-reachable
23
 *                              via every place a config string or xlat
24
 *                              operand is turned into a tmpl.
25
 *
26
 *   tmpl_afrom_attr_str()    - the attribute-only convenience wrapper.
27
 *                              Takes a NUL-terminated C string.  Used by
28
 *                              callers that already have a flat name
29
 *                              (e.g. legacy callers, some unit tests).
30
 *
31
 *  The APIs are called based on mode (see below), and then
32
 *  tmpl_resolve() and tmpl_print() are called to fully exercise the
33
 *  tmpl code.
34
 *
35
 * Input layout:
36
 *   byte[0]      - mode selector, used mod the number of variants.
37
 *   byte[1..]    - the tmpl text.  For tmpl_afrom_substr() the bytes are
38
 *                  used verbatim and NOT NUL-terminated; for
39
 *                  tmpl_afrom_attr_str() they are copied into a separate
40
 *                  NUL-terminated scratch buffer first (the function's
41
 *                  contract requires that).
42
 */
43
RCSID("$Id: 196785c16dfa576f82e24e91cef4b4e212bb07a5 $")
44
45
#include <freeradius-devel/fuzzer/common.h>
46
#include <freeradius-devel/server/base.h>
47
#include <freeradius-devel/server/tmpl.h>
48
#include <freeradius-devel/unlang/base.h>
49
50
int LLVMFuzzerInitialize(int *argc, char ***argv);
51
int LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len);
52
53
static tmpl_res_rules_t tr_rules;
54
55
int LLVMFuzzerInitialize(int *argc, char ***argv)
56
8
{
57
8
  if (dict) return 0;
58
59
8
  if (fuzzer_common_init(argc, argv, false) < 0) fr_exit_now(EXIT_FAILURE);
60
61
8
  if (fr_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) {
62
0
  error:
63
0
    fr_perror("fuzzer_tmpl");
64
0
    fr_exit_now(EXIT_FAILURE);
65
0
  }
66
67
8
  tr_rules = (tmpl_res_rules_t) { .dict_def = dict };
68
69
  /*
70
   *  tmpl_global_init() sets up tmpl_attr_unspec which the
71
   *  [<filter>]-only path of tmpl_attr_afrom_attr_substr()
72
   *  dereferences unconditionally - without it, an input like
73
   *  "[0]" SIGSEGVs on a NULL ar_da.  See tmpl_eval.c:1378.
74
   */
75
8
  if (tmpl_global_init() < 0) goto error;
76
77
  /*
78
   *  request_attr_request and friends are read out of the
79
   *  tmpl_rules .list_def below, so request_global_init() has to
80
   *  have populated them before we tokenise anything.
81
   */
82
8
  if (request_global_init() < 0) goto error;
83
84
  /*
85
   *  Pull in the xlat infrastructure too: tmpl_afrom_substr()
86
   *  hands a TMPL_TYPE_XLAT subtree off to xlat_tokenize_*, and
87
   *  xlat_tmpl_normalize() is called from inside that path.
88
   *  Without unlang_global_init() the xlat function tree is
89
   *  empty and every "%foo(...)" tail-call short-circuits to
90
   *  "unknown function" before reaching the parser surface we
91
   *  want to fuzz.
92
   */
93
8
  if (unlang_global_init() < 0) goto error;
94
95
8
  return 0;
96
8
}
97
98
/*
99
 *  Use the same poison scheme as fuzzer_xlat.c: ASan poisons
100
 *  either side of the live fmt region so any code path that walks
101
 *  past the declared sbuff bounds (instead of respecting the
102
 *  explicit length) trips a report.  fr_sbuff lengths are honest,
103
 *  but a bug in one of the dispatched parsers -
104
 *  tmpl_request_ref_list_from_substr, the OID branch in
105
 *  tmpl_attr_afrom_attr_substr, etc. - that does strlen() or
106
 *  memchr() over the underlying buffer will get caught here.
107
 */
108
12.4k
#define POISON_START 64
109
#define POISON_END   64
110
111
/*
112
 *  The four tmpl_afrom_substr() quote variants, plus
113
 *  tmpl_afrom_attr_str().  Keep these contiguous so that a single
114
 *  mode selects between them.
115
 */
116
10.7k
#define MODE_SUBSTR_BARE  0
117
12.2k
#define MODE_SUBSTR_DOUBLE  1
118
8.84k
#define MODE_SUBSTR_SINGLE  2
119
14.7k
#define MODE_SUBSTR_BACK  3
120
689
#define MODE_ATTR_STR   4
121
12.4k
#define MODE_COUNT    5
122
123
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
124
12.4k
{
125
12.4k
  TALLOC_CTX    *ctx;
126
12.4k
  tmpl_t      *vpt = NULL;
127
12.4k
  tmpl_rules_t    t_rules;
128
12.4k
  tmpl_attr_error_t attr_err = TMPL_ATTR_ERROR_NONE;
129
12.4k
  uint8_t     mode;
130
12.4k
  uint8_t     *raw_fmt = NULL;
131
12.4k
  char      *fmt = NULL;
132
12.4k
  size_t      fmt_len;
133
12.4k
  fr_slen_t   slen = -1;
134
135
12.4k
  if (!dict) return 0;
136
12.4k
  if (size < 2) return 0;
137
12.4k
  if (size > 4096) return 0;  /* keep iterations fast */
138
139
12.4k
  mode    = data[0] % MODE_COUNT;
140
12.4k
  fmt_len = size - 1;
141
142
12.4k
  ctx = talloc_init_const("fuzzer_tmpl");
143
12.4k
  if (!ctx) return 0;
144
145
  /*
146
   *  Tokenisers consume an sbuff with an explicit length, so we
147
   *  deliberately do NOT NUL-terminate. Any code path that does
148
   *  strlen/strchr/memchr on the underlying buffer (rather than
149
   *  respecting the sbuff end) will read into the poisoned gutter
150
   *  and trip ASan, which is the bug we want to surface.
151
   *
152
   *  We allocate an extra byte to NUL terminate the input
153
   *  for functions which need that.
154
   */
155
12.4k
  raw_fmt = talloc_array(ctx, uint8_t, POISON_START + fmt_len + 1 + POISON_END);
156
12.4k
  if (!raw_fmt) goto done;
157
12.4k
  fmt = (char *)(raw_fmt + POISON_START);
158
12.4k
  if (fmt_len) memcpy(fmt, data + 1, fmt_len);
159
12.4k
  fmt[fmt_len] = '\0';  /* always present, _str needs it, substr ignores it */
160
161
12.4k
  ASAN_POISON_MEMORY_REGION(raw_fmt, POISON_START);
162
12.4k
  ASAN_POISON_MEMORY_REGION(raw_fmt + POISON_START + fmt_len + 1, POISON_END);
163
164
  /*
165
   *  Tighten tmpl_rules: refuse unresolved attribute references
166
   *  at tokenize-time so we never reach eval with a half-resolved
167
   *  tree (which trips the per-node tmpl_needs_resolving assert
168
   *  in xlat_frame_eval, see xlat_eval.c:1475).
169
   */
170
12.4k
  t_rules = (tmpl_rules_t) {
171
12.4k
    .attr = (tmpl_attr_rules_t) {
172
12.4k
      .dict_def   = dict,
173
12.4k
      .list_def   = request_attr_request,
174
12.4k
      .allow_unresolved = false,
175
12.4k
      .allow_unknown    = false,
176
12.4k
      .allow_wildcard   = true,
177
12.4k
    },
178
12.4k
  };
179
180
12.4k
  switch (mode) {
181
5.37k
  case MODE_SUBSTR_BARE:
182
8.81k
  case MODE_SUBSTR_DOUBLE:
183
8.83k
  case MODE_SUBSTR_SINGLE:
184
11.7k
  case MODE_SUBSTR_BACK:
185
11.7k
  {
186
11.7k
    fr_token_t  quote;
187
11.7k
    fr_sbuff_t  sbuff;
188
189
11.7k
    switch (mode) {
190
0
    default:
191
5.37k
    case MODE_SUBSTR_BARE:
192
5.37k
      quote = T_BARE_WORD;
193
5.37k
      break;
194
195
3.44k
    case MODE_SUBSTR_DOUBLE:
196
3.44k
      quote = T_DOUBLE_QUOTED_STRING;
197
3.44k
      break;
198
199
15
    case MODE_SUBSTR_SINGLE:
200
15
      quote = T_SINGLE_QUOTED_STRING;
201
15
      break;
202
203
2.94k
    case MODE_SUBSTR_BACK:
204
2.94k
      quote = T_BACK_QUOTED_STRING;
205
2.94k
      break;
206
11.7k
    }
207
208
11.7k
    sbuff = FR_SBUFF_IN(fmt, fmt_len);
209
11.7k
    slen = tmpl_afrom_substr(ctx, &vpt, &sbuff, quote, NULL, &t_rules);
210
11.7k
    break;
211
11.7k
  }
212
213
689
  case MODE_ATTR_STR:
214
    /*
215
     *  The _str signature takes a length too, but its body
216
     *  uses strlen() internally for the substr it builds.
217
     *  Pass fmt_len so the explicit-length API stays
218
     *  honest, but the NUL we wrote at fmt[fmt_len] is what
219
     *  the function actually relies on.
220
     */
221
689
    slen = tmpl_afrom_attr_str(ctx, &attr_err, &vpt, fmt, &t_rules);
222
689
    break;
223
12.4k
  }
224
225
12.4k
  if (slen <= 0 || !vpt) goto done;
226
227
  /*
228
   *  Resolve walks the whole tmpl AST: request refs, attr
229
   *  refs, nested xlats inside TMPL_TYPE_XLAT. Most fuzzer
230
   *  inputs leave unresolved refs, but the resolution walk
231
   *  still exercises the code.
232
   */
233
2.51k
  if (tmpl_resolve(vpt, &tr_rules) < 0) goto done;
234
235
  /*
236
   *  Write the tmpl out.  This check exercises every
237
   *  per-type print branch in tmpl_tokenize.c (attr OID
238
   *  rendering, request-ref prefixing, xlat unparse via
239
   *  xlat_print, escape rules, etc.) and validates the tree
240
   *  is well-formed enough to serialise back to text.
241
   *
242
   *  We deliberately do not chase further (tmpl_eval / map_proc)
243
   *  for the same reason fuzzer_xlat doesn't: eval requires
244
   *  invariants that only a full compile pass establishes, and
245
   *  fuzzer-generated trees skip those.
246
   */
247
2.46k
  {
248
2.46k
    fr_sbuff_t  print_sb;
249
2.46k
    char    print_buf[1024];
250
251
2.46k
    print_sb = FR_SBUFF_OUT(print_buf, sizeof(print_buf));
252
2.46k
    (void) tmpl_print(&print_sb, vpt, NULL);
253
2.46k
  }
254
255
12.4k
done:
256
12.4k
  if (raw_fmt) {
257
12.4k
    ASAN_UNPOISON_MEMORY_REGION(raw_fmt, POISON_START + fmt_len + 1 + POISON_END);
258
12.4k
  }
259
260
  talloc_free(ctx);
261
12.4k
  fr_strerror_clear();
262
12.4k
  return 0;
263
2.46k
}