/src/r-oss-fuzz/harnesses/zip.c
Line | Count | Source |
1 | | /* |
2 | | * libFuzzer harness for R's bundled zip reader. |
3 | | * |
4 | | * src/main/dounzip.c carries a copy of minizip (unzOpen, the central |
5 | | * directory search, local-header coherency checks, unzReadCurrentFile) |
6 | | * plus R's own listing and extraction loops on top of it. It runs on |
7 | | * every utils::unzip() and unz() call, which is how binary packages |
8 | | * and download.file() payloads get unpacked -- the archive is untrusted |
9 | | * by construction. The copy is private to R, so nobody else fuzzes it. |
10 | | * |
11 | | * The slots cover listing, extraction into a per-process scratch |
12 | | * directory, and reading the first entry through an unz() connection in |
13 | | * both binary and text mode. unzip is pinned to "internal" so the |
14 | | * harness can never shell out to a system unzip. The input is staged in |
15 | | * a per-process scratch file since the entry points only take paths |
16 | | * (see common.h). |
17 | | */ |
18 | | |
19 | | #include <stdint.h> |
20 | | #include <string.h> |
21 | | |
22 | | #include "common.h" |
23 | | |
24 | 9.17k | #define FUZZ_MAX_INPUT (1024 * 64) |
25 | 9.14k | #define N_CALLS 4 |
26 | | |
27 | | /* Both paths stay reachable from here for the life of the process, so |
28 | | * LeakSanitizer does not report them at exit. */ |
29 | | static char *scratch_path; |
30 | | static char *scratch_dir; |
31 | | static SEXP calls[N_CALLS]; |
32 | | |
33 | | /* Extraction needs a directory of its own. junkpaths = TRUE keeps |
34 | | * every entry inside it regardless of the path stored in the archive, |
35 | | * and the wrapper empties it again on exit (error or not) so one |
36 | | * iteration's files never collide with the next. Output volume is |
37 | | * bounded by the deflate ratio (~1000:1) times the input cap. */ |
38 | | static char *make_scratch_dir(void) |
39 | 2 | { |
40 | 2 | const char *tmpdir = getenv("TMPDIR"); |
41 | 2 | if (tmpdir == NULL || *tmpdir == '\0') |
42 | 2 | tmpdir = "/tmp"; |
43 | | |
44 | 2 | char *path = malloc(PATH_MAX); |
45 | 2 | if (path == NULL) |
46 | 0 | return NULL; |
47 | 2 | snprintf(path, PATH_MAX, "%s/fuzz-zip-exdir-XXXXXX", tmpdir); |
48 | 2 | if (mkdtemp(path) == NULL) { |
49 | 0 | free(path); |
50 | 0 | return NULL; |
51 | 0 | } |
52 | 2 | return path; |
53 | 2 | } |
54 | | |
55 | | int LLVMFuzzerInitialize(int *argc, char ***argv) |
56 | 2 | { |
57 | 2 | (void)argc; |
58 | 2 | (void)argv; |
59 | 2 | fuzz_init_r(); |
60 | | |
61 | 2 | scratch_path = fuzz_scratch_file("zip"); |
62 | 2 | scratch_dir = make_scratch_dir(); |
63 | 2 | if (scratch_path == NULL || scratch_dir == NULL) |
64 | 0 | abort(); |
65 | | |
66 | | /* [0] listing only: central directory walk (ziplist). |
67 | | * [1] extraction: local headers and inflate (zipunzip/extract_one). |
68 | | * [2] unz() connection, binary read of the first entry. |
69 | | * [3] unz() connection, text read of the first entry. |
70 | | * The readBin/readLines caps bound how much of a bomb is inflated. */ |
71 | 2 | static const char *const sources[N_CALLS] = { |
72 | 2 | "function(path, exdir) " |
73 | 2 | "utils::unzip(path, list = TRUE, unzip = \"internal\")", |
74 | | |
75 | 2 | "function(path, exdir) { " |
76 | 2 | "on.exit(unlink(list.files(exdir, all.files = TRUE, " |
77 | 2 | "full.names = TRUE, no.. = TRUE), recursive = TRUE)); " |
78 | 2 | "utils::unzip(path, exdir = exdir, junkpaths = TRUE, " |
79 | 2 | "unzip = \"internal\") }", |
80 | | |
81 | 2 | "function(path, exdir) { " |
82 | 2 | "l <- utils::unzip(path, list = TRUE, unzip = \"internal\"); " |
83 | 2 | "if (nrow(l)) { con <- unz(path, l$Name[1], open = \"rb\"); " |
84 | 2 | "on.exit(close(con)); readBin(con, \"raw\", 65536L) } }", |
85 | | |
86 | 2 | "function(path, exdir) { " |
87 | 2 | "l <- utils::unzip(path, list = TRUE, unzip = \"internal\"); " |
88 | 2 | "if (nrow(l)) { con <- unz(path, l$Name[1]); on.exit(close(con)); " |
89 | 2 | "readLines(con, n = 1024L, warn = FALSE) } }", |
90 | 2 | }; |
91 | 2 | SEXP path, exdir_string; |
92 | 2 | Rf_protect(path = Rf_mkString(scratch_path)); |
93 | 2 | Rf_protect(exdir_string = Rf_mkString(scratch_dir)); |
94 | 10 | for (int i = 0; i < N_CALLS; i++) { |
95 | 8 | SEXP wrapper; |
96 | 8 | Rf_protect(wrapper = fuzz_make_wrapper(sources[i])); |
97 | 8 | Rf_protect(calls[i] = Rf_lang3(wrapper, path, exdir_string)); |
98 | 8 | } |
99 | 2 | return 0; |
100 | 2 | } |
101 | | |
102 | | int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) |
103 | 9.17k | { |
104 | 9.17k | if (size < 2 || size > FUZZ_MAX_INPUT) |
105 | 40 | return 0; |
106 | | |
107 | 9.13k | if (!fuzz_write_scratch(scratch_path, data + 1, size - 1)) |
108 | 0 | return 0; |
109 | | |
110 | 9.13k | fuzz_eval_silent(calls[data[0] % N_CALLS], R_GlobalEnv); |
111 | 9.13k | return 0; |
112 | 9.13k | } |