Coverage Report

Created: 2025-07-18 06:12

/src/libass/fuzz/fuzz.c
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Copyright (C) 2022 libass contributors
3
 *
4
 * This file is part of libass.
5
 *
6
 * Permission to use, copy, modify, and distribute this software for any
7
 * purpose with or without fee is hereby granted, provided that the above
8
 * copyright notice and this permission notice appear in all copies.
9
 *
10
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17
 */
18
19
#include <assert.h>
20
#include <stdarg.h>
21
#include <stdbool.h>
22
#include <stdint.h>
23
#include <stdio.h>
24
#include <stdlib.h>
25
#include <string.h>
26
#ifdef _WIN32
27
#include <io.h>
28
#else
29
#include <unistd.h>
30
#endif
31
#include "ass.h"
32
#include "ass_types.h"
33
34
#define FUZZMODE_STANDALONE       0
35
#define FUZZMODE_AFLXX_SHAREDMEM  1
36
#define FUZZMODE_LIBFUZZER        2
37
#ifndef ASS_FUZZMODE
38
    #define ASS_FUZZMODE FUZZMODE_STANDALONE
39
#endif
40
41
// Limit maximal processed size in continuous fuzzing;
42
// to be used when the fuzzing setup doesn't expose its own max size control.
43
// Has no effect in standalone builds
44
#ifdef ASSFUZZ_MAX_LEN
45
11.1k
    #define LEN_IN_RANGE(s) ((s) <= (ASSFUZZ_MAX_LEN))
46
#else
47
    #define LEN_IN_RANGE(s) true
48
#endif
49
50
#define STR_(x) #x
51
#define STR(x) STR_(x)
52
53
// MSAN: will trigger MSAN if any pixel in bitmap not written to (costly)
54
#ifndef ASSFUZZ_HASH_WHOLEBITMAP
55
    #define ASSFUZZ_HASH_WHOLEBITMAP 0
