Coverage Report

Created: 2026-10-16 07:06

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
6.96k
{
178
6.96k
    int error = 0;
179
6.96k
    R_tryEvalSilent(call, env, &error);
180
6.96k
}
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
0
{
198
0
    fuzz_string_data_t *sd = (fuzz_string_data_t *)data;
199
0
    SET_STRING_ELT(sd->vec, 0, Rf_mkChar(sd->str));
200
0
}
201
202
static Rboolean fuzz_set_string(SEXP vec, const char *str)
203
0
{
204
0
    fuzz_string_data_t sd = { vec, str };
205
0
    return R_ToplevelExec(fuzz_do_set_string, &sd);
206
0
}
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
44
{
219
44
    ParseStatus status;
220
44
    SEXP text, parsed, wrapper;
221
44
    Rf_protect(text = Rf_mkString(source));
222
44
    Rf_protect(parsed = R_ParseVector(text, -1, &status, R_NilValue));
223
44
    if (status != PARSE_OK || XLENGTH(parsed) != 1)
224
0
        abort();
225
226
44
    wrapper = Rf_eval(VECTOR_ELT(parsed, 0), R_BaseEnv);
227
44
    Rf_unprotect(2);
228
44
    return wrapper;
229
44
}
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 */