Coverage Report

Created: 2026-10-16 07:06

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/r-oss-fuzz/harnesses/datetime.c
Line
Count
Source
1
/*
2
 * libFuzzer harness for R's date/time parsing and formatting.
3
 *
4
 * Passes fuzzed input through strptime, as.Date, as.POSIXct, as.POSIXlt,
5
 * format.POSIXlt and balancePOSIXlt -- exercising R_strptime (Rstrptime.h)
6
 * and src/main/datetime.c: ~2000 lines of locale-dependent parsing,
7
 * timezone handling, leap seconds, and DST transitions.
8
 *
9
 * Input layout: the first byte selects a call slot, the rest is the
10
 * payload.  Slots 0-8 take the whole payload as the datetime string and
11
 * use fixed format strings (the original shape of this target, so the
12
 * stored corpus keeps its meaning).  Slots 9 and up split the payload at
13
 * its first newline: the format string comes before it and the datetime
14
 * string after, so strptime directives and strftime output are fuzzed
15
 * too.  Without a newline the payload is the datetime string and a
16
 * default format is used.
17
 *
18
 * Adapted from r-afl's datetime harness.
19
 */
20
21
#include <locale.h>
22
#include <stdint.h>
23
#include <string.h>
24
25
#include "common.h"
26
27
7.38k
#define FUZZ_MAX_INPUT (1024 * 64)
28
29
/*
30
 * strptime format strings, each exercising a different path in
31
 * Rstrptime.h's strptime_internal (numeric fields, locale month/weekday
32
 * names, ISO 8601 zone offset, locale preferred representations).
33
 */
34
11.1k
#define N_FORMATS 6
35
static const char *strptime_formats[N_FORMATS] = {
36
    "%Y-%m-%d %H:%M:%S",
37
    "%d/%b/%Y",
38
    "%Y-%m-%dT%H:%M:%S%z",
39
    "%c",
40
    "%x %X",
41
    "%a %d %b %Y",
42
};
43
44
11.0k
#define N_FIXED_CALLS (N_FORMATS + 3)
45
7.40k
#define N_FMT_CALLS 8
46
7.40k
#define N_CALLS (N_FIXED_CALLS + N_FMT_CALLS)
47
48
/* Slots from this index on run with a UTF-8 LC_CTYPE so R_strptime takes
49
 * its wide-character path (w_strptime_internal), which is otherwise
50
 * unreachable from a C-locale fuzzer. */
51
3.68k
#define FIRST_UTF8_CALL (N_CALLS - 2)
52
53
3.68k
#define DEFAULT_FORMAT "%Y-%m-%d %H:%M:%OS"
54
55
static SEXP x_str;
56
static SEXP fmt_str;
57
static SEXP calls[N_CALLS];
58
59
static SEXP call_ctype_utf8;
60
static SEXP call_ctype_c;
61
static int ctype_is_utf8 = 0;
62
63
/* Pick a UTF-8 locale this runner has.  Returns NULL when none exists,
64
 * in which case the UTF-8 slots simply run in the C locale. */
65
static const char *find_utf8_locale(void)
66
2
{
67
2
    static const char *const candidates[] = { "C.UTF-8", "en_US.UTF-8", NULL };
68
2
    const char *found = NULL;
69
70
2
    for (int i = 0; candidates[i] != NULL; i++) {
71
2
        if (setlocale(LC_CTYPE, candidates[i]) != NULL) {
72
2
            found = candidates[i];
73
2
            break;
74
2
        }
75
2
    }
76
2
    setlocale(LC_CTYPE, "C");
77
2
    return found;
78
2
}
79
80
static SEXP make_setlocale_call(const char *name)
81
4
{
82
4
    SEXP call;
83
4
    Rf_protect(call = Rf_lang3(Rf_install("Sys.setlocale"),
84
4
                               Rf_mkString("LC_CTYPE"), Rf_mkString(name)));
85
4
    return call;
86
4
}
87
88
/* Switch LC_CTYPE only when a slot needs the other setting; Sys.setlocale
89
 * is cheap but not free, and consecutive inputs usually share a slot. */