56
#endif
57
58
ASS_Library *ass_library = NULL;
59
ASS_Renderer *ass_renderer = NULL;
60
bool quiet = false;
61
62
uint8_t hval = 0;
63
64
#if ASSFUZZ_HASH_WHOLEBITMAP
65
static inline void hash(const void *buf, size_t len)
66
{
67
    const uint8_t *ptr = buf;
68
    const uint8_t *end = ptr + len;
69
    while (ptr < end)
70
        hval ^= *ptr++;
71
    // MSAN doesn't trigger on the XORs, but will on conditional branches
72
    if (hval)
73
        hval ^= 57;
74
}
75
#endif
76
77
void msg_callback(int level, const char *fmt, va_list va, void *data)
78
322k
{
79
#if ASS_FUZZMODE == FUZZMODE_STANDALONE
80
    if (level > 6 || quiet) return;
81
    printf("libass: ");
82
    vprintf(fmt, va);
83
    printf("\n");
84
#else
85
    // still check for type-mismatches even when not printing
86
    // (seems to be cheap enough from some simple perormance tests)
87
322k
    char msg[2048];
88
322k
    int l = vsnprintf(msg, sizeof(msg), fmt, va) - 1;
89
322k
    l = l >= sizeof(msg) ? sizeof(msg) - 1 : l;
90
322k
    l = l < 0 ? 0 : l;
91
322k
    hval ^= *(msg + l);
92
322k
#endif
93
322k
}
94
95
static const int RWIDTH  = 854;
96
static const int RHEIGHT = 480;
97
98
static bool init_renderer(void)
99
11.1k
{
100
11.1k
    if (ass_renderer)
101
0
        return true;
102
103
11.1k
    ass_renderer = ass_renderer_init(ass_library);
104
11.1k
    if (!ass_renderer)
105
0
        return false;
106
107
11.1k
    ass_set_fonts(ass_renderer, NULL, "sans-serif",
108
11.1k
                  ASS_FONTPROVIDER_AUTODETECT, NULL, 1);
109
11.1k
    ass_set_frame_size(ass_renderer, RWIDTH, RHEIGHT);
110
11.1k
    ass_set_storage_size(ass_renderer, RWIDTH, RHEIGHT);
111
112
11.1k
    return true;
113
11.1k
}
114
115
static bool init(void)
116
11.1k
{
117
11.1k
    ass_library = ass_library_init();
118
11.1k
    if (!ass_library) {
119
0
        printf("ass_library_init failed!\n");
120
0
        return false;
121
0
    }
122
123
11.1k
    ass_set_message_cb(ass_library, msg_callback, NULL);
124
125
11.1k
    if (!init_renderer()) {
126
0
        ass_library_done(ass_library);
127
0
        ass_library = NULL;
128
0
        printf("ass_renderer_init failed!\n");
129
0
        return false;
130
0
    }
131
132
11.1k
    return true;
133
11.1k
}
134
135
136
static inline void process_image(ASS_Image *imgs)
137
52.8k
{
138
537k
    for (; imgs; imgs = imgs->next) {
139
484k
        assert(imgs->w >= 0 && imgs->h >= 0 &&
140
484k
               imgs->dst_x >= 0 && imgs->dst_y >= 0 &&
141
484k
               imgs->dst_x + imgs->w <= RWIDTH &&
142
484k
               imgs->dst_y + imgs->h <= RHEIGHT &&
143
484k
               imgs->stride >= imgs->w);
144
484k
#if !ASSFUZZ_HASH_WHOLEBITMAP
145
        // Check last pixel to probe for out-of-bounds errors
146
484k
        if (imgs->w && imgs->h)
147
429k
            hval ^= *(imgs->bitmap + imgs->stride * (imgs->h - 1) + imgs->w - 1);
148
#else
149
        unsigned char *src = imgs->bitmap;
150
        for (int y = 0; y < imgs->h; ++y) {
151
            hash(src, imgs->w);
152
            src += imgs->stride;
153
        }
154
#endif
155
484k
    }
156
52.8k
}
157
158
static void consume_track(ASS_Renderer *renderer, ASS_Track *track)
159
7.94k
{
160
28.3k
    for (int n = 0; n < track->n_events; ++n) {
161
20.4k
        int change;
162
20.4k
        ASS_Event event = track->events[n];
163
20.4k
        process_image(ass_render_frame(ass_renderer, track, event.Start, &change));
164
20.4k
        if (event.Duration > 1) {
165
16.1k
            process_image(ass_render_frame(ass_renderer, track, event.Start + event.Duration/2, &change));
166
16.1k
            process_image(ass_render_frame(ass_renderer, track, event.Start + event.Duration-1, &change));
167
16.1k
        }
168
20.4k
    }
169
7.94k
}
170
171
#if ASS_FUZZMODE == FUZZMODE_STANDALONE
172
#include "writeout.h"
173
174
struct settings {
175
    enum {
176
        CONSUME_INPUT,
177
        WRITEOUT_TRACK
178
    } mode;
179
    const char *input; // path or "-" for stdin
180
    const char *output; // path or NULL for tmp file
181
};
182
183
#ifdef _WIN32
184
#define READ_RET int
185
#else
186
#define READ_RET ssize_t
187
#endif
188
189
static ASS_Track *read_track_from_stdin(void)
190
{
191
    size_t smax = 4096;
192
    char *buf = malloc(smax);
193
    if (!buf)
194
        goto error;
195
    size_t s = 0;
196
    READ_RET read_b = 0;
197
    do {
198
        // AFL++ docs recommend using raw file descriptors
199
        // to avoid buffering issues with stdin
200
        read_b = read(fileno(stdin), buf + s, smax - s);
201
        s += read_b > 0 ? read_b : 0;
202
        if (s == smax) {
203
            size_t new_smax = smax > SIZE_MAX / 2 ? SIZE_MAX : smax * 2;
204
            char *new_buf = realloc(buf, new_smax);
205
            if (!new_buf || new_smax <= smax) {
206
                free(new_buf ? new_buf : buf);
207
                goto error;
208
            }
209
            smax = new_smax;
210
            buf = new_buf;
211
        }
212
    } while (read_b > 0);
213
    buf[s] = '\0';
214
    ASS_Track *track = ass_read_memory(ass_library, buf, s, NULL);
215
    free(buf);
216
    return track;
217
error:
218
    printf("Input too large!\n");
219
    return NULL;
220
}
221
222
/**
223
 * \param argc
224
 * \param argv
225
 * \param settings will be filled according to parsed args or defaults
226
 * \return whether CLI args could be parsed successfully
227
 */
