Coverage Report

Created: 2026-10-28 06:40

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/r-oss-fuzz/harnesses/eval.c
Line
Count
Source
1
/*
2
 * Evaluate R source in a fresh environment with selected primitives only.
3
 *
4
 * The parse target stops at R_ParseVector; this one runs what it parsed,
5
 * which is where the interpreter core lives (eval.c, envir.c, subset.c,
6
 * subassign.c, coerce.c, arithmetic.c).  Each input is evaluated in a new
7
 * environment whose locked parent holds a small allowlist of primitives,
8
 * so the input can define closures, build and index vectors, loop and
9
 * branch, but cannot reach I/O, native calls, namespaces, reflection or
10
 * process-global state.
11
 *
12
 * The allowlist is not a security sandbox -- it cannot contain a bug in
13
 * the interpreter -- and does not need to be: the fuzzing host is
14
 * disposable.  It exists so that fuzzing cycles go to the evaluator rather
15
 * than the filesystem, and so that every finding is a crash reachable from
16
 * ordinary R code.  That second property is what makes a report from this
17
 * target unambiguous: nothing here needs .Internal, body<- or any other
18
 * "you asked for it" entry point.
19
 *
20
 * Each top-level expression is byte-compiled before it runs, so what is
21
 * fuzzed is the byte-code interpreter with the AST interpreter behind it,
22
 * the same mix a session runs; see eval_compile.
23
 */
24
25
/* 8 MiB, not common.h's 1 GiB: the budget below is counted in evaluations,
26
 * and one evaluation can do a whole vector's worth of work, so the vector
27
 * heap is what caps the work of any one evaluation.  Base R itself needs
28
 * about 1 MiB of it.  R ignores a maximum below its initial heap (64 MiB),
29
 * so fuzz_target_init lowers R_VSIZE to match. */
30
2
#define FUZZ_R_MAX_VSIZE "8Mb"
31
#include "common.h"
32
33
#include <time.h>
34
#include <R_ext/eventloop.h>   /* R_PolledEvents */
35
36
18.3k
#define FUZZ_MAX_INPUT 8192
37
877k
#define FUZZ_MAX_DEPTH 64
38
1.27M
#define FUZZ_MAX_NODES 1024
39
40
/* Interrupt checks one input may make before it is cut off.  eval() checks
41
 * for interrupts once per 10 evaluations (instrumentation/0001; stock R:
42
 * once per 1000) and C loops that call R_CheckUserInterrupt add
43
 * their own, so this is roughly 200k evaluations: well under a second even
44
 * with sanitizers, and deterministic, which a wall-clock limit is not.
45
 * See eval_poll_budget. */
46
8.02k
#define FUZZ_EVAL_BUDGET 20000
47
48
/* CPU seconds an input may use before the next budget check cuts it off,
49
 * whatever the budget says.  A backstop for vector-heavy loops, where the
50
 * budget's unit of work is too coarse; see eval_poll_budget. */
51
3.94M
#define FUZZ_EVAL_CPU_LIMIT 1.0
52
53
/* The allowlist frame, the wrappers frame below it, and compiler::compile. */
54
static SEXP eval_builtins, eval_wrappers, eval_compile_fn;
55
56
/* Remaining interrupt checks for the current input; 0 means no limit, which
57
 * is also how eval_compile pauses the count. */
58
static long eval_budget;
59
static clock_t eval_cpu_start;
60
61
/* Only primitives: importing a base closure would also import its enclosing
62
 * namespace and the capabilities reachable through its implementation.
63
 * No class/attribute setters or reflection/metaprogramming APIs are exposed.
64
 * Closures created by the input inherit only the restricted environment.
65
 * `:` is provided separately, through a length-capped wrapper (below).
66
 */
