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