From 7620c03b7278ed631efa98455c5e116538c23ed7 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 30 Mar 2026 10:19:03 +0100 Subject: [PATCH] Add new fuzzers for command parsing, formats and styles, from David Korczynski in GitHub issue 4957. --- Makefile.am | 12 ++- fuzz/cmd-parse-fuzzer.c | 83 +++++++++++++++++++++ fuzz/cmd-parse-fuzzer.dict | 133 ++++++++++++++++++++++++++++++++++ fuzz/cmd-parse-fuzzer.options | 2 + fuzz/format-fuzzer.c | 88 ++++++++++++++++++++++ fuzz/format-fuzzer.dict | 71 ++++++++++++++++++ fuzz/format-fuzzer.options | 2 + fuzz/style-fuzzer.c | 79 ++++++++++++++++++++ fuzz/style-fuzzer.dict | 73 +++++++++++++++++++ fuzz/style-fuzzer.options | 2 + 10 files changed, 544 insertions(+), 1 deletion(-) create mode 100644 fuzz/cmd-parse-fuzzer.c create mode 100644 fuzz/cmd-parse-fuzzer.dict create mode 100644 fuzz/cmd-parse-fuzzer.options create mode 100644 fuzz/format-fuzzer.c create mode 100644 fuzz/format-fuzzer.dict create mode 100644 fuzz/format-fuzzer.options create mode 100644 fuzz/style-fuzzer.c create mode 100644 fuzz/style-fuzzer.dict create mode 100644 fuzz/style-fuzzer.options diff --git a/Makefile.am b/Makefile.am index 7dddbe49..3ec467f4 100644 --- a/Makefile.am +++ b/Makefile.am @@ -237,9 +237,19 @@ dist_tmux_SOURCES += image.c image-sixel.c endif if NEED_FUZZING -check_PROGRAMS = fuzz/input-fuzzer +check_PROGRAMS = \ + fuzz/input-fuzzer \ + fuzz/cmd-parse-fuzzer \ + fuzz/format-fuzzer \ + fuzz/style-fuzzer fuzz_input_fuzzer_LDFLAGS = $(FUZZING_LIBS) fuzz_input_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS) +fuzz_cmd_parse_fuzzer_LDFLAGS = $(FUZZING_LIBS) +fuzz_cmd_parse_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS) +fuzz_format_fuzzer_LDFLAGS = $(FUZZING_LIBS) +fuzz_format_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS) +fuzz_style_fuzzer_LDFLAGS = $(FUZZING_LIBS) +fuzz_style_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS) endif # Install tmux.1 in the right format. diff --git a/fuzz/cmd-parse-fuzzer.c b/fuzz/cmd-parse-fuzzer.c new file mode 100644 index 00000000..2e135422 --- /dev/null +++ b/fuzz/cmd-parse-fuzzer.c @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2026 David Korczynski + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * Fuzz the tmux command parser (cmd_parse_from_buffer). + * + * This exercises: + * - cmd-parse.y (yacc grammar, lexer, command building) + * - cmd.c (command lookup and validation) + * - arguments.c (argument parsing and flag handling) + * - cmd-find.c (target resolution) + * - options.c (option name lookups during parsing) + */ + +#include +#include + +#include "tmux.h" + +struct event_base *libevent; + +int +LLVMFuzzerTestOneInput(const u_char *data, size_t size) +{ + struct cmd_parse_input pi; + struct cmd_parse_result *pr; + + if (size > 2048 || size == 0) + return 0; + + memset(&pi, 0, sizeof pi); + pi.flags = CMD_PARSE_QUIET; + + pr = cmd_parse_from_buffer(data, size, &pi); + switch (pr->status) { + case CMD_PARSE_SUCCESS: + cmd_list_free(pr->cmdlist); + break; + case CMD_PARSE_ERROR: + free(pr->error); + break; + default: + break; + } + + return 0; +} + +int +LLVMFuzzerInitialize(__unused int *argc, __unused char ***argv) +{ + const struct options_table_entry *oe; + + global_environ = environ_create(); + global_options = options_create(NULL); + global_s_options = options_create(NULL); + global_w_options = options_create(NULL); + for (oe = options_table; oe->name != NULL; oe++) { + if (oe->scope & OPTIONS_TABLE_SERVER) + options_default(global_options, oe); + if (oe->scope & OPTIONS_TABLE_SESSION) + options_default(global_s_options, oe); + if (oe->scope & OPTIONS_TABLE_WINDOW) + options_default(global_w_options, oe); + } + libevent = osdep_event_init(); + socket_path = xstrdup("dummy"); + + return 0; +} diff --git a/fuzz/cmd-parse-fuzzer.dict b/fuzz/cmd-parse-fuzzer.dict new file mode 100644 index 00000000..cbd933f9 --- /dev/null +++ b/fuzz/cmd-parse-fuzzer.dict @@ -0,0 +1,133 @@ +# tmux command names +"set-option" +"bind-key" +"unbind-key" +"send-keys" +"new-session" +"new-window" +"split-window" +"select-window" +"select-pane" +"kill-pane" +"kill-window" +"kill-session" +"kill-server" +"list-sessions" +"list-windows" +"list-panes" +"list-keys" +"list-buffers" +"list-clients" +"list-commands" +"attach-session" +"detach-client" +"switch-client" +"rename-session" +"rename-window" +"resize-pane" +"resize-window" +"display-message" +"display-menu" +"display-popup" +"display-panes" +"copy-mode" +"paste-buffer" +"capture-pane" +"save-buffer" +"load-buffer" +"set-buffer" +"delete-buffer" +"show-buffer" +"choose-buffer" +"choose-tree" +"choose-client" +"if-shell" +"run-shell" +"source-file" +"command-prompt" +"confirm-before" +"pipe-pane" +"wait-for" +"set-environment" +"show-environment" +"set-hook" +"show-hooks" +"show-messages" +"show-options" +"set-window-option" +"show-window-options" +"clear-history" +"clock-mode" +"find-window" +"join-pane" +"move-pane" +"break-pane" +"swap-pane" +"swap-window" +"move-window" +"link-window" +"unlink-window" +"rotate-window" +"next-window" +"previous-window" +"last-window" +"last-pane" +"next-layout" +"previous-layout" +"select-layout" +"customize-mode" +"refresh-client" +"suspend-client" +"lock-client" +"lock-server" +"lock-session" +"respawn-pane" +"respawn-window" +"start-server" +"server-access" +"send-prefix" +"clear-prompt-history" +"show-prompt-history" + +# Common flags and syntax +"-t" +"-s" +"-g" +"-w" +"-p" +"-a" +"-o" +"-q" +"-u" +"-F" +"-f" +"-b" +"-d" +"-e" +"-n" +"-r" + +# Targets and option names +"status" +"default" +"mouse" +"on" +"off" +"vi" +"emacs" +"set -g" +"set -s" +"setw" +"bind" +"unbind" + +# Syntax elements +"{" +"}" +";" +"#{" +"%if" +"%elif" +"%else" +"%endif" +"%hidden" diff --git a/fuzz/cmd-parse-fuzzer.options b/fuzz/cmd-parse-fuzzer.options new file mode 100644 index 00000000..60bd9b0b --- /dev/null +++ b/fuzz/cmd-parse-fuzzer.options @@ -0,0 +1,2 @@ +[libfuzzer] +max_len = 2048 diff --git a/fuzz/format-fuzzer.c b/fuzz/format-fuzzer.c new file mode 100644 index 00000000..d447c506 --- /dev/null +++ b/fuzz/format-fuzzer.c @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2026 David Korczynski + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * Fuzz the tmux format string expander (format_expand). + * + * This exercises: + * - format.c (format parsing, modifier chains, conditionals, math, regex) + * - colour.c (colour name and RGB parsing within formats) + * - utf8.c (UTF-8 width calculations in format padding) + */ + +#include +#include + +#include "tmux.h" + +struct event_base *libevent; + +int +LLVMFuzzerTestOneInput(const u_char *data, size_t size) +{ + struct format_tree *ft; + char *buf, *expanded; + + if (size > 2048 || size == 0) + return 0; + + /* Null-terminate the input for format_expand. */ + buf = malloc(size + 1); + if (buf == NULL) + return 0; + memcpy(buf, data, size); + buf[size] = '\0'; + + ft = format_create(NULL, NULL, 0, FORMAT_NOJOBS); + format_add(ft, "session_name", "%s", "fuzz-session"); + format_add(ft, "window_index", "%d", 0); + format_add(ft, "window_name", "%s", "fuzz-window"); + format_add(ft, "pane_index", "%d", 0); + format_add(ft, "pane_id", "%s", "%%0"); + format_add(ft, "host", "%s", "fuzzhost"); + format_add(ft, "pane_width", "%d", 80); + format_add(ft, "pane_height", "%d", 25); + + expanded = format_expand(ft, buf); + free(expanded); + format_free(ft); + + free(buf); + return 0; +} + +int +LLVMFuzzerInitialize(__unused int *argc, __unused char ***argv) +{ + const struct options_table_entry *oe; + + global_environ = environ_create(); + global_options = options_create(NULL); + global_s_options = options_create(NULL); + global_w_options = options_create(NULL); + for (oe = options_table; oe->name != NULL; oe++) { + if (oe->scope & OPTIONS_TABLE_SERVER) + options_default(global_options, oe); + if (oe->scope & OPTIONS_TABLE_SESSION) + options_default(global_s_options, oe); + if (oe->scope & OPTIONS_TABLE_WINDOW) + options_default(global_w_options, oe); + } + libevent = osdep_event_init(); + socket_path = xstrdup("dummy"); + + return 0; +} diff --git a/fuzz/format-fuzzer.dict b/fuzz/format-fuzzer.dict new file mode 100644 index 00000000..fb79cf9a --- /dev/null +++ b/fuzz/format-fuzzer.dict @@ -0,0 +1,71 @@ +# Format expansion syntax +"#{" +"}" +"#{?" +"#{==" +"#{!=" +"#{<" +"#{>" +"#{m:" +"#{C:" +"#{s/" +"#{t:" +"#{T:" +"#{E:" +"#{S:" +"#{W:" +"#{P:" +"##" + +# Common format modifiers +"b:" +"d:" +"q:" +"l:" +"e:" +"t:" +"p:" +"w:" +"n:" +"a:" +"=:" +"||" +"&&" +"," +"#{e|" +"#{p-1:" +"#{=/10/...:" + +# Format variables +"session_name" +"window_index" +"window_name" +"pane_index" +"pane_id" +"host" +"pane_width" +"pane_height" +"pane_current_command" +"pane_pid" +"session_id" +"window_id" +"client_name" +"version" +"line" + +# Conditionals +"#{?pane_active," +",}" +"#{?#{==:" + +# Math and comparison +"#{e|+:" +"#{e|-:" +"#{e|*:" +"#{e|/:" +"#{e|%:" + +# Nested +"#{=" +"#{" +"#{l:" diff --git a/fuzz/format-fuzzer.options b/fuzz/format-fuzzer.options new file mode 100644 index 00000000..60bd9b0b --- /dev/null +++ b/fuzz/format-fuzzer.options @@ -0,0 +1,2 @@ +[libfuzzer] +max_len = 2048 diff --git a/fuzz/style-fuzzer.c b/fuzz/style-fuzzer.c new file mode 100644 index 00000000..f9d40e61 --- /dev/null +++ b/fuzz/style-fuzzer.c @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026 David Korczynski + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * Fuzz the tmux style parser (style_parse). + * + * This exercises: + * - style.c (style string parsing, alignment, ranges) + * - colour.c (colour name, RGB, and indexed colour parsing) + */ + +#include +#include + +#include "tmux.h" + +struct event_base *libevent; + +int +LLVMFuzzerTestOneInput(const u_char *data, size_t size) +{ + struct style sy; + struct grid_cell gc; + char *buf; + + if (size > 512 || size == 0) + return 0; + + /* Null-terminate the input for style_parse. */ + buf = malloc(size + 1); + if (buf == NULL) + return 0; + memcpy(buf, data, size); + buf[size] = '\0'; + + memset(&gc, 0, sizeof gc); + style_set(&sy, &gc); + + style_parse(&sy, &gc, buf); + + free(buf); + return 0; +} + +int +LLVMFuzzerInitialize(__unused int *argc, __unused char ***argv) +{ + const struct options_table_entry *oe; + + global_environ = environ_create(); + global_options = options_create(NULL); + global_s_options = options_create(NULL); + global_w_options = options_create(NULL); + for (oe = options_table; oe->name != NULL; oe++) { + if (oe->scope & OPTIONS_TABLE_SERVER) + options_default(global_options, oe); + if (oe->scope & OPTIONS_TABLE_SESSION) + options_default(global_s_options, oe); + if (oe->scope & OPTIONS_TABLE_WINDOW) + options_default(global_w_options, oe); + } + libevent = osdep_event_init(); + socket_path = xstrdup("dummy"); + + return 0; +} diff --git a/fuzz/style-fuzzer.dict b/fuzz/style-fuzzer.dict new file mode 100644 index 00000000..2834734c --- /dev/null +++ b/fuzz/style-fuzzer.dict @@ -0,0 +1,73 @@ +# Style attributes +"default" +"ignore" +"nodefaults" +"bright" +"bold" +"dim" +"underscore" +"blink" +"reverse" +"hidden" +"italics" +"overline" +"strikethrough" +"double-underscore" +"curly-underscore" +"dotted-underscore" +"dashed-underscore" +"nobright" +"nobold" +"nodim" +"nounderscore" +"noblink" +"noreverse" +"nohidden" +"noitalics" +"nooverline" +"nostrikethrough" + +# Colours +"fg=" +"bg=" +"us=" +"fill=" +"colour0" +"colour255" +"red" +"green" +"blue" +"yellow" +"cyan" +"magenta" +"white" +"black" + +# RGB and hex +"#ff0000" +"#00ff00" +"#0000ff" + +# Alignment and ranges +"align=left" +"align=centre" +"align=right" +"align=absolute-centre" +"list=on" +"list=focus" +"list=left-marker" +"list=right-marker" +"range=left" +"range=right" +"range=pane" +"range=window" +"range=session" +"range=user" + +# Width and padding +"width=" +"pad=" + +# Delimiters +"," +" " diff --git a/fuzz/style-fuzzer.options b/fuzz/style-fuzzer.options new file mode 100644 index 00000000..5d468bc6 --- /dev/null +++ b/fuzz/style-fuzzer.options @@ -0,0 +1,2 @@ +[libfuzzer] +max_len = 512