Coverage Report

Created: 2026-09-14 06:55

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_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
# 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
625
{
165
625
    fuzz_eval_data_t *ed = (fuzz_eval_data_t *)data;
166
625
    Rf_eval(ed->call, ed->env);
167
625
}
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
0
{
185
0
    fuzz_string_data_t *sd = (fuzz_string_data_t *)data;
186
0
    SET_STRING_ELT(sd->vec, 0, Rf_mkChar(sd->str));
187
0
}
188
189
static Rboolean fuzz_set_string(SEXP vec, const char *str)
190
0
{
191
0
    fuzz_string_data_t sd = { vec, str };
192
0
    return R_ToplevelExec(fuzz_do_set_string, &sd);
193
0
}
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
625
{
203
625
    fuzz_raw_data_t *rd = (fuzz_raw_data_t *)data;
204
205
    /* The raw vector is reachable from the (protected) call as soon as
206
     * SETCADR runs, and nothing allocates before the memcpy completes,
207
     * so no extra protection is needed. */
208
625
    SEXP raw = Rf_allocVector(RAWSXP, (R_xlen_t)rd->size);
209
625
    SETCADR(rd->call, raw);
210
625
    memcpy(RAW(raw), rd->data, rd->size);
211
625
}
212
213
static Rboolean fuzz_set_raw_arg(SEXP call, const uint8_t *data, size_t size)
214
625
{
215
625
    fuzz_raw_data_t rd = { call, data, size };
216
625
    return R_ToplevelExec(fuzz_do_set_raw, &rd);
217
625
}
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 */