/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_raw_arg() - Stage a raw-vector input likewise |
9 | | * fuzz_repeat_product_excessive() - Guard against TRE repeat blowup |
10 | | * |
11 | | * Adapted from r-afl's fuzz.h for use with libFuzzer instead of AFL++. |
12 | | */ |
13 | | |
14 | | #ifndef FUZZ_COMMON_H |
15 | | #define FUZZ_COMMON_H |
16 | | |
17 | | #include <limits.h> |
18 | | #include <stdint.h> |
19 | | #include <stdlib.h> |
20 | | #include <string.h> |
21 | | #include <unistd.h> |
22 | | |
23 | | #define R_NO_REMAP 1 |
24 | | #include <Rembedded.h> |
25 | | #include <Rinternals.h> |
26 | | #include <Rinterface.h> /* R_SignalHandlers */ |
27 | | #include <R_ext/Parse.h> |
28 | | |
29 | | /* |
30 | | * Suppress R warnings globally. |
31 | | * |
32 | | * Many R functions emit warnings on invalid input (e.g., "NAs introduced |
33 | | * by coercion"). In persistent fuzzing, these accumulate and can overflow |
34 | | * R's warning buffer, triggering fatal errors. Setting warn = -1 |
35 | | * suppresses all warnings. |
36 | | */ |
37 | | static void fuzz_suppress_warnings(void) |
38 | 2 | { |
39 | 2 | int error = 0; |
40 | 2 | SEXP warn_call; |
41 | 2 | Rf_protect(warn_call = Rf_lang2(Rf_install("options"), |
42 | 2 | Rf_ScalarInteger(-1))); |
43 | 2 | SET_TAG(CDR(warn_call), Rf_install("warn")); |
44 | 2 | R_tryEval(warn_call, R_GlobalEnv, &error); |
45 | 2 | Rf_unprotect(1); |
46 | 2 | } |
47 | | |
48 | | /* |
49 | | * Set R_HOME based on the fuzzer binary's location. |
50 | | * |
51 | | * OSS-Fuzz places everything in $OUT. The build.sh installs R to |
52 | | * $OUT/r-install, so R_HOME is at $OUT/r-install/lib/R. We derive |
53 | | * this from /proc/self/exe so it works regardless of the mount path. |
54 | | */ |
55 | | static void fuzz_set_r_home(void) |
56 | 2 | { |
57 | | /* Skip if R_HOME is already set (e.g., for local testing) */ |
58 | 2 | if (getenv("R_HOME") != NULL) |
59 | 1 | return; |
60 | | |
61 | 1 | char exe[PATH_MAX]; |
62 | 1 | ssize_t len = readlink("/proc/self/exe", exe, sizeof(exe) - 1); |
63 | 1 | if (len <= 0) |
64 | 0 | return; |
65 | 1 | exe[len] = '\0'; |
66 | | |
67 | | /* Find the directory containing the binary */ |
68 | 1 | char *slash = strrchr(exe, '/'); |
69 | 1 | if (slash == NULL) |
70 | 0 | return; |
71 | 1 | *slash = '\0'; |
72 | | |
73 | 1 | char r_home[PATH_MAX]; |
74 | 1 | snprintf(r_home, sizeof(r_home), "%s/r-install/lib/R", exe); |
75 | 1 | setenv("R_HOME", r_home, 1); |
76 | 1 | } |
77 | | |
78 | | /* |
79 | | * Cap R's vector heap. |
80 | | * |
81 | | * Hostile input reaches R's allocator directly -- a serialized vector can |
82 | | * declare any length it likes, and coercion or scanning can be talked into |
83 | | * asking for one -- so a 64KB input can request gigabytes. |
84 | | * |
85 | | * R enforces R_MAX_VSIZE in allocVector and signals an ordinary R error |
86 | | * ("vector memory limit of N reached"). When it fires inside R_ToplevelExec |
87 | | * the iteration is discarded and fuzzing carries on; every per-iteration |
88 | | * allocation must therefore go through fuzz_set_string / fuzz_set_raw_arg |
89 | | * below, which run under such a context. That catchability is the reason |
90 | | * to prefer this over a sanitizer-level RSS limit: ASAN's soft_rss_limit_mb |
91 | | * either aborts the process or starts handing NULL back to allocators that |
92 | | * never expected it, and both look like crashes. |
93 | | * |
94 | | * Caveats: |
95 | | * - The cap bounds R's cumulative LIVE vector heap, not one input's |
96 | | * allocations. Interned symbols are never collected, so the parse and |
97 | | * unserialize targets ratchet toward the cap over a long run. |
98 | | * - It bounds R's vector heap ONLY. malloc-based allocations, like TRE |
99 | | * compiling a pattern, bypass it entirely; see |
100 | | * fuzz_repeat_product_excessive for that guard. |
101 | | * - It does cost coverage of code paths that need larger results. |
102 | | * Targets with legitimately large outputs (decompress: a 64KB bzip2 |
103 | | * stream of zeros expands past 1GB) raise the default by defining |
104 | | * FUZZ_R_MAX_VSIZE before including this header. |
105 | | * - R's suffix parsing is case-sensitive: "4Gb" works, "4gb" silently |
106 | | * leaves the heap uncapped (R only warns on stderr at startup). The |
107 | | * env override is for local runs; the CI runner does not forward |
108 | | * arbitrary environment variables. |
109 | | * |
110 | | * Local reproduction of vector-limit findings under plain Rscript needs |
111 | | * the same value exported; see README.md. |
112 | | */ |
113 | | #ifndef FUZZ_R_MAX_VSIZE |
114 | 1 | # define FUZZ_R_MAX_VSIZE "1Gb" |
115 | | #endif |
116 | | |
117 | | static void fuzz_set_vsize_limit(void) |
118 | 2 | { |
119 | 2 | if (getenv("R_MAX_VSIZE") == NULL) |
120 | 1 | setenv("R_MAX_VSIZE", FUZZ_R_MAX_VSIZE, 1); |
121 | 2 | } |
122 | | |
123 | | /* |
124 | | * Initialize R for fuzzing. Call once from LLVMFuzzerInitialize. |
125 | | * |
126 | | * Performs: |
127 | | * 1. Set R_HOME from binary location |
128 | | * 2. Cap the vector heap (must precede R's startup, which reads it) |
129 | | * 3. Initialize embedded R, without R's signal handlers |
130 | | * 4. Suppress warnings |
131 | | */ |
132 | | static void fuzz_init_r(void) |
133 | 2 | { |
134 | 2 | fuzz_set_r_home(); |
135 | 2 | fuzz_set_vsize_limit(); |
136 | | |
137 | | /* R's SIGSEGV/SIGILL/etc. handlers enter an interactive recovery |
138 | | * prompt instead of crashing, which would hang the fuzzer. Telling |
139 | | * R not to install them (rather than resetting to SIG_DFL after |
140 | | * init) keeps the sanitizers' own handlers in place, so wild-pointer |
141 | | * faults still get full ASAN reports and clean deduplication. */ |
142 | 2 | R_SignalHandlers = 0; |
143 | | |
144 | 2 | char *r_argv[] = {"R", "--vanilla", "--no-echo", "--no-restore"}; |
145 | 2 | int r_argc = sizeof(r_argv) / sizeof(r_argv[0]); |
146 | 2 | Rf_initEmbeddedR(r_argc, r_argv); |
147 | | |
148 | 2 | fuzz_suppress_warnings(); |
149 | 2 | } |
150 | | |
151 | | /* |
152 | | * Helper for protected evaluation via R_ToplevelExec. |
153 | | * |
154 | | * R API calls that touch R objects can longjmp on error. Without a |
155 | | * top-level context, R's error handler crashes. Wrapping calls in |
156 | | * R_ToplevelExec catches the longjmp cleanly. |
157 | | */ |
158 | | typedef struct { |
159 | | SEXP call; |
160 | | SEXP env; |
161 | | } fuzz_eval_data_t; |
162 | | |
163 | | static void fuzz_do_eval(void *data) |
164 | 7.56k | { |
165 | 7.56k | fuzz_eval_data_t *ed = (fuzz_eval_data_t *)data; |
166 | 7.56k | Rf_eval(ed->call, ed->env); |
167 | 7.56k | } |
168 | | |
169 | | /* |
170 | | * Stage per-iteration inputs under a toplevel context. |
171 | | * |
172 | | * Rf_mkChar and Rf_allocVector can themselves signal the vector-limit |
173 | | * error. Outside a toplevel context that error longjmps into a stack |
174 | | * frame that returned during Rf_initEmbeddedR -- undefined behaviour -- |
175 | | * so input staging must be wrapped just like evaluation. Both helpers |
176 | | * return FALSE when the allocation failed; skip the iteration then. |
177 | | */ |
178 | | typedef struct { |
179 | | SEXP vec; |
180 | | const char *str; |
181 | | } fuzz_string_data_t; |
182 | | |
183 | | static void fuzz_do_set_string(void *data) |
184 | 1.89k | { |
185 | 1.89k | fuzz_string_data_t *sd = (fuzz_string_data_t *)data; |
186 | 1.89k | SET_STRING_ELT(sd->vec, 0, Rf_mkChar(sd->str)); |
187 | 1.89k | } |
188 | | |
189 | | static Rboolean fuzz_set_string(SEXP vec, const char *str) |
190 | 1.89k | { |
191 | 1.89k | fuzz_string_data_t sd = { vec, str }; |
192 | 1.89k | return R_ToplevelExec(fuzz_do_set_string, &sd); |
193 | 1.89k | } |
194 | | |
195 | | typedef struct { |
196 | | SEXP call; |
197 | | const uint8_t *data; |
198 | | size_t size; |
199 | | } fuzz_raw_data_t; |
200 | | |
201 | | static void fuzz_do_set_raw(void *data) |
202 | 0 | { |
203 | 0 | fuzz_raw_data_t *rd = (fuzz_raw_data_t *)data; |
204 | 0 |
|
205 | 0 | /* The raw vector is reachable from the (protected) call as soon as |
206 | 0 | * SETCADR runs, and nothing allocates before the memcpy completes, |
207 | 0 | * so no extra protection is needed. */ |
208 | 0 | SEXP raw = Rf_allocVector(RAWSXP, (R_xlen_t)rd->size); |
209 | 0 | SETCADR(rd->call, raw); |
210 | 0 | memcpy(RAW(raw), rd->data, rd->size); |
211 | 0 | } |
212 | | |
213 | | static Rboolean fuzz_set_raw_arg(SEXP call, const uint8_t *data, size_t size) |
214 | 0 | { |
215 | 0 | fuzz_raw_data_t rd = { call, data, size }; |
216 | 0 | return R_ToplevelExec(fuzz_do_set_raw, &rd); |
217 | 0 | } |
218 | | |
219 | | /* |
220 | | * Guard against TRE's bounded-repeat blowup. |
221 | | * |
222 | | * TRE compiles bounded repeats by duplicating the pattern AST, so nested |
223 | | * counted repeats multiply: a 36-byte pattern like |
224 | | * (?:a{2,101,})(?:a{2,101,}){100}{100} expands to ~10^8 nodes and >8GB of |
225 | | * allocations at compile time. Those allocations are malloc, not R's |
226 | | * vector heap, so R_MAX_VSIZE cannot bound them; reject the pattern |
227 | | * before it reaches R instead. |
228 | | * |
229 | | * The product of all repeat bounds is a deliberate over-estimate -- |
230 | | * sequential (non-nested) repeats add rather than multiply -- trading a |
231 | | * little coverage of repeat-heavy patterns for a hard bound on compile |
232 | | * cost. |
233 | | */ |
234 | | #define FUZZ_MAX_REPEAT_PRODUCT 1000000.0 |
235 | | |
236 | | static int fuzz_repeat_product_excessive(const char *pattern) |
237 | 0 | { |
238 | 0 | double product = 1.0; |
239 | 0 |
|
240 | 0 | for (const char *p = pattern; *p != '\0'; p++) { |
241 | 0 | if (*p != '{') |
242 | 0 | continue; |
243 | 0 |
|
244 | 0 | /* The largest number inside the braces bounds this repeat. */ |
245 | 0 | unsigned long bound = 0, cur = 0; |
246 | 0 | int counted = 0; |
247 | 0 | const char *q = p + 1; |
248 | 0 | for (; *q != '\0' && *q != '}'; q++) { |
249 | 0 | if (*q >= '0' && *q <= '9') { |
250 | 0 | cur = cur * 10 + (unsigned long)(*q - '0'); |
251 | 0 | if (cur > 10000000) |
252 | 0 | cur = 10000000; /* saturate; already over any budget */ |
253 | 0 | counted = 1; |
254 | 0 | } else if (*q == ',') { |
255 | 0 | if (cur > bound) |
256 | 0 | bound = cur; |
257 | 0 | cur = 0; |
258 | 0 | } else { |
259 | 0 | counted = 0; /* not a counted repeat, e.g. "{a}" */ |
260 | 0 | break; |
261 | 0 | } |
262 | 0 | } |
263 | 0 | if (!counted || *q != '}') |
264 | 0 | continue; |
265 | 0 |
|
266 | 0 | if (cur > bound) |
267 | 0 | bound = cur; |
268 | 0 | if (bound > 1) |
269 | 0 | product *= (double)bound; |
270 | 0 | if (product > FUZZ_MAX_REPEAT_PRODUCT) |
271 | 0 | return 1; |
272 | 0 |
|
273 | 0 | p = q; |
274 | 0 | } |
275 | 0 |
|
276 | 0 | return 0; |
277 | 0 | } |
278 | | |
279 | | #endif /* FUZZ_COMMON_H */ |