90
static void set_ctype(int want_utf8)
91
3.68k
{
92
3.68k
    if (call_ctype_utf8 == NULL || want_utf8 == ctype_is_utf8)
93
1.92k
        return;
94
95
1.75k
    fuzz_eval_silent(want_utf8 ? call_ctype_utf8 : call_ctype_c, R_GlobalEnv);
96
1.75k
    ctype_is_utf8 = want_utf8;
97
1.75k
}
98
99
int LLVMFuzzerInitialize(int *argc, char ***argv)
100
2
{
101
2
    fuzz_init_r();
102
103
2
    SEXP sym_strptime   = Rf_install("strptime");
104
2
    SEXP sym_as_Date    = Rf_install("as.Date");
105
2
    SEXP sym_as_POSIXct = Rf_install("as.POSIXct");
106
2
    SEXP sym_as_POSIXlt = Rf_install("as.POSIXlt");
107
2
    SEXP sym_format     = Rf_install("format");
108
2
    SEXP sym_tz         = Rf_install("tz");
109
110
    /* Reusable input containers -- each iteration swaps their CHARSXPs. */
111
2
    Rf_protect(x_str = Rf_allocVector(STRSXP, 1));
112
2
    Rf_protect(fmt_str = Rf_allocVector(STRSXP, 1));
113
114
2
    SEXP tz_utc, tz_london, true_val;
115
2
    Rf_protect(tz_utc = Rf_mkString("UTC"));
116
2
    Rf_protect(tz_london = Rf_mkString("Europe/London"));
117
2
    Rf_protect(true_val = Rf_ScalarLogical(TRUE));
118
119
    /* strptime(x, format=fmt, tz="UTC") for each fixed format */
120
14
    for (int i = 0; i < N_FORMATS; i++) {
121
12
        SEXP fmt;
122
12
        Rf_protect(fmt = Rf_mkString(strptime_formats[i]));
123
12
        Rf_protect(calls[i] = Rf_lang4(sym_strptime, x_str, fmt, tz_utc));
124
12
        SET_TAG(CDDR(calls[i]), sym_format);
125
12
        SET_TAG(CDR(CDDR(calls[i])), sym_tz);
126
12
    }
127
128
    /* as.Date(x, format="%Y-%m-%d") */
129
2
    {
130
2
        SEXP date_fmt;
131
2
        Rf_protect(date_fmt = Rf_mkString("%Y-%m-%d"));
132
2
        Rf_protect(calls[N_FORMATS] = Rf_lang3(sym_as_Date, x_str, date_fmt));
133
2
        SET_TAG(CDDR(calls[N_FORMATS]), sym_format);
134
2
    }
135
136
    /* as.POSIXct(x, tz="UTC") */
137
2
    Rf_protect(calls[N_FORMATS + 1] = Rf_lang3(sym_as_POSIXct, x_str, tz_utc));
138
2
    SET_TAG(CDDR(calls[N_FORMATS + 1]), sym_tz);
139
140
    /* as.POSIXlt(x, tz="UTC") */
141
2
    Rf_protect(calls[N_FORMATS + 2] = Rf_lang3(sym_as_POSIXlt, x_str, tz_utc));
142
2
    SET_TAG(CDDR(calls[N_FORMATS + 2]), sym_tz);
143
144
    /* The fuzzed-format slots.  strptime(x, fmt, tz="UTC") is shared by
145
     * several of them as the inner call. */
146
2
    SEXP strptime_fmt;
147
2
    Rf_protect(strptime_fmt = Rf_lang4(sym_strptime, x_str, fmt_str, tz_utc));
148
2
    SET_TAG(CDDR(strptime_fmt), sym_format);
149
2
    SET_TAG(CDR(CDDR(strptime_fmt)), sym_tz);
150
151
2
    int k = N_FIXED_CALLS;
152
153
    /* [9] strptime(x, fmt, tz="UTC") */
154
2
    calls[k++] = strptime_fmt;
155
156
    /* [10] format(strptime(x, fmt, tz="UTC"), format=fmt, usetz=TRUE)
157
     * -- do_formatPOSIXlt with a fuzzed strftime format */
158
2
    Rf_protect(calls[k] = Rf_lang4(sym_format, strptime_fmt, fmt_str, true_val));
159
2
    SET_TAG(CDDR(calls[k]), sym_format);
160
2
    SET_TAG(CDR(CDDR(calls[k])), Rf_install("usetz"));
161
2
    k++;
162
163
    /* [11] as.POSIXlt(as.Date(x, format=fmt)) -- do_D2POSIXlt */
164
2
    {
165
2
        SEXP as_date;
166
2
        Rf_protect(as_date = Rf_lang3(sym_as_Date, x_str, fmt_str));
167
2
        SET_TAG(CDDR(as_date), sym_format);
168
2
        Rf_protect(calls[k] = Rf_lang2(sym_as_POSIXlt, as_date));
169
2
        k++;
170
2
    }
171
172
    /* [12] format(as.POSIXct(x, format=fmt, tz="UTC"), format=fmt) */
173
2
    {
174
2
        SEXP as_ct;
175
2
        Rf_protect(as_ct = Rf_lang4(sym_as_POSIXct, x_str, fmt_str, tz_utc));
176
2
        SET_TAG(CDDR(as_ct), sym_format);
177
2
        SET_TAG(CDR(CDDR(as_ct)), sym_tz);
178
2
        Rf_protect(calls[k] = Rf_lang3(sym_format, as_ct, fmt_str));
179
2
        SET_TAG(CDDR(calls[k]), sym_format);
180
2
        k++;
181
2
    }
182
183
    /* [13], [14] Perturb the parsed fields by amounts taken from the
184
     * input bytes, then balance, convert and format the result.  A
185
     * strptime result carries a "balanced" attribute that short-circuits
186
     * balancePOSIXlt, so dropping it is what reaches the normalisation
187
     * code in balancePOSIXlt, mktime0 and localtime0; the second slot
188
     * uses a DST-observing zone so the local-time paths run. */
189
2
    {
190
2
        static const char *const source =
191
2
            "function(x, fmt, tz) {\n"
192
2
            "    lt <- unclass(strptime(x, fmt, tz = tz))\n"
193
2
            "    attr(lt, \"balanced\") <- NULL\n"
194
2
            "    b <- as.integer(charToRaw(x))\n"
195
2
            "    fields <- c(\"sec\", \"min\", \"hour\", \"mday\", \"mon\", \"year\")\n"
196
2
            "    for (i in seq_len(min(length(b), length(fields))))\n"
197
2
            "        lt[[fields[i]]] <- lt[[fields[i]]] + (b[i] - 128L) * 7L\n"
198
2
            "    class(lt) <- c(\"POSIXlt\", \"POSIXt\")\n"
199
2
            "    bal <- balancePOSIXlt(lt)\n"
200
2
            "    ct <- as.POSIXct(lt)\n"
201
2
            "    invisible(list(as.POSIXlt(ct, tz = tz), as.Date(lt),\n"
202
2
            "                   format(bal, fmt, usetz = TRUE), format(ct)))\n"
203
2
            "}";
204
2
        SEXP wrapper;
205
2
        Rf_protect(wrapper = fuzz_make_wrapper(source));
206
2
        Rf_protect(calls[k] = Rf_lang4(wrapper, x_str, fmt_str, tz_utc));
207
2
        k++;
208
2
        Rf_protect(calls[k] = Rf_lang4(wrapper, x_str, fmt_str, tz_london));
209
2
        k++;
210
2
    }
211
212
    /* [15], [16] The same strptime and format calls, run under a UTF-8
213
     * LC_CTYPE (see set_ctype). */
214
2
    calls[k++] = calls[N_FIXED_CALLS];
215
2
    calls[k++] = calls[N_FIXED_CALLS + 1];
216
217
2
    if (k != N_CALLS)
218
0
        abort();
219
220
2
    const char *utf8_locale = find_utf8_locale();
221
2
    if (utf8_locale != NULL) {
222
2
        call_ctype_utf8 = make_setlocale_call(utf8_locale);
223
2
        call_ctype_c = make_setlocale_call("C");
224
2
    }
225
226
    /* Warmup: prime datetime internals. */
227
2
    {
228
2
        int error = 0;
229
2
        SET_STRING_ELT(x_str, 0, Rf_mkChar("2024-01-15 12:30:00"));
230
2
        SET_STRING_ELT(fmt_str, 0, Rf_mkChar(DEFAULT_FORMAT));
231
36
        for (int i = 0; i < N_CALLS; i++) {
232
34
            R_tryEval(calls[i], R_GlobalEnv, &error);
233
34
            error = 0;
234
34
        }
235
2
    }
236
237
2
    return 0;
238
2
}
239
240
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
241
3.69k
{
242
3.69k
    if (size < 2 || size > FUZZ_MAX_INPUT || memchr(data + 1, '\0', size - 1))
243
11
        return 0;
244
245
3.68k
    int selected = data[0] % N_CALLS;
246
3.68k
    const uint8_t *payload = data + 1;
247
3.68k
    size_t payload_size = size - 1;
248
249
3.68k
    char buf[FUZZ_MAX_INPUT + 1];
250
3.68k
    const char *fmt = DEFAULT_FORMAT;
251
3.68k
    const char *value = buf;
252
253
3.68k
    if (selected < N_FIXED_CALLS) {
254
618
        memcpy(buf, payload, payload_size);
255
618
        buf[payload_size] = '\0';
256
3.06k
    } else {
257
        /* "<format>\n<datetime>"; the NUL replacing the newline splits
258
         * the one buffer into both strings. */
259
3.06k
        memcpy(buf, payload, payload_size);
260
3.06k
        buf[payload_size] = '\0';
261
3.06k
        char *nl = memchr(buf, '\n', payload_size);
262
3.06k
        if (nl != NULL) {
263
2.33k
            *nl = '\0';
264
2.33k
            fmt = buf;
265
2.33k
            value = nl + 1;
266
2.33k
        }
267
3.06k
    }
268
269
3.68k
    if (!fuzz_set_string(x_str, value))
270
0
        return 0;
271
3.68k
    if (!fuzz_set_string(fmt_str, fmt))
272
0
        return 0;
273
274
3.68k
    set_ctype(selected >= FIRST_UTF8_CALL);
275
276
3.68k
    fuzz_eval_data_t ed = { .call = calls[selected], .env = R_GlobalEnv };
277
3.68k
    R_ToplevelExec(fuzz_do_eval, &ed);
278
279
3.68k
    return 0;
280
3.68k
}