/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 | 9.17k | #define FUZZ_MAX_INPUT (1024 * 64) |
48 | 9.15k | #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 | 9.17k | { |
101 | 9.17k | if (size < 2 || size > FUZZ_MAX_INPUT) |
102 | 40 | 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 | 9.13k | int selected = data[size - 1] % N_CALLS; |
108 | 9.13k | if (!fuzz_set_raw_arg(calls[selected], data, size)) |
109 | 0 | return 0; |
110 | | |
111 | 9.13k | fuzz_eval_silent(calls[selected], R_GlobalEnv); |
112 | 9.13k | return 0; |
113 | 9.13k | } |