/src/r-oss-fuzz/harnesses/common.h
Line | Count | Source |
1 | | /* |
2 | | * Shared helpers for R fuzzing harnesses (libFuzzer). |
3 | | * |
4 | | * Provides: |
5 | | * fuzz_suppress_warnings() - Suppress R warnings to avoid buffer overflow |
6 | | * fuzz_init_r() - Full R initialization sequence |
7 | | * fuzz_set_string() - Stage a string input under a toplevel context |
8 | | * fuzz_set_string_ce() - Stage a string with an explicit encoding mark |
9 | | * fuzz_set_raw_arg() - Stage a raw-vector input likewise |
10 | | * fuzz_make_wrapper() - Parse an R closure that implements a call slot |
11 | | * fuzz_scratch_file() - Per-process temp file for path-only entry points |
12 | | * fuzz_write_scratch() - Rewrite that file with the current input |
13 | | * fuzz_repeat_product_excessive() - Guard against TRE repeat blowup |
14 | | * |
15 | | * Adapted from r-afl's fuzz.h for use with libFuzzer instead of AFL++. |
16 | | */ |
17 | | |
18 | | #ifndef FUZZ_COMMON_H |
19 | | #define FUZZ_COMMON_H |
20 | | |
21 | | #include <limits.h> |
22 | | #include <stdint.h> |
23 | | #include <stdio.h> |
24 | | #include <stdlib.h> |
25 | | #include <string.h> |
26 | | #include <unistd.h> |
27 | | |
28 | | #define R_NO_REMAP 1 |
29 | | #include <Rembedded.h> |
30 | | #include <Rinternals.h> |
31 | | #include <Rinterface.h> /* R_SignalHandlers */ |
32 | | #include <R_ext/Parse.h> |
33 | | |
34 | | /* |
35 | | * Suppress R warnings globally. |
36 | | * |
37 | | * Many R functions emit warnings on invalid input (e.g., "NAs introduced |
38 | | * by coercion"). In persistent fuzzing, these accumulate and can overflow |
39 | | * R's warning buffer, triggering fatal errors. Setting warn = -1 |
40 | | * suppresses all warnings. |
41 | | */ |
42 | | static void fuzz_suppress_warnings(void) |
43 | 2 | { |
44 | 2 | int error = 0; |
45 | 2 | SEXP warn_call; |
46 | 2 | Rf_protect(warn_call = Rf_lang2(Rf_install("options"), |
47 | 2 | Rf_ScalarInteger(-1))); |
48 | 2 | SET_TAG(CDR(warn_call), Rf_install("warn")); |
49 | 2 | R_tryEval(warn_call, R_GlobalEnv, &error); |
50 | 2 | Rf_unprotect(1); |
51 | 2 | } |
52 | | |
53 | | /* |
54 | | * Set R_HOME based on the fuzzer binary's location. |
55 | | * |
56 | | * OSS-Fuzz places everything in $OUT. The build.sh installs R to |
57 | | * $OUT/r-install, so R_HOME is at $OUT/r-install/lib/R. We derive |
58 | | * this from /proc/self/exe so it works regardless of the mount path. |
59 | | */ |
60 | | static void fuzz_set_r_home(void) |
61 | 2 | { |
62 | | /* Skip if R_HOME is already set (e.g., for local testing) */ |
63 | 2 | if (getenv("R_HOME") != NULL) |
64 | 1 | return; |
65 | | |
66 | 1 | char exe[PATH_MAX]; |
67 | 1 | ssize_t len = readlink("/proc/self/exe", exe, sizeof(exe) - 1); |
68 | 1 | if (len <= 0) |
69 | 0 | return; |
70 | 1 | exe[len] = '\0'; |
71 | | |
72 | | /* Find the directory containing the binary */ |
73 | 1 | char *slash = strrchr(exe, '/'); |
74 | 1 | if (slash == NULL) |
75 | 0 | return; |
76 | 1 | *slash = '\0'; |
77 | | |
78 | 1 | char r_home[PATH_MAX]; |
79 | 1 | snprintf(r_home, sizeof(r_home), "%s/r-install/lib/R", exe); |
80 | 1 | setenv("R_HOME", r_home, 1); |
81 | 1 | } |
82 | | |
83 | | /* |
84 | | * Cap R's vector heap. |
85 | | * |
86 | | * Hostile input reaches R's allocator directly -- a serialized vector can |
87 | | * declare any length it likes, and coercion or scanning can be talked into |
88 | | * asking for one -- so a 64KB input can request gigabytes. |
89 | | * |
90 | | * R enforces R_MAX_VSIZE in allocVector and signals an ordinary R error |
91 | | * ("vector memory limit of N reached"). When it fires inside R_ToplevelExec |
92 | | * the iteration is discarded and fuzzing carries on; every per-iteration |
93 | | * allocation must therefore go through fuzz_set_string / fuzz_set_raw_arg |
94 | | * below, which run under such a context. That catchability is the reason |
95 | | * to prefer this over a sanitizer-level RSS limit: ASAN's soft_rss_limit_mb |
96 | | * either aborts the process or starts handing NULL back to allocators that |
97 | | * never expected it, and both look like crashes. |
98 | | * |
99 | | * Caveats: |
100 | | * - The cap bounds R's cumulative LIVE vector heap, not one input's |
101 | | * allocations. Interned symbols are never collected, so the parse and |
102 | | * unserialize targets ratchet toward the cap over a long run. |
103 | | * - It bounds R's vector heap ONLY. malloc-based allocations, like TRE |
104 | | * compiling a pattern, bypass it entirely; see |
105 | | * fuzz_repeat_product_excessive for that guard. |
106 | | * - It does cost coverage of code paths that need larger results. |
107 | | * Targets with legitimately large outputs (decompress: a 64KB bzip2 |
108 | | * stream of zeros expands past 1GB) raise the default by defining |
109 | | * FUZZ_R_MAX_VSIZE before including this header. |
110 | | * - R's suffix parsing is case-sensitive: "4Gb" works, "4gb" silently |
111 | | * leaves the heap uncapped (R only warns on stderr at startup). The |
112 | | * env override is for local runs; the CI runner does not forward |
113 | | * arbitrary environment variables. |
114 | | * |
115 | | * Local reproduction of vector-limit findings under plain Rscript needs |
116 | | * the same value exported; see README.md. |
117 | | */ |
118 | | #ifndef FUZZ_R_MAX_VSIZE |
119 | 1 | # define FUZZ_R_MAX_VSIZE "1Gb" |
120 | | #endif |
121 | | |
122 | | static void fuzz_set_vsize_limit(void) |
123 | 2 | { |
124 | 2 | if (getenv("R_MAX_VSIZE") == NULL) |
125 | 1 | setenv("R_MAX_VSIZE", FUZZ_R_MAX_VSIZE, 1); |
126 | 2 | } |
127 | | |
128 | | /* |
129 | | * Initialize R for fuzzing. Call once from LLVMFuzzerInitialize. |
130 | | * |
131 | | * Performs: |
132 | | * 1. Set R_HOME from binary location |
133 | | * 2. Cap the vector heap (must precede R's startup, which reads it) |
134 | | * 3. Initialize embedded R, without R's signal handlers |
135 | | * 4. Suppress warnings |
136 | | */ |
137 | | static void fuzz_init_r(void) |
138 | 2 | { |
139 | 2 | fuzz_set_r_home(); |
140 | 2 | fuzz_set_vsize_limit(); |
141 | | |
142 | | /* R's SIGSEGV/SIGILL/etc. handlers enter an interactive recovery |
143 | | * prompt instead of crashing, which would hang the fuzzer. Telling |
144 | | * R not to install them (rather than resetting to SIG_DFL after |
145 | | * init) keeps the sanitizers' own handlers in place, so wild-pointer |
146 | | * faults still get full ASAN reports and clean deduplication. */ |
147 | 2 | R_SignalHandlers = 0; |
148 | | |
149 | 2 | char *r_argv[] = {"R", "--vanilla", "--no-echo", "--no-restore"}; |
150 | 2 | int r_argc = sizeof(r_argv) / sizeof(r_argv[0]); |
151 | 2 | Rf_initEmbeddedR(r_argc, r_argv); |
152 | | |
153 | 2 | fuzz_suppress_warnings(); |
154 | 2 | } |
155 | | |
156 | | /* |
157 | | * Helper for protected evaluation via R_ToplevelExec. |
158 | | * |
159 | | * R API calls that touch R objects can longjmp on error. Without a |
160 | | * top-level context, R's error handler crashes. Wrapping calls in |
161 | | * R_ToplevelExec catches the longjmp cleanly. |
162 | | */ |
163 | | typedef struct { |
164 | | SEXP call; |
165 | | SEXP env; |
166 | | } fuzz_eval_data_t; |
167 | | |
168 | | static void fuzz_do_eval(void *data) |
169 | 0 | { |
170 | 0 | fuzz_eval_data_t *ed = (fuzz_eval_data_t *)data; |
171 | 0 | Rf_eval(ed->call, ed->env); |
172 | 0 | } |
173 | | |
174 | | /* Evaluate malformed-input-heavy targets without printing each expected |
175 | | * R error. R_tryEvalSilent installs its own top-level error context. */ |
176 | | static void fuzz_eval_silent(SEXP call, SEXP env) |
177 | 4.65k | { |
178 | 4.65k | int error = 0; |
179 | 4.65k | R_tryEvalSilent(call, env, &error); |
180 | 4.65k | } |
181 | | |
182 | | /* |
183 | | * Stage per-iteration inputs under a toplevel context. |
184 | | * |
185 | | * Rf_mkChar and Rf_allocVector can themselves signal the vector-limit |
186 | | * error. Outside a toplevel context that error longjmps into a stack |
187 | | * frame that returned during Rf_initEmbeddedR -- undefined behaviour -- |
188 | | * so input staging must be wrapped just like evaluation. Both helpers |
189 | | * return FALSE when the allocation failed; skip the iteration then. |
190 | | */ |
191 | | typedef struct { |
192 | | SEXP vec; |
193 | | const char *str; |
194 | | } fuzz_string_data_t; |
195 | | |
196 | | static void fuzz_do_set_string(void *data) |
197 | 5.53k | { |
198 | 5.53k | fuzz_string_data_t *sd = (fuzz_string_data_t *)data; |
199 | 5.53k | SET_STRING_ELT(sd->vec, 0, Rf_mkChar(sd->str)); |
200 | 5.53k | } |
201 | | |
202 | | static Rboolean fuzz_set_string(SEXP vec, const char *str) |
203 | 5.53k | { |
204 | 5.53k | fuzz_string_data_t sd = { vec, str }; |
205 | 5.53k | return R_ToplevelExec(fuzz_do_set_string, &sd); |
206 | 5.53k | } |
207 | | |
208 | | /* |
209 | | * Parse and evaluate one R function literal, once, at initialization. |
210 | | * |
211 | | * Harnesses whose calls need named arguments, on.exit(close(con)) or a |
212 | | * few R-level steps around the entry point express each slot as an R |
213 | | * closure and call it with Rf_lang*; that keeps the C side free of |
214 | | * argument plumbing. The source is harness-authored (never fuzz |
215 | | * input), so a parse or evaluation failure is a harness bug: abort. |
216 | | */ |
217 | | static SEXP fuzz_make_wrapper(const char *source) |
218 | 12 | { |
219 | 12 | ParseStatus status; |
220 | 12 | SEXP text, parsed, wrapper; |
221 | 12 | Rf_protect(text = Rf_mkString(source)); |
222 | 12 | Rf_protect(parsed = R_ParseVector(text, -1, &status, R_NilValue)); |
223 | 12 | if (status != PARSE_OK || XLENGTH(parsed) != 1) |
224 | 0 | abort(); |
225 | | |
226 | 12 | wrapper = Rf_eval(VECTOR_ELT(parsed, 0), R_BaseEnv); |
227 | 12 | Rf_unprotect(2); |
228 | 12 | return wrapper; |
229 | 12 | } |
230 | | |
231 | | /* |
232 | | * fuzz_set_string with an explicit encoding mark. |
233 | | * |
234 | | * R does not validate the mark (Encoding<- lets users set it on any |
235 | | * bytes), so handing the same payload over as UTF-8 or latin1 reaches |
236 | | * the multibyte branches and their handling of invalid sequences |
237 | | * without touching the process locale. |
238 | | */ |
239 | | typedef struct { |
240 | | SEXP vec; |
241 | | const char *str; |
242 | | cetype_t ce; |
243 | | } fuzz_ce_string_data_t; |
244 | | |
245 | | static void fuzz_do_set_string_ce(void *data) |
246 | 0 | { |
247 | 0 | fuzz_ce_string_data_t *sd = (fuzz_ce_string_data_t *)data; |
248 | 0 | SET_STRING_ELT(sd->vec, 0, Rf_mkCharCE(sd->str, sd->ce)); |
249 | 0 | } |
250 | | |
251 | | static Rboolean fuzz_set_string_ce(SEXP vec, const char *str, cetype_t ce) |
252 | 0 | { |
253 | 0 | fuzz_ce_string_data_t sd = { vec, str, ce }; |
254 | 0 | return R_ToplevelExec(fuzz_do_set_string_ce, &sd); |
255 | 0 | } |
256 | | |
257 | | typedef struct { |
258 | | SEXP call; |
259 | | const uint8_t *data; |
260 | | size_t size; |
261 | | } fuzz_raw_data_t; |
262 | | |
263 | | static void fuzz_do_set_raw(void *data) |
264 | 0 | { |
265 | 0 | fuzz_raw_data_t *rd = (fuzz_raw_data_t *)data; |
266 | 0 |
|
267 | 0 | /* The raw vector is reachable from the (protected) call as soon as |
268 | 0 | * SETCADR runs, and nothing allocates before the memcpy completes, |
269 | 0 | * so no extra protection is needed. */ |
270 | 0 | SEXP raw = Rf_allocVector(RAWSXP, (R_xlen_t)rd->size); |
271 | 0 | SETCADR(rd->call, raw); |
272 | 0 | memcpy(RAW(raw), rd->data, rd->size); |
273 | 0 | } |
274 | | |
275 | | static Rboolean fuzz_set_raw_arg(SEXP call, const uint8_t *data, size_t size) |
276 | 0 | { |
277 | 0 | fuzz_raw_data_t rd = { call, data, size }; |
278 | 0 | return R_ToplevelExec(fuzz_do_set_raw, &rd); |
279 | 0 | } |
280 | | |
281 | | /* |
282 | | * Stage the input in a scratch file. |
283 | | * |
284 | | * Some R entry points only take a path (load() of a pre-1.4 save file, |
285 | | * unzip(), file(encoding=)), so those targets rewrite one per-process |
286 | | * file each iteration instead of handing R a raw vector. The file lives |
287 | | * in the system temp directory, which OSS-Fuzz runners allow writes to, |
288 | | * and is created once in LLVMFuzzerInitialize so fuzzing never pays a |
289 | | * mkstemp per input. Returns a malloc'd path, or NULL on failure. |
290 | | */ |
291 | | static char *fuzz_scratch_file(const char *name) |
292 | 0 | { |
293 | 0 | const char *tmpdir = getenv("TMPDIR"); |
294 | 0 | if (tmpdir == NULL || *tmpdir == '\0') |
295 | 0 | tmpdir = "/tmp"; |
296 | 0 |
|
297 | 0 | char *path = malloc(PATH_MAX); |
298 | 0 | if (path == NULL) |
299 | 0 | return NULL; |
300 | 0 | snprintf(path, PATH_MAX, "%s/fuzz-%s-XXXXXX", tmpdir, name); |
301 | 0 |
|
302 | 0 | int fd = mkstemp(path); |
303 | 0 | if (fd < 0) { |
304 | 0 | free(path); |
305 | 0 | return NULL; |
306 | 0 | } |
307 | 0 | close(fd); |
308 | 0 | return path; |
309 | 0 | } |
310 | | |
311 | | /* Replace the scratch file's contents. Returns FALSE on any I/O error so |
312 | | * the iteration can be skipped rather than fuzzing a stale file. */ |
313 | | static Rboolean fuzz_write_scratch(const char *path, const uint8_t *data, |
314 | | size_t size) |
315 | 0 | { |
316 | 0 | FILE *fp = fopen(path, "wb"); |
317 | 0 | if (fp == NULL) |
318 | 0 | return FALSE; |
319 | 0 |
|
320 | 0 | size_t written = size > 0 ? fwrite(data, 1, size, fp) : 0; |
321 | 0 | int rc = fclose(fp); |
322 | 0 | return written == size && rc == 0; |
323 | 0 | } |
324 | | |
325 | | /* |
326 | | * Guard against TRE's bounded-repeat blowup. |
327 | | * |
328 | | * TRE compiles bounded repeats by duplicating the pattern AST, so nested |
329 | | * counted repeats multiply: a 36-byte pattern like |
330 | | * (?:a{2,101,})(?:a{2,101,}){100}{100} expands to ~10^8 nodes and >8GB of |
331 | | * allocations at compile time. Those allocations are malloc, not R's |
332 | | * vector heap, so R_MAX_VSIZE cannot bound them; reject the pattern |
333 | | * before it reaches R instead. |
334 | | * |
335 | | * The product of all repeat bounds is a deliberate over-estimate -- |
336 | | * sequential (non-nested) repeats add rather than multiply -- trading a |
337 | | * little coverage of repeat-heavy patterns for a hard bound on compile |
338 | | * cost. |
339 | | */ |
340 | | #define FUZZ_MAX_REPEAT_PRODUCT 1000000.0 |
341 | | |
342 | | static int fuzz_repeat_product_excessive(const char *pattern) |
343 | 0 | { |
344 | 0 | double product = 1.0; |
345 | 0 |
|
346 | 0 | for (const char *p = pattern; *p != '\0'; p++) { |
347 | 0 | if (*p != '{') |
348 | 0 | continue; |
349 | 0 |
|
350 | 0 | /* The largest number inside the braces bounds this repeat. */ |
351 | 0 | unsigned long bound = 0, cur = 0; |
352 | 0 | int counted = 0; |
353 | 0 | const char *q = p + 1; |
354 | 0 | for (; *q != '\0' && *q != '}'; q++) { |
355 | 0 | if (*q >= '0' && *q <= '9') { |
356 | 0 | cur = cur * 10 + (unsigned long)(*q - '0'); |
357 | 0 | if (cur > 10000000) |
358 | 0 | cur = 10000000; /* saturate; already over any budget */ |
359 | 0 | counted = 1; |
360 | 0 | } else if (*q == ',') { |
361 | 0 | if (cur > bound) |
362 | 0 | bound = cur; |
363 | 0 | cur = 0; |
364 | 0 | } else { |
365 | 0 | counted = 0; /* not a counted repeat, e.g. "{a}" */ |
366 | 0 | break; |
367 | 0 | } |
368 | 0 | } |
369 | 0 | if (!counted || *q != '}') |
370 | 0 | continue; |
371 | 0 |
|
372 | 0 | if (cur > bound) |
373 | 0 | bound = cur; |
374 | 0 | if (bound > 1) |
375 | 0 | product *= (double)bound; |
376 | 0 | if (product > FUZZ_MAX_REPEAT_PRODUCT) |
377 | 0 | return 1; |
378 | 0 |
|
379 | 0 | p = q; |
380 | 0 | } |
381 | 0 |
|
382 | 0 | return 0; |
383 | 0 | } |
384 | | |
385 | | #endif /* FUZZ_COMMON_H */ |