Coverage Report

Created: 2026-09-14 06:56

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/r-oss-fuzz/harnesses/unserialize.c
Line
Count
Source
1
/*
2
 * libFuzzer harness for R's deserializer.
3
 *
4
 * Feeds raw bytes to unserialize() as a raw vector -- exercising R's
5
 * serialization format parser (src/main/serialize.c).
6
 *
7
 * The serialization format is used for .rds/.RData files, and users
8
 * routinely deserialize data from untrusted sources via readRDS().
9
 * This is a critical security surface.
10
 *
11
 * Adapted from r-afl's unserialize harness.
12
 */
13
14
#include <stdint.h>
15
#include <string.h>
16
17
/* This target spends most of its time in R's GC rather than in the
18
 * deserializer.  A serialized stream names the length of a vector before
19
 * its contents, so a few hundred bytes of input ask allocVector for
20
 * hundreds of megabytes; against the shared 1Gb default R answers each
21
 * such request with a full collection of a near-1Gb heap.  In CI the
22
 * target sits pinned at the cap (rss 917Mb) running 12-16 exec/s while
23
 * every other target runs in the hundreds, and that is what starved the
24
 * corpus merge during the prune run.
25
 *
26
 * Halving the cap roughly halves the cost of each of those collections.
27
 * Replaying the pruned corpus locally, one pass over 535 inputs:
28
 *
29
 *     1Gb     5.78s / 6.43s      ~88 exec/s
30
 *     512Mb   2.35s / 2.53s     ~220 exec/s
31
 *     256Mb   2.64s / 2.78s     ~198 exec/s
32
 *
33
 * The win is all in the first step down and it plateaus below that, so
34
 * take 512Mb: the smallest change from the default that captures it.
35
 * The cost is coverage of the paths that read a vector larger than
36
 * 512Mb -- those inputs now hit the vector limit inside allocVector and
37
 * are discarded before InIntegerVec/InRealVec run.  That is a deliberate
38
 * trade: bugs in those read loops reproduce at any length, and the loops
39
 * are still reached by every input under the cap. */
40
1
#define FUZZ_R_MAX_VSIZE "512Mb"
41
42
#include "common.h"
43
44
4.35k
#define FUZZ_MAX_INPUT (1024 * 64)
45
46
static SEXP call_unser;
47
48
int LLVMFuzzerInitialize(int *argc, char ***argv)
49
2
{
50
2
    fuzz_init_r();
51
52
    /* Pre-build the call: unserialize(<placeholder>) */
53
2
    Rf_protect(call_unser = Rf_lang2(Rf_install("unserialize"),
54
2
                                     Rf_allocVector(RAWSXP, 1)));
55
56
    /* Warmup: serialize(NULL, NULL) to get a valid RDS blob, then
57
     * round-trip it through unserialize to prime the code path. */
58
2
    {
59
2
        int error = 0;
60
2
        SEXP ser_call;
61
2
        Rf_protect(ser_call = Rf_lang3(Rf_install("serialize"),
62
2
                                       R_NilValue, R_NilValue));
63
2
        SEXP w_raw = R_tryEval(ser_call, R_GlobalEnv, &error);
64
2
        if (!error && w_raw != R_NilValue) {
65
2
            Rf_protect(w_raw);
66
2
            SETCADR(call_unser, w_raw);
67
2
            fuzz_eval_data_t ed = { .call = call_unser, .env = R_GlobalEnv };
68
2
            R_ToplevelExec(fuzz_do_eval, &ed);
69
2
            Rf_unprotect(1);
70
2
        }
71
2
        Rf_unprotect(1); /* ser_call */
72
2
    }
73
74
2
    return 0;
75
2
}
76
77
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
78
4.35k
{
79
4.35k
    if (size == 0 || size > FUZZ_MAX_INPUT)
80
7
        return 0;
81
82
4.35k
    if (!fuzz_set_raw_arg(call_unser, data, size))
83
0
        return 0;
84
85
4.35k
    fuzz_eval_data_t ed = { .call = call_unser, .env = R_GlobalEnv };
86
4.35k
    R_ToplevelExec(fuzz_do_eval, &ed);
87
88
4.35k
    return 0;
89
4.35k
}