Coverage Report

Created: 2026-10-16 07:07

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 the serialization format parser (src/main/serialize.c)
5
 * either directly, as unserialize() on a raw vector, or through the
6
 * connection-backed readers that readRDS() really uses: a rawConnection,
7
 * and a rawConnection wrapped in gzcon() so the gzip framing layer in
8
 * connections.c is parsed too.  The last input byte selects the route.
9
 *
10
 * The serialization format is used for .rds/.RData files, and users
11
 * routinely deserialize data from untrusted sources via readRDS().
12
 * This is a critical security surface.
13
 *
14
 * Adapted from r-afl's unserialize harness.
15
 */
16
17
#include <stdint.h>
18
#include <string.h>
19
20
/* This target spends most of its time in R's GC rather than in the
21
 * deserializer.  A serialized stream names the length of a vector before
22
 * its contents, so a few hundred bytes of input ask allocVector for
23
 * hundreds of megabytes; against the shared 1Gb default R answers each
24
 * such request with a full collection of a near-1Gb heap.  In CI the
25
 * target sits pinned at the cap (rss 917Mb) running 12-16 exec/s while
26
 * every other target runs in the hundreds, and that is what starved the
27
 * corpus merge during the prune run.
28
 *
29
 * Halving the cap roughly halves the cost of each of those collections.
30
 * Replaying the pruned corpus locally, one pass over 535 inputs:
31
 *
32
 *     1Gb     5.78s / 6.43s      ~88 exec/s
33
 *     512Mb   2.35s / 2.53s     ~220 exec/s
34
 *     256Mb   2.64s / 2.78s     ~198 exec/s
35
 *
36
 * The win is all in the first step down and it plateaus below that, so
37
 * take 512Mb: the smallest change from the default that captures it.
38
 * The cost is coverage of the paths that read a vector larger than
39
 * 512Mb -- those inputs now hit the vector limit inside allocVector and
40
 * are discarded before InIntegerVec/InRealVec run.  That is a deliberate
41
 * trade: bugs in those read loops reproduce at any length, and the loops
42
 * are still reached by every input under the cap. */
43
1
#define FUZZ_R_MAX_VSIZE "512Mb"
44
45
#include "common.h"
46
47
5.28k
#define FUZZ_MAX_INPUT (1024 * 64)
48
5.29k
#define N_CALLS 4
49
50
/* Slot 0 is the raw-vector route the original harness used.  The others
51
 * close their connection on exit so a failed read cannot leak one of
52
 * R's 128 connection slots across iterations. */
53
static const char *const sources[N_CALLS] = {
54
    "function(x) unserialize(x)",
55
    "function(x) { con <- rawConnection(x); on.exit(close(con)); "
56
    "unserialize(con) }",
57
    "function(x) { con <- rawConnection(x); on.exit(close(con)); "
58
    "readRDS(con) }",
59
    "function(x) { con <- gzcon(rawConnection(x)); on.exit(close(con)); "
60
    "readRDS(con) }",
61
};
62
63
static SEXP calls[N_CALLS];
64
65
int LLVMFuzzerInitialize(int *argc, char ***argv)
66
2
{
67
2
    (void)argc;
68
2
    (void)argv;
69
2
    fuzz_init_r();
70
71
10
    for (int i = 0; i < N_CALLS; i++) {
72
8
        SEXP wrapper;
73
8
        Rf_protect(wrapper = fuzz_make_wrapper(sources[i]));
74
8
        Rf_protect(calls[i] = Rf_lang2(wrapper, Rf_allocVector(RAWSXP, 1)));
75
8
    }
76
77
    /* Warmup: serialize(NULL, NULL) to get a valid RDS blob, then
78
     * round-trip it through every route to prime the code paths. */
79
2
    {
80
2
        int error = 0;
81
2
        SEXP ser_call;
82
2
        Rf_protect(ser_call = Rf_lang3(Rf_install("serialize"),
83
2
                                       R_NilValue, R_NilValue));
84
2
        SEXP w_raw = R_tryEval(ser_call, R_GlobalEnv, &error);
85
2
        if (!error && w_raw != R_NilValue) {
86
2
            Rf_protect(w_raw);
87
10
            for (int i = 0; i < N_CALLS; i++) {
88
8
                SETCADR(calls[i], w_raw);
89
8
                fuzz_eval_silent(calls[i], R_GlobalEnv);
90
8
            }
91
2
            Rf_unprotect(1);
92
2
        }
93
2
        Rf_unprotect(1); /* ser_call */
94
2
    }
95
96
2
    return 0;
97
2
}
98
99
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
100
5.28k
{
101
5.28k
    if (size < 2 || size > FUZZ_MAX_INPUT)
102
11
        return 0;
103
104
    /* The last byte picks the route and stays in the payload: the
105
     * deserializer ignores trailing bytes, so every stored input keeps
106
     * its full meaning on every route. */
107
5.27k
    int selected = data[size - 1] % N_CALLS;
108
5.27k
    if (!fuzz_set_raw_arg(calls[selected], data, size))
109
0
        return 0;
110
111
5.27k
    fuzz_eval_silent(calls[selected], R_GlobalEnv);
112
5.27k
    return 0;
113
5.27k
}