228
static bool parse_cmdline(int argc, char *argv[], struct settings *settings)
229
{
230
    // defaults
231
    settings->mode = CONSUME_INPUT;
232
    settings->input = NULL;
233
    settings->output = NULL;
234
235
    int i;
236
    for (i = 1; i < argc; i++) {
237
        const char *param = argv[i];
238
        if (!param || param[0] != '-' || !param[1])
239
            goto no_more_args;
240
241
        switch (param[1]) {
242
        case 'q':
243
            quiet = true;
244
            break;
245
246
        case 'o':
247
            settings->mode = WRITEOUT_TRACK;
248
            // optional argument
249
            if (argc - i > 1 && argv[i + 1][0] != '-') {
250
                settings->output = argv[i + 1];
251
                i++;
252
            }
253
            break;
254
255
        case '-':
256
            if (param[2]) {
257
                return false;
258
            } else {
259
                i++;
260
                goto no_more_args;
261
            }
262
263
        default:
264
            return false;
265
        }
266
267
        continue;
268
269
no_more_args:
270
        break;
271
    }
272
273
    if (argc < 2 || argc - i > 1 || argc == i)
274
        return false;
275
276
    settings->input = argv[argc - 1];
277
    return !!settings->input;
278
}
279
280
int main(int argc, char *argv[])
281
{
282
    /* Default failure code of sanitisers is 1, unless
283
     * changed via env (A|UB|..)SAN_OPTIONS=exitcode=21
284
     * Except, LLVM's UBSAN always exits with 0 (unless using
285
     * -fsanitize-undefined-trap-on-error which will SIGILL without an
286
     * error report being printed), see https://reviews.llvm.org/D35085
287
     */
288
    enum {
289
        FUZZ_OK = 0,
290
        //SANITISER_FAIL = 1,
291
        // Invalid parameters passed etc
292
        FUZZ_BAD_USAGE = 2,
293
        // Error before rendering starts
294
        FUZZ_INIT_ERR = 0
295
    };
296
297
    ASS_Track *track = NULL;
298
    int retval = FUZZ_OK;
299
300
    struct settings settings;
301
    if (!parse_cmdline(argc, argv, &settings)) {
302
        printf("usage: %s [-q] [-o [output_file]] [--] <subtitle file>\n"
303
               "  -q:\n"
304
               "    Hide libass log messages\n"
305
               "\n"
306
               "  -o [FILE]:\n"
307
               "    Write out parsed file content in a standardized form\n"
308
               "    into FILE or if omitted a generated temporary file.\n"
309
               "    If used the input file will not be processed, only parsed.\n",
310
               argc ? argv[0] : "fuzz");
311
        return FUZZ_BAD_USAGE;
312
    }
313
314
    if (!init()) {
315
        printf("library init failed!\n");
316
        retval = FUZZ_INIT_ERR;
317
        goto cleanup;
318
    }
319
320
    if (strcmp(settings.input, "-"))
321
        track = ass_read_file(ass_library, settings.input, NULL);
322
    else
323
        track = read_track_from_stdin();
324
325
    if (!track) {
326
        printf("track init failed!\n");
327
        retval = FUZZ_INIT_ERR;
328
        goto cleanup;
329
    }
330
331
    switch (settings.mode) {
332
    case CONSUME_INPUT:
333
        consume_track(ass_renderer, track);
334
        break;
335
336
    case WRITEOUT_TRACK:
337
        write_out_track(track, settings.output);
338
        break;
339
    }
340
341
cleanup:
342
    if (track)        ass_free_track(track);
343
    if (ass_renderer) ass_renderer_done(ass_renderer);
344
    if (ass_library)  ass_library_done(ass_library);
345
346
    return retval;
347
}
348
#elif ASS_FUZZMODE == FUZZMODE_AFLXX_SHAREDMEM
349
__AFL_FUZZ_INIT();
350
/*
351
 * AFL++ docs recommend to disable optimisation for the main function
352
 * and GCC and Clang are the only AFL compilers.
353
 */
354
#pragma clang optimize off
355
#pragma GCC   optimize("O0")
356
int main(int argc, char *argv[])
357
{
358
    // AFLs buffer and length macros should not be used directly
359
    ssize_t len;
360
    unsigned char *buf;
361
362
#ifdef ASSFUZZ_FONTCONFIG_SYSROOT
363
    setenv("FONTCONFIG_SYSROOT", STR(ASSFUZZ_FONTCONFIG_SYSROOT), 1);
364
#endif
365
366
    if (!init()) {
367
        printf("library init failed!\n");
368
        return 1;
369
    }
370
371
    __AFL_INIT();
372
    buf = __AFL_FUZZ_TESTCASE_BUF;
373
    while (__AFL_LOOP(100000)) {
374
        len = __AFL_FUZZ_TESTCASE_LEN;
375
        if (!LEN_IN_RANGE(len))
376
            continue;
377
378
        if (!init_renderer()) {
379
            printf("Failing renderer init, skipping a sample!\n");
380
            continue;
381
        }
382
383
        ASS_Track *track = ass_read_memory(ass_library, (char *)buf, len, NULL);
384
        if (!track)
385
            continue;
386
        consume_track(ass_renderer, track);
387
388
        ass_free_track(track);
389
        ass_renderer_done(ass_renderer);
390
        ass_renderer = NULL;
391
        ass_clear_fonts(ass_library);
392
    }
393
394
    ass_renderer_done(ass_renderer);
395
    ass_library_done(ass_library);
396
397
    return 0;
398
}
399
#elif ASS_FUZZMODE == FUZZMODE_LIBFUZZER
400
int LLVMFuzzerInitialize(int *argc, char ***argv)
401
2
{
402
#ifdef ASSFUZZ_FONTCONFIG_SYSROOT
403
    setenv("FONTCONFIG_SYSROOT", STR(ASSFUZZ_FONTCONFIG_SYSROOT), 1);
404
#endif
405
2
    return 0;
406
2
}
407
408
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
409
11.1k
{
410
    // OSS Fuzz docs recommend just returning 0 on too large input
411
    // libFuzzer docs tell us to use -1 to prevent addition to corpus
412
    // BUT HongFuzz' LLVMFuzzerTestOneInput hook can't handle it and
413
    // neither does OSS Fuzz convert this in their wrapper, see:
414
    // https://github.com/google/oss-fuzz/issues/11983
415
11.1k
    if (!LEN_IN_RANGE(size))
416
11
        return 0;
417
418
11.1k
    ASS_Track *track = NULL;
419
420
11.1k
    if (!init())
421
0
        exit(1);
422
423
11.1k
    track = ass_read_memory(ass_library, (char *)data, size, NULL);
424
11.1k
    if (track) {
425
7.94k
        consume_track(ass_renderer, track);
426
7.94k
        ass_free_track(track);
427
7.94k
    }
428
429
11.1k
    ass_renderer_done(ass_renderer);
430
11.1k
    ass_library_done(ass_library);
431
11.1k
    ass_renderer = NULL;
432
11.1k
    ass_library = NULL;
433
434
11.1k
    return 0;
435
11.1k
}
436
#else
437
    #error Unknown fuzzer mode selected!
438
#endif