67
static const char *const eval_primitives[] = {
68
    "{", "(", "if", "for", "while", "repeat", "break", "next",
69
    "function", "return", "<-", "=",
70
    "+", "-", "*", "/", "^", "%%", "%/%",
71
    "==", "!=", "<", "<=", ">", ">=", "!", "&", "|", "&&", "||",
72
    "c", "list", "[", "[[", "$", "[<-", "[[<-", "$<-",
73
    "length", "is.null", "is.logical", "is.integer", "is.double",
74
    "is.complex", "is.character", "is.list", "is.na", "is.nan", "is.finite",
75
    "is.infinite", "as.integer", "as.double", "as.logical", "as.character",
76
    "as.complex", "as.raw", "abs", "sqrt", "floor", "ceiling", "trunc",
77
    "sum", "prod", "min", "max", "any", "all", "invisible"
78
};
79
80
/*
81
 * `:` is wrapped rather than imported.  A compact ALTREP sequence costs
82
 * nothing to create whatever its length, and the reductions without an
83
 * ALTREP shortcut (prod, or min/max/sum with two arguments) then walk
84
 * 1e9 elements in C without an interrupt check: 8-18 s under ASAN, which
85
 * the budget cannot see and ClusterFuzz would file as a timeout.  Nothing
86
 * else in the allowlist builds a long vector without allocating it, and
87
 * allocation is bounded by the vector heap.
88
 *
89
 * The wrapper's enclosure holds only what its definition and body name
90
 * (`function` included: it is a primitive looked up like any other), so
91
 * it adds no capability.  .Internal is bound there to raise the error,
92
 * and nowhere the input can see.
93
 *
94
 * The wrapper is bound in eval_wrappers, a frame of its own below the
95
 * allowlist: the compiler inlines calls to names it finds in the base-like
96
 * frame, and the COLON instruction would bypass the cap (see eval_compile).
97
 */
98
static const char *const eval_wrapper_primitives[] = {
99
    "function", "{", "if", "&&", "==", ">=", "-", "length", "is.finite", "abs",
100
    ".Internal"
101
};
102
103
static const char *const eval_colon_source =
104
    "function(from, to) {\n"
105
    "    if (length(from) == 1L && length(to) == 1L &&\n"
106
    "        is.finite(from) && is.finite(to) && abs(to - from) >= 1e6)\n"
107
    "        .Internal(stop(TRUE, \"sequence too long for the eval target\"))\n"
108
    "    .colon(from, to)\n"
109
    "}";
110
111
typedef struct {
112
    const uint8_t *data;
113
    size_t size;
114
} eval_input_t;
115
116
/*
117
 * Execution budget, counted in interrupt checks.
118
 *
119
 * R_CheckUserInterrupt() -> R_ProcessEvents() -> R_PolledEvents() is the
120
 * hook R offers for event loops, and it is reached from the same places
121
 * that honour setTimeLimit(): every 10th eval() of the AST interpreter
122
 * (every 1000th in stock R; instrumentation/0001 lowers it),
123
 * the byte-code loop back-edges, and the long-running C loops that poll
124
 * for interrupts.  Counting those calls instead of CPU time gives every
125
 * input the same budget on every machine, so an input that is cut off
126
 * under ASAN on a slow bot is cut off the same way in a local reproduction,
127
 * and libFuzzer's coverage feedback stops depending on timing.
128
 *
129
 * Exactness is to within one check interval: eval()'s counter is static
130
 * and carries over between inputs.
131
 *
132
 * The unit is an evaluation, and an evaluation can be `!x` over the whole
133
 * heap: with a 64 MiB heap and a check every 1000 evaluations,
134
 * `x <- !1:4e4; repeat !x` ran 15 s under ASAN before the budget was
135
 * spent, and would have been filed as a timeout.  Three things bound that.
136
 * The heap is small (8 MiB), which caps the work of any one evaluation.
137
 * The check interval is short (instrumentation/0001), which caps the number of
138
 * evaluations between two checks.  And a CPU backstop ends the input at
139
 * the first check after FUZZ_EVAL_CPU_LIMIT seconds, whatever the budget
140
 * says.  The backstop is the one timing-dependent limit left, and it only
141
 * decides whether a vector-heavy input completes or errors, never whether
142
 * it crashes; it sits far below ClusterFuzz's timeout threshold so that
143
 * difference stays out of the tracker.
144
 *
145
 * The error unwinds to R_tryEvalSilent like any other R error.  Raising it
146
 * from inside R_ProcessEvents is what R_CheckTimeLimits does, so the
147
 * interpreter is prepared for a longjmp at this point.  The budget is
148
 * zero by the time either error is raised, which switches the check off,
149
 * so nothing on the error path can re-enter it.
150
 */
151
static void eval_poll_budget(void)
152
9.16M
{
153
9.16M
    if (eval_budget <= 0)
154
5.22M
        return;
155
156
3.94M
    if (--eval_budget == 0)
157
168
        Rf_error("evaluation budget exhausted");
158
159
3.94M
    if ((double)(clock() - eval_cpu_start) / CLOCKS_PER_SEC > FUZZ_EVAL_CPU_LIMIT) {
160
34
        eval_budget = 0;
161
34
        Rf_error("evaluation CPU limit exhausted");
162
34
    }
163
3.94M
}
164
165
/* The allowlist is only ever primitives (see eval_primitives): a name that
166
 * stops being one would silently import a closure and its namespace. */
167
static SEXP eval_primitive(const char *name)
168
160
{
169
160
    SEXP value = Rf_findFun(Rf_install(name), R_BaseEnv);
170
160
    if (TYPEOF(value) != BUILTINSXP && TYPEOF(value) != SPECIALSXP) {
171
0
        fprintf(stderr, "eval: %s is no longer a primitive\n", name);
172
0
        abort();
173
0
    }
174
160
    return value;
175
160
}
176
177
static void eval_import_primitives(SEXP env, const char *const *names, size_t n)
178
4
{
179
162
    for (size_t i = 0; i < n; i++)
180
158
        Rf_defineVar(Rf_install(names[i]), eval_primitive(names[i]), env);
181
4
}
182
183
/* Parse and evaluate one expression of set-up code.  A failure here is a
184
 * harness bug, so it stops the process rather than becoming an R error. */
185
static SEXP eval_run_init(const char *source, SEXP env)
186
4
{
187
4
    ParseStatus status;
188
4
    SEXP text, parsed, value;
189
4
    int error = 0;
190
191
4
    Rf_protect(text = Rf_mkString(source));
192
4
    Rf_protect(parsed = R_ParseVector(text, -1, &status, R_NilValue));
193
4
    if (status != PARSE_OK || XLENGTH(parsed) != 1)
194
0
        abort();
195
196
4
    value = R_tryEvalSilent(VECTOR_ELT(parsed, 0), env, &error);
197
4
    if (error) {
198
0
        fprintf(stderr, "eval: set-up failed: %s", R_curErrorBuf());
199
0
        abort();
200
0
    }
201
202
4
    Rf_unprotect(2);
203
4
    return value;
204
4
}
205
206
static int eval_valid_utf8(const uint8_t *data, size_t size)
207
9.19k
{
208
9.19k
    size_t i = 0;
209
1.88M
    while (i < size) {
210
1.87M
        uint8_t c = data[i++];
211
1.87M
        if (c < 0x80)
212
1.85M
            continue;
213
23.1k
        if (c >= 0xc2 && c <= 0xdf) {
214
18.5k
            if (i >= size || (data[i++] & 0xc0) != 0x80)
215
2
                return 0;
216
18.5k
            continue;
217
18.5k
        }
218
4.58k
        if (c >= 0xe0 && c <= 0xef) {
219
3.84k
            if (i + 1 >= size || (data[i] & 0xc0) != 0x80 ||
220
3.84k
                (data[i + 1] & 0xc0) != 0x80 ||
221
3.83k
                (c == 0xe0 && data[i] < 0xa0) ||
222
3.83k
                (c == 0xed && data[i] >= 0xa0))
223
8
                return 0;
224
3.83k
            i += 2;
225
3.83k
            continue;
226
3.84k
        }
227
740
        if (c >= 0xf0 && c <= 0xf4) {
228
725
            if (i + 2 >= size || (data[i] & 0xc0) != 0x80 ||
229
722
                (data[i + 1] & 0xc0) != 0x80 ||
230
720
                (data[i + 2] & 0xc0) != 0x80 ||
231
716
                (c == 0xf0 && data[i] < 0x90) ||
232
715
                (c == 0xf4 && data[i] >= 0x90))
233
12
                return 0;
234
713
            i += 3;
235
713
            continue;
236
725
        }
237
15
        return 0;
238
740
    }
239
9.15k
    return 1;
240
9.19k
}
241
242
/* Bound the post-parse walk, including wide argument lists. This limits test
243
 * complexity, not execution time: even a tiny expression can loop forever.
244
 */
245
static int eval_tree_within_budget(SEXP x, unsigned depth, unsigned *nodes)
246
438k
{
247
438k
    if (depth > FUZZ_MAX_DEPTH || ++*nodes > FUZZ_MAX_NODES)
248
24
        return 0;
249
250
438k
    switch (TYPEOF(x)) {
251
157k
    case LANGSXP:
252
157k
    case LISTSXP:
253
572k
        for (SEXP p = x; p != R_NilValue; p = CDR(p)) {
254
416k
            if (++*nodes > FUZZ_MAX_NODES ||
255
416k
                !eval_tree_within_budget(CAR(p), depth + 1, nodes))
256
1.74k
                return 0;
257
416k
        }
258
155k
        return 1;
259
7.95k
    case EXPRSXP:
260
21.7k
        for (R_xlen_t i = 0; i < XLENGTH(x); i++) {
261
13.8k
            if (!eval_tree_within_budget(VECTOR_ELT(x, i), depth + 1, nodes))
262
50
                return 0;
263
13.8k
        }
264
7.90k
        return 1;
265
273k
    default:
266
273k
        return 1;
267
438k
    }
268
438k
}
269
270
/*
271
 * Byte-code.
272
 *
273
 * Nearly all R code that runs in a session is byte-compiled: packages at
274
 * install time, everything else by the JIT after a call or two.  The JIT
275
 * is not used here (R_ENABLE_JIT=0): it compiles on its own schedule,
276
 * keeps a cache across inputs, and catches errors inside the compiler,
277
 * where a budget error would be swallowed and the budget left switched
278
 * off.  Instead the harness compiles each top-level expression itself,
279
 * with the budget paused, and evaluates the result.  Closures the input
280
 * defines are created with compiled bodies (MAKECLOSURE), top-level loops
281
 * are compiled too, and anything the compiler declines runs through the
282
 * AST interpreter, which is what the JIT falls back to as well.  Compile
283
 * time is bounded by the parsed-tree limits; constant folding evaluates
284
 * base functions on literals only.
285
 *
286
 * The compiler (compiler:::cmp.R) needs two things from the sandbox.  It
287
 * refuses an environment whose chain does not end in a frame topenv()
288
 * recognises, and it inlines base syntax and arithmetic only for names it
289
 * finds bound in a frame it classifies as base, by the frame's "name"
290
 * attribute.  So eval_builtins carries the name of base's package frame.
291
 * Its bindings are base's own primitives, so the BASEGUARD check that
292
 * compiled code makes before an inlined builtin call passes, and a name
293
 * the input rebinds is looked up like any other.  isBaseVar, which gates
294
 * whether break/next compile to a jump or to a loop context, compares
295
 * frames by identity instead, so it is rebound to accept the named frame;
296
 * without that every loop with a conditional break would compile with a
297
 * context, which a session's would not.
298
 */
299
static const char *const eval_compiler_setup =
300
    "local({\n"
301
    "    ns <- asNamespace(\"compiler\")\n"
302
    "    isBaseVar <- function(var, cntxt) {\n"
303
    "        info <- getInlineInfo(var, cntxt)\n"
304
    "        !is.null(info) &&\n"
305
    "            (identical(info$frame, .BaseNamespaceEnv) ||\n"
306
    "             identical(info$frame, baseenv()) ||\n"
307
    "             packFrameName(info$frame) == \"base\")\n"
308
    "    }\n"
309
    "    environment(isBaseVar) <- ns\n"
310
    "    unlockBinding(\"isBaseVar\", ns)\n"
311
    "    assign(\"isBaseVar\", isBaseVar, envir = ns)\n"
312
    "    lockBinding(\"isBaseVar\", ns)\n"
313
    "    compiler::compile\n"
314
    "})";
315
316
/* Run once at start-up, so the compiler's lazily loaded internals are in
317
 * memory before the first input and every input sees the same R. */
318
static const char *const eval_warmup_source =
319
    "f <- function(n) {\n"
320
    "    s <- 0\n"
321
    "    for (i in 1:n) if (i %% 2 == 0) s <- s + i else next\n"
322
    "    s\n"
323
    "}\n"
324
    "x <- c(f(10), 1)\n"
325
    "x[[2]] <- x[1]\n"
326
    "g <- function(...) list(...)\n"
327
    "g(x, repeat break)\n";
328
329
/* Byte-code for expr, or expr itself if the compiler declined it. */
330
static SEXP eval_compile(SEXP expr, SEXP env)
331
11.7k
{
332
11.7k
    SEXP quoted, call, code;
333
11.7k
    int error = 0;
334
11.7k
    long budget = eval_budget;
335
336
11.7k
    Rf_protect(quoted = Rf_lang2(R_QuoteSymbol, expr));
337
11.7k
    Rf_protect(call = Rf_lang3(eval_compile_fn, quoted, env));
338
11.7k
    eval_budget = 0;
339
11.7k
    code = R_tryEvalSilent(call, R_BaseEnv, &error);
340
11.7k
    eval_budget = budget;
341
11.7k
    Rf_unprotect(2);
342
343
11.7k
    return error ? expr : code;
344
11.7k
}
345
346
/* Caller protects the returned object immediately if it needs the result.
347
 * All errors (including parsing/allocation) must be under R_ToplevelExec.
348
 */
349
static SEXP eval_source(const eval_input_t *input, int *error)
350
9.16k
{
351
9.16k
    ParseStatus status;
352
9.16k
    SEXP text, parsed, env, result = R_NilValue;
353
9.16k
    Rf_protect(text = Rf_allocVector(STRSXP, 1));
354
9.16k
    SET_STRING_ELT(text, 0,
355
9.16k
                  Rf_mkCharLenCE((const char *)input->data, (int)input->size,
356
9.16k
                                 CE_UTF8));
357
9.16k
    Rf_protect(parsed = R_ParseVector(text, -1, &status, R_NilValue));
358
9.16k
    unsigned nodes = 0;
359
9.16k
    if (status != PARSE_OK || !eval_tree_within_budget(parsed, 0, &nodes)) {
360
1.13k
        *error = 1;
361
1.13k
        Rf_unprotect(2);
362
1.13k
        return R_NilValue;
363
1.13k
    }
364
365
    /* One budget covers all top-level expressions of an input. */
366
8.02k
    Rf_protect(env = R_NewEnv(eval_wrappers, TRUE, 29));
367
8.02k
    eval_cpu_start = clock();
368
8.02k
    eval_budget = FUZZ_EVAL_BUDGET;
369
14.0k
    for (R_xlen_t i = 0; i < XLENGTH(parsed); i++) {
370
11.7k
        SEXP code;
371
11.7k
        Rf_protect(code = eval_compile(VECTOR_ELT(parsed, i), env));
372
11.7k
        result = R_tryEvalSilent(code, env, error);
373
11.7k
        Rf_unprotect(1);
374
11.7k
        if (*error)
375
5.70k
            break;
376
11.7k
    }
377
8.02k
    eval_budget = 0;
378
379
8.02k
    Rf_unprotect(3);
380
8.02k
    return *error ? R_NilValue : result;
381
9.16k
}
382
383
static void eval_one(void *data)
384
9.16k
{
385
9.16k
    int error = 0;
386
9.16k
    eval_source((const eval_input_t *)data, &error);
387
9.16k
}
388
389
static void eval_run(const uint8_t *data, size_t size)
390
9.16k
{
391
9.16k
    eval_input_t input = { data, size };
392
9.16k
    R_ToplevelExec(eval_one, &input);
393
    /* A jump past eval_source (an interrupt, not an error) skips its reset. */
394
9.16k
    eval_budget = 0;
395
9.16k
}
396
397
int fuzz_target_init(int *argc, char ***argv)
398
2
{
399
2
    (void)argc;
400
2
    (void)argv;
401
    /* The harness compiles explicitly (eval_compile); nothing is attached
402
     * that a method lookup could reach. */
403
2
    setenv("R_ENABLE_JIT", "0", 1);
404
2
    setenv("R_DEFAULT_PACKAGES", "NULL", 1);
405
2
    if (getenv("R_VSIZE") == NULL)
406
1
        setenv("R_VSIZE", FUZZ_R_MAX_VSIZE, 1);
407
2
    fuzz_init_r();
408
409
2
    Rf_protect(eval_builtins = R_NewEnv(R_EmptyEnv, TRUE, 97));
410
2
    Rf_setAttrib(eval_builtins, R_NameSymbol, Rf_mkString("package:base"));
411
2
    eval_import_primitives(eval_builtins, eval_primitives,
412
2
                           sizeof(eval_primitives) / sizeof(*eval_primitives));
413
2
    R_LockEnvironment(eval_builtins, TRUE);
414
415
2
    Rf_protect(eval_wrappers = R_NewEnv(eval_builtins, TRUE, 29));
416
2
    SEXP private_env, colon;
417
2
    Rf_protect(private_env = R_NewEnv(R_EmptyEnv, TRUE, 29));
418
2
    eval_import_primitives(private_env, eval_wrapper_primitives,
419
2
                           sizeof(eval_wrapper_primitives) / sizeof(*eval_wrapper_primitives));
420
2
    Rf_defineVar(Rf_install(".colon"), eval_primitive(":"), private_env);
421
2
    R_LockEnvironment(private_env, TRUE);
422
2
    Rf_protect(colon = eval_run_init(eval_colon_source, private_env));
423
2
    Rf_defineVar(Rf_install(":"), colon, eval_wrappers);
424
2
    Rf_unprotect(2);
425
2
    R_LockEnvironment(eval_wrappers, TRUE);
426
427
2
    eval_compile_fn = eval_run_init(eval_compiler_setup, R_BaseEnv);
428
2
    R_PreserveObject(eval_compile_fn);
429
430
2
    R_PolledEvents = eval_poll_budget;
431
2
    eval_run((const uint8_t *)eval_warmup_source, strlen(eval_warmup_source));
432
2
    return 0;
433
2
}
434
435
int fuzz_target_run(const uint8_t *data, size_t size)
436
9.19k
{
437
9.19k
    if (size == 0 || size > FUZZ_MAX_INPUT || memchr(data, '\0', size) ||
438
9.19k
        !eval_valid_utf8(data, size))
439
40
        return 0;
440
441
9.15k
    eval_run(data, size);
442
9.15k
    return 0;
443
9.19k
}