/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 | } |