Commit: 47c5162ea8bc27fd7e810a8b7cc02146c59783ac
Parent: 20b2be7e6e78c9ac59165855854494a762e14f71
Author: Randy Palamar
Date: Sun, 16 Feb 2025 17:19:20 -0700
tests: add a basic fuzzer test
This mostly only checks input passed to the terminal for now. It
uncovered a couple bugs which were fixed:
* invalid clamping in fb_scroll_up and fb_scroll_down
* invalid handling of setting/clearing a tabstop on column 0
* integer overflow for bad CSI inputs
Also this enable address and ubsan for all tests now. This brought
to my attention that CLZ/CTZ with a 0 input is UB in C (but
not for the processor). I added a workaround which will just get
compiled out in optimized builds (but avoids the UB).
There are still more things to fuzz as well as determining better
input for the fuzzer and adding some ASAN poison in a few places
when compiling with it enabled.
Diffstat:
11 files changed, 214 insertions(+), 121 deletions(-)
diff --git a/build.sh b/build.sh
@@ -1,10 +1,20 @@
#!/bin/sh
cc=${CC:-cc}
+build=release
-build=${BUILD:-"release"}
-#build="debug"
-#build="optimized_debug"
+for arg in $@; do
+ case "$arg" in
+ clang) cc=clang ;;
+ gcc) cc=gcc ;;
+ debug) build=debug ;;
+ optimized_debug) build=optimized_debug ;;
+ fuzz) build=fuzz ;;
+ fuzz_results) build=fuzz_results ;;
+ release) build=release ;;
+ *) echo "usage: $0 [release|debug|optimized_debug|fuzz] [gcc|clang]"; exit 1 ;;
+ esac
+done
version="$(git describe --dirty --always)-${build}"
@@ -14,30 +24,42 @@ cflags="-march=native -Wall -DVERSION=\"${version}\""
#cflags="${cflags} -fsanitize=address,undefined"
ldflags="-lm -lGL -lglfw -lX11"
+testcflags="-march=native -Wall -ggdb -D_DEBUG -DVERSION=test -I."
+testcflags="${testcflags} -Wno-unused-variable -Wno-unused-function -Wno-undefined-internal"
+testcflags="${testcflags} -fsanitize=address,undefined"
+
[ ! -s "./config.h" ] && cp config.def.h config.h
case ${build} in
-"debug")
+debug)
cflags="${cflags} -O0 -ggdb -D_DEBUG -Wno-unused-function -Wno-undefined-internal"
build_lib=1
;;
-"optimized_debug")
+optimized_debug)
cflags="${cflags} -O3 -ggdb -D_DEBUG -Wno-unused-function -Wno-undefined-internal"
build_lib=1
;;
-"release")
+release)
cflags="${cflags} -O3"
;;
+fuzz)
+ afl-clang-fast ${testcflags} -O3 -o tests/test-fuzz tests/test-fuzz.c
+ afl-fuzz -o fuzz_out -i tests/fuzz_in ./tests/test-fuzz
+ exit 0
+ ;;
+fuzz_results)
+ ${cc} ${testcflags} -O0 tests/test-fuzz-results.c -o tests/test-fuzz-results
+ for file in fuzz_out/default/crashes/id*; do
+ ./tests/test-fuzz-results "${file}"
+ done
+ exit 0
+ ;;
*)
echo unsupported build type: ${build}
exit 1
;;
esac
-testcflags="-march=native -Wall -O0 -ggdb -D_DEBUG -DVERSION=test"
-testcflags="${testcflags} -Wno-unused-variable -Wno-unused-function -Wno-undefined-internal"
-testldflags="-lm -static"
-
[ ${build_lib} ] && ${cc} ${cflags} -fPIC vtgl.c -o vtgl.so ${ldflags} -shared
${cc} ${cflags} -o vtgl platform_linux_x11.c ${ldflags}
-${cc} ${testcflags} -I. -o tests/test tests/test.c ${testldflags}
+${cc} ${testcflags} -O0 -o tests/test tests/test.c
diff --git a/intrinsics.c b/intrinsics.c
@@ -1,7 +1,20 @@
#define FORCE_INLINE inline __attribute__((always_inline))
-#define clz_u32(a) __builtin_clz(a)
-#define ctz_u32(a) __builtin_ctz(a)
+static FORCE_INLINE u32
+clz_u32(u32 a)
+{
+ u32 result = 32;
+ if (a) result = __builtin_clz(a);
+ return result;
+}
+
+static FORCE_INLINE u32
+ctz_u32(u32 a)
+{
+ u32 result = 32;
+ if (a) result = __builtin_ctz(a);
+ return result;
+}
#ifdef __ARM_ARCH_ISA_A64
/* TODO? debuggers just loop here forever and need a manual PC increment (jump +1 in gdb) */
diff --git a/terminal.c b/terminal.c
@@ -258,7 +258,7 @@ fb_scroll_down(Term *t, u32 top, u32 n)
goto end;
TermView *tv = t->views + t->view_idx;
- CLAMP(n, 0, t->bot - top + 1);
+ CLAMP(n, 0, t->bot - top);
fb_clear_region(t, t->bot - n + 1, t->bot, 0, t->size.w);
for (u32 i = t->bot; i >= top + n; i--) {
@@ -279,7 +279,7 @@ fb_scroll_up(Term *t, u32 top, u32 n)
goto end;
TermView *tv = t->views + t->view_idx;
- CLAMP(n, 0, t->bot - top + 1);
+ CLAMP(n, 0, t->bot - top);
#if 0
size cell_count = (t->bot - n + 1) * t->size.w;
@@ -422,8 +422,8 @@ static void
term_tab_col(Term *t, u32 col, b32 set)
{
ASSERT(col < t->size.w);
- u32 idx = (col - 1) / ARRAY_COUNT(t->tabs);
- u32 bit = (col - 1) % ARRAY_COUNT(t->tabs);
+ u32 idx = col ? ((col - 1) / ARRAY_COUNT(t->tabs)) : 0;
+ u32 bit = col ? ((col - 1) % ARRAY_COUNT(t->tabs)) : 0;
u32 mask = 1u;
if (bit) mask = safe_left_shift(1, bit);
if (set) t->tabs[idx] |= mask;
@@ -893,27 +893,33 @@ push_tab(Term *t, i32 n)
cursor_move_to(t, t->cursor.pos.y, next_tab_position(t, n < 0));
}
-static i32
+static b32
parse_csi(s8 *r, CSI *csi)
{
BEGIN_TIMED_BLOCK();
- i32 result = 0;
+ b32 result = 1;
if (peek(*r, 0) == '?') {
csi->priv = 1;
get_ascii(r);
}
+ i32 accum = 0;
while (r->len) {
u32 cp = get_ascii(r);
if (ISCONTROL(cp)) {
continue;
} else if (BETWEEN(cp, '0', '9')) {
- csi->argv[csi->argc] *= 10;
- csi->argv[csi->argc] += cp - '0';
+ i32 digit = cp - '0';
+ if (accum <= (I32_MAX - digit) / 10) {
+ accum = 10 * accum + digit;
+ } else {
+ /* TODO(rnp): report error/out of range? */
+ }
continue;
}
- csi->argc++;
+ csi->argv[csi->argc++] = accum;
+ accum = 0;
if (cp != ';' || csi->argc == ESC_ARG_SIZ) {
if (cp == ';') csi->mode = get_ascii(r);
@@ -922,7 +928,7 @@ parse_csi(s8 *r, CSI *csi)
}
}
/* NOTE: if we fell out of the loop then we ran out of characters */
- result = -1;
+ result = 0;
end:
END_TIMED_BLOCK();
@@ -934,8 +940,8 @@ handle_csi(Term *t, CSI *csi)
{
BEGIN_TIMED_BLOCK();
s8 raw = csi->raw;
- i32 ret = parse_csi(&raw, csi);
- ASSERT(ret != -1);
+ b32 ret = parse_csi(&raw, csi);
+ ASSERT(ret);
#define ORONE(x) ((x)? (x) : 1)
diff --git a/tests/fuzz_in/data b/tests/fuzz_in/data
@@ -0,0 +1,3 @@
+ a(B>[!p[1m[24;1H[2J[3;1H a
+[47m a[4;19H[4»1H[?1h[?3;4l[H ~
+[K ~ a[mc
+\ No newline at end of file
diff --git a/tests/test-common.c b/tests/test-common.c
@@ -0,0 +1,80 @@
+/* See LICENSE for copyright details */
+#include "vtgl.h"
+#include "config.h"
+
+/* NOTE: stubs for stuff we aren't testing */
+static void get_gpu_glyph_index(Arena, void *, void *, u32, u32, u32, CachedGlyph **);
+
+KEYBIND_FN(copy) { return 0; }
+KEYBIND_FN(paste) { return 0; }
+KEYBIND_FN(scroll) { return 0; }
+KEYBIND_FN(zoom) { return 0; }
+
+#include "font.c"
+#include "terminal.c"
+
+static size
+copy_into_ringbuf(RingBuf *rb, s8 raw)
+{
+ ASSERT(raw.len < rb->cap);
+ for (size i = 0; i < raw.len; i++)
+ rb->buf[rb->widx + i] = raw.data[i];
+
+ rb->widx += raw.len;
+ rb->filled += raw.len;
+
+ CLAMP(rb->filled, 0, rb->cap);
+ if (rb->widx >= rb->cap)
+ rb->widx -= rb->cap;
+
+ ASSERT(rb->filled >= 0);
+ ASSERT(rb->widx >= 0 && rb->widx < rb->cap);
+ return raw.len;
+}
+
+static s8
+launder_static_string(Term *term, s8 static_str)
+{
+ RingBuf *rb = &term->views[term->view_idx].log;
+ term->unprocessed_bytes += copy_into_ringbuf(rb, static_str);
+ s8 raw = {
+ .len = term->unprocessed_bytes,
+ .data = rb->buf + (rb->widx - term->unprocessed_bytes)
+ };
+ return raw;
+}
+
+static Term *
+place_term_into_memory(MemoryBlock memory, i32 rows, i32 columns)
+{
+ Term *t = memory.memory;
+ t->arena_for_frame = arena_from_memory_block(memory);
+ t->arena_for_frame.beg += sizeof(*t);
+ t->size = (iv2){.w = 80, .h = 24};
+
+ os_allocate_ring_buffer(&t->views[0].log, MB(2));
+ line_buf_alloc(&t->views[0].lines, &t->arena_for_frame, t->views[0].log.buf, t->cursor.style,
+ BACKLOG_LINES);
+
+ os_allocate_ring_buffer(&t->views[1].log, MB(2));
+ line_buf_alloc(&t->views[1].lines, &t->arena_for_frame, t->views[1].log.buf, t->cursor.style,
+ ALT_BACKLOG_LINES);
+
+ t->views[0].fb.backing_store = memory_block_from_arena(&t->arena_for_frame, MB(1));
+ t->views[1].fb.backing_store = memory_block_from_arena(&t->arena_for_frame, MB(1));
+ initialize_framebuffer(&t->views[0].fb, t->size);
+ initialize_framebuffer(&t->views[1].fb, t->size);
+
+ term_reset(t);
+
+ return t;
+}
+
+static void
+release_term_memory(MemoryBlock backing)
+{
+ Term *t = backing.memory;
+ os_release_ring_buffer(&t->views[0].log);
+ os_release_ring_buffer(&t->views[1].log);
+ os_release_memory_block(backing);
+}
diff --git a/tests/test-fuzz-results.c b/tests/test-fuzz-results.c
@@ -0,0 +1,28 @@
+/* See LICENSE for copyright details */
+#define ASSERT(c) do { (void)(c); } while(0)
+#include "test-common.c"
+
+i32
+main(i32 argc, char *argv[])
+{
+ if (argc != 2) {
+ os_write_err_msg(s8("usage: test-fuzz-results crash_input\n"));
+ return 1;
+ }
+
+ u8 buf[4096];
+
+ Arena file_backing = arena_from_memory_block(os_block_alloc(MB(1)));
+ s8 file_data = os_read_file((u8 *)argv[1], &file_backing);
+
+ MemoryBlock term_backing = os_block_alloc(MB(4));
+ Term *term = place_term_into_memory(term_backing, 24, 80);
+ term->error_stream = arena_stream(arena_from_memory_block(os_block_alloc(MB(4))));
+ s8 raw = launder_static_string(term, file_data);
+ handle_input(term, term->arena_for_frame, raw);
+
+ if (term->error_stream.widx != 0)
+ os_write_err_msg(stream_to_s8(&term->error_stream));
+
+ return 0;
+}
diff --git a/tests/test-fuzz.c b/tests/test-fuzz.c
@@ -0,0 +1,20 @@
+/* See LICENSE for copyright details */
+#define ASSERT(c) do { (void)(c); } while(0)
+#include "test-common.c"
+#include <unistd.h>
+
+__AFL_FUZZ_INIT();
+i32
+main(void)
+{
+ __AFL_INIT();
+ u8 *buf = __AFL_FUZZ_TESTCASE_BUF;
+ while (__AFL_LOOP(10000)) {
+ MemoryBlock term_backing = os_block_alloc(MB(4));
+ Term *term = place_term_into_memory(term_backing, 24, 80);
+ i32 len = __AFL_FUZZ_TESTCASE_LEN;
+ s8 raw = launder_static_string(term, (s8){.data = buf, .len = len});
+ handle_input(term, term->arena_for_frame, raw);
+ }
+ return 0;
+}
diff --git a/tests/test.c b/tests/test.c
@@ -1,29 +1,5 @@
/* See LICENSE for copyright details */
-#include "vtgl.h"
-#include "config.h"
-
-/* NOTE: stubs for stuff we aren't testing */
-static void get_gpu_glyph_index(Arena, void *, void *, u32, u32, u32, CachedGlyph **);
-
-KEYBIND_FN(copy) { return 0; }
-KEYBIND_FN(paste) { return 0; }
-KEYBIND_FN(scroll) { return 0; }
-KEYBIND_FN(zoom) { return 0; }
-
-#include "font.c"
-#include "terminal.c"
-
-static b32
-mem_cmp(void *a_, void *b_, size len)
-{
- /* NOTE: small size assumption */
- ASSERT(len < 32);
- b32 result = 0;
- u8 *a = a_, *b = b_;
- for (size i = 0; i < len; i++)
- result |= a[i] != b[i];
- return result;
-}
+#include "test-common.c"
struct test_result { b32 status; const char *info; };
#define TEST_FN(name) struct test_result name(Term *term, Arena arena)
@@ -104,25 +80,6 @@ static s8 failure_string = s8("\x1B[31mFAILURE\x1B[0m\n");
static s8 success_string = s8("\x1B[32mSUCCESS\x1B[0m\n");
static s8 unsupported_string = s8("\x1B[33mUNSUPPORTED\x1B[0m\n");
-static size
-copy_into_ringbuf(RingBuf *rb, s8 raw)
-{
- ASSERT(raw.len < rb->cap);
- for (size i = 0; i < raw.len; i++)
- rb->buf[rb->widx + i] = raw.data[i];
-
- rb->widx += raw.len;
- rb->filled += raw.len;
-
- CLAMP(rb->filled, 0, rb->cap);
- if (rb->widx >= rb->cap)
- rb->widx -= rb->cap;
-
- ASSERT(rb->filled >= 0);
- ASSERT(rb->widx >= 0 && rb->widx < rb->cap);
- return raw.len;
-}
-
static b32
check_cells_equal(Cell *a, Cell *b)
{
@@ -132,18 +89,6 @@ check_cells_equal(Cell *a, Cell *b)
return result;
}
-static s8
-launder_static_string(Term *term, s8 static_str)
-{
- RingBuf *rb = &term->views[term->view_idx].log;
- term->unprocessed_bytes += copy_into_ringbuf(rb, static_str);
- s8 raw = {
- .len = term->unprocessed_bytes,
- .data = rb->buf + (rb->widx - term->unprocessed_bytes)
- };
- return raw;
-}
-
static TEST_FN(csi_embedded_control)
{
struct test_result result = {.info = __FUNCTION__};
@@ -1057,41 +1002,6 @@ static TEST_FN(set_top_bottom_margins_v4)
return result;
}
-static Term *
-place_term_into_memory(MemoryBlock memory, i32 rows, i32 columns)
-{
- Term *t = memory.memory;
- t->arena_for_frame = arena_from_memory_block(memory);
- t->arena_for_frame.beg += sizeof(*t);
- t->size = (iv2){.w = 80, .h = 24};
-
- os_allocate_ring_buffer(&t->views[0].log, MB(2));
- line_buf_alloc(&t->views[0].lines, &t->arena_for_frame, t->views[0].log.buf, t->cursor.style,
- BACKLOG_LINES);
-
- os_allocate_ring_buffer(&t->views[1].log, MB(2));
- line_buf_alloc(&t->views[1].lines, &t->arena_for_frame, t->views[1].log.buf, t->cursor.style,
- ALT_BACKLOG_LINES);
-
- t->views[0].fb.backing_store = memory_block_from_arena(&t->arena_for_frame, MB(1));
- t->views[1].fb.backing_store = memory_block_from_arena(&t->arena_for_frame, MB(1));
- initialize_framebuffer(&t->views[0].fb, t->size);
- initialize_framebuffer(&t->views[1].fb, t->size);
-
- term_reset(t);
-
- return t;
-}
-
-static void
-release_term_memory(MemoryBlock backing)
-{
- Term *t = backing.memory;
- os_release_ring_buffer(&t->views[0].log);
- os_release_ring_buffer(&t->views[1].log);
- os_release_memory_block(backing);
-}
-
int
main(void)
{
diff --git a/util.c b/util.c
@@ -312,10 +312,15 @@ i32_from_cstr(char *s, char delim)
}
for (; *s && *s != delim; s++) {
- if (!BETWEEN(s[0], '0', '9'))
+ i32 digit = s[0] - '0';
+ if (!BETWEEN(digit, '0', '9'))
return ret;
- ret.i *= 10;
- ret.i += s[0] - '0';
+
+ if (ret.i > (I32_MAX - digit) / 10) {
+ ret.status = CR_OUT_OF_RANGE;
+ return ret;
+ }
+ ret.i = 10 * ret.i + digit;
}
ret.i *= scale;
diff --git a/util.h b/util.h
@@ -276,9 +276,9 @@ typedef __attribute__((aligned(64))) struct {
#define MIN_FONT_SIZE 8
#define MAX_FONT_SIZE 128
-enum conversion_status { CR_FAILURE, CR_SUCCESS };
+typedef enum { CR_FAILURE, CR_SUCCESS, CR_OUT_OF_RANGE } conversion_status;
struct conversion_result {
- i32 status;
+ conversion_status status;
char *unparsed;
union { i32 i; f32 f; Colour colour;};
};
diff --git a/vtgl.h b/vtgl.h
@@ -54,6 +54,7 @@
#define ALT_BACKLOG_SIZE (MB(2))
#define ALT_BACKLOG_LINES (1024UL)
+#define I32_MAX INT32_MAX
#define I64_MIN INT64_MIN
typedef float f32;
@@ -75,10 +76,14 @@ typedef size_t usize;
#include "intrinsics.c"
#ifdef _DEBUG
+#ifndef ASSERT
#define ASSERT(c) do { if (!(c)) debugbreak(); } while(0)
+#endif
#define DEBUG_EXPORT
#else
+#ifndef ASSERT
#define ASSERT(c) do { (void)(c); } while(0)
+#endif
#define DEBUG_EXPORT static
#endif