/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 | } |