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