diff --git a/README-NIX.md b/README-NIX.md index fd609704..0701daa1 100644 --- a/README-NIX.md +++ b/README-NIX.md @@ -74,12 +74,13 @@ ninja -C Build-fuzz ## Fuzzing -The project includes four fuzz targets built with [libFuzzer](https://llvm.org/docs/LibFuzzer.html): +The project includes five fuzz targets built with [libFuzzer](https://llvm.org/docs/LibFuzzer.html): | Fuzzer | Tests | |--------|-------| | `irssi-fuzz` | Text formatting (`printtext_string()`) | | `server-fuzz` | IRC protocol message parsing | +| `dcc-fuzz` | DCC protocol message parsing (SEND, CHAT, RESUME, ACCEPT) | | `event-get-params-fuzz` | IRC event parameter parsing | | `theme-load-fuzz` | Theme file loading | @@ -118,9 +119,10 @@ Initial seed inputs are provided in `fuzz-corpora/`: ```bash # Copy seeds to corpus directories -mkdir -p corpus/irssi-fuzz corpus/server-fuzz corpus/event-get-params-fuzz corpus/theme-load-fuzz +mkdir -p corpus/irssi-fuzz corpus/server-fuzz corpus/dcc-fuzz corpus/event-get-params-fuzz corpus/theme-load-fuzz cp fuzz-corpora/irssi-fuzz/* corpus/irssi-fuzz/ cp fuzz-corpora/server-fuzz/* corpus/server-fuzz/ +cp fuzz-corpora/dcc-fuzz/* corpus/dcc-fuzz/ cp fuzz-corpora/event-get-params-fuzz/* corpus/event-get-params-fuzz/ cp fuzz-corpora/theme-load-fuzz/* corpus/theme-load-fuzz/ ``` @@ -141,6 +143,7 @@ ASAN_OPTIONS=symbolize=1 ./result/bin/server-fuzz crash- - **irssi-fuzz**: Arbitrary text, may contain irssi format codes (`%B`, `%U`, etc.) - **server-fuzz**: Byte 0 selects prefix mode, remaining bytes are `\r\n`-separated IRC messages +- **dcc-fuzz**: Byte 0 selects DCC type (0=SEND, 1=CHAT, 2=RESUME, 3=ACCEPT, 4=GET cmd, 5=CLOSE cmd, 6=raw), remaining bytes are DCC message content - **event-get-params-fuzz**: Byte 0 selects parsing mode (0-7), remaining bytes are parameters - **theme-load-fuzz**: irssi theme file format diff --git a/fuzz-corpora/dcc-fuzz/dcc_accept_basic.seed b/fuzz-corpora/dcc-fuzz/dcc_accept_basic.seed new file mode 100644 index 00000000..73349201 --- /dev/null +++ b/fuzz-corpora/dcc-fuzz/dcc_accept_basic.seed @@ -0,0 +1 @@ +test.txt 1234 100 \ No newline at end of file diff --git a/fuzz-corpora/dcc-fuzz/dcc_chat_basic.seed b/fuzz-corpora/dcc-fuzz/dcc_chat_basic.seed new file mode 100644 index 00000000..6365c7e9 --- /dev/null +++ b/fuzz-corpora/dcc-fuzz/dcc_chat_basic.seed @@ -0,0 +1 @@ +chat 3232235777 1234 \ No newline at end of file diff --git a/fuzz-corpora/dcc-fuzz/dcc_close_cmd.seed b/fuzz-corpora/dcc-fuzz/dcc_close_cmd.seed new file mode 100644 index 00000000..db41856a --- /dev/null +++ b/fuzz-corpora/dcc-fuzz/dcc_close_cmd.seed @@ -0,0 +1 @@ +SEND testnick testfile.txt \ No newline at end of file diff --git a/fuzz-corpora/dcc-fuzz/dcc_get_cmd.seed b/fuzz-corpora/dcc-fuzz/dcc_get_cmd.seed new file mode 100644 index 00000000..b686b90c --- /dev/null +++ b/fuzz-corpora/dcc-fuzz/dcc_get_cmd.seed @@ -0,0 +1 @@ +testnick testfile.txt \ No newline at end of file diff --git a/fuzz-corpora/dcc-fuzz/dcc_raw_ctcp.seed b/fuzz-corpora/dcc-fuzz/dcc_raw_ctcp.seed new file mode 100644 index 00000000..3a8c387c --- /dev/null +++ b/fuzz-corpora/dcc-fuzz/dcc_raw_ctcp.seed @@ -0,0 +1 @@ +UNKNOWN test data \ No newline at end of file diff --git a/fuzz-corpora/dcc-fuzz/dcc_resume_basic.seed b/fuzz-corpora/dcc-fuzz/dcc_resume_basic.seed new file mode 100644 index 00000000..7120be98 --- /dev/null +++ b/fuzz-corpora/dcc-fuzz/dcc_resume_basic.seed @@ -0,0 +1 @@ +test.txt 1234 100 \ No newline at end of file diff --git a/fuzz-corpora/dcc-fuzz/dcc_send_basic.seed b/fuzz-corpora/dcc-fuzz/dcc_send_basic.seed new file mode 100644 index 00000000..449e610f Binary files /dev/null and b/fuzz-corpora/dcc-fuzz/dcc_send_basic.seed differ diff --git a/fuzz-corpora/dcc-fuzz/dcc_send_ipv6.seed b/fuzz-corpora/dcc-fuzz/dcc_send_ipv6.seed new file mode 100644 index 00000000..0019725f Binary files /dev/null and b/fuzz-corpora/dcc-fuzz/dcc_send_ipv6.seed differ diff --git a/fuzz-corpora/dcc-fuzz/dcc_send_minimal.seed b/fuzz-corpora/dcc-fuzz/dcc_send_minimal.seed new file mode 100644 index 00000000..388cb715 Binary files /dev/null and b/fuzz-corpora/dcc-fuzz/dcc_send_minimal.seed differ diff --git a/fuzz-corpora/dcc-fuzz/dcc_send_multiword.seed b/fuzz-corpora/dcc-fuzz/dcc_send_multiword.seed new file mode 100644 index 00000000..ea175227 Binary files /dev/null and b/fuzz-corpora/dcc-fuzz/dcc_send_multiword.seed differ diff --git a/fuzz-corpora/dcc-fuzz/dcc_send_passive.seed b/fuzz-corpora/dcc-fuzz/dcc_send_passive.seed new file mode 100644 index 00000000..ad890eb9 Binary files /dev/null and b/fuzz-corpora/dcc-fuzz/dcc_send_passive.seed differ diff --git a/fuzz-corpora/dcc-fuzz/dcc_send_quoted.seed b/fuzz-corpora/dcc-fuzz/dcc_send_quoted.seed new file mode 100644 index 00000000..9be7fcd5 Binary files /dev/null and b/fuzz-corpora/dcc-fuzz/dcc_send_quoted.seed differ diff --git a/src/fe-fuzz/dcc.c b/src/fe-fuzz/dcc.c new file mode 100644 index 00000000..8563f246 --- /dev/null +++ b/src/fe-fuzz/dcc.c @@ -0,0 +1,242 @@ +/* + dcc.c : irssi DCC fuzzer + + Copyright (C) 2018 Joseph Bisch + Copyright (C) 2025 irssi contributors + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +/* irc-core.c */ +void irc_core_init(void); +void irc_core_deinit(void); + +/* irc-session.c */ +void irc_session_init(void); +void irc_session_deinit(void); + +/* fe-common-irc.c */ +void fe_common_irc_init(void); +void fe_common_irc_deinit(void); + +SERVER_REC *server; + +void event_connected(IRC_SERVER_REC *server, const char *data, const char *from) +{ + char *params, *nick; + + g_return_if_fail(server != NULL); + + params = event_get_params(data, 1, &nick); + + if (g_strcmp0(server->nick, nick) != 0) { + /* nick changed unexpectedly .. connected via proxy, etc. */ + g_free(server->nick); + server->nick = g_strdup(nick); + } + + /* set the server address */ + g_free(server->real_address); + server->real_address = from == NULL ? + g_strdup(server->connrec->address) : /* shouldn't happen.. */ + g_strdup(from); + + /* last welcome message found - commands can be sent to server now. */ + server->connected = 1; + server->real_connect_time = time(NULL); + + /* let the queue send now that we are identified */ + g_get_current_time(&server->wait_cmd); + + if (server->connrec->usermode != NULL) { + /* Send the user mode, before the autosendcmd. + * Do not pass this through cmd_mode because it + * is not known whether the resulting MODE message + * (if any) is the initial umode or a reply to this. + */ + irc_send_cmdv(server, "MODE %s %s", server->nick, + server->connrec->usermode); + g_free_not_null(server->wanted_usermode); + server->wanted_usermode = g_strdup(server->connrec->usermode); + } + + signal_emit("event connected", 1, server); + g_free(params); +} + +void irc_server_init_bare_minimum(IRC_SERVER_REC *server) { + server->rawlog = rawlog_create(); + + /* isupport is already created by server_init_connect, just populate it */ + g_hash_table_insert(server->isupport, g_strdup("CHANMODES"), g_strdup("beI,k,l,imnpst")); + g_hash_table_insert(server->isupport, g_strdup("PREFIX"), g_strdup("(ohv)@%+")); +} + +void test_server() { + CHAT_PROTOCOL_REC *proto; + SERVER_CONNECT_REC *conn; + GIOChannel *handle = g_io_channel_unix_new(open("/dev/null", O_RDWR)); + g_io_channel_set_encoding(handle, NULL, NULL); + g_io_channel_set_close_on_unref(handle, TRUE); + + proto = chat_protocol_find("IRC"); + conn = server_create_conn(proto->id, "localhost", 0, "", "", "user"); + server = proto->server_init_connect(conn); + server->session_reconnect = TRUE; + g_free(server->tag); + server->tag = g_strdup("testserver"); + server->handle = net_sendbuffer_create(handle, 0); + + /* we skip some initialisations that would try to send data */ + irc_session_deinit(); + irc_irc_deinit(); + + server_connect_finished(server); + + /* make up for the skipped session init */ + irc_server_init_bare_minimum(IRC_SERVER(server)); + + irc_irc_init(); + irc_session_init(); + + server_connect_unref(conn); +} + +int LLVMFuzzerInitialize(int *argc, char ***argv) { +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + g_log_set_null_logger(); +#endif + core_register_options(); + fe_common_core_register_options(); + /* no args */ + args_execute(0, NULL); + core_preinit((*argv)[0]); + core_init(); + irssi_ssl_init(); + irc_core_init(); + fe_common_core_init(); + fe_common_irc_init(); + signal_add("event 001", (SIGNAL_FUNC) event_connected); + module_register("core", "fe-fuzz"); + rawlog_set_size(1); + return 0; +} + +/* + * DCC fuzzer input format: + * Byte 0: DCC type selector + * 0 = DCC SEND + * 1 = DCC CHAT + * 2 = DCC RESUME + * 3 = DCC ACCEPT + * 4 = DCC GET (command parsing) + * 5 = DCC CLOSE (command parsing) + * 6+ = raw CTCP DCC message + * + * Remaining bytes: DCC message content (after "DCC ") + */ +int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + gchar *copy; + gchar *ctcp_line; + gchar *irc_line; + int dcc_type; + int disconnected; + + if (size < 2) return 0; + + test_server(); + + dcc_type = data[0] % 7; + copy = g_strndup((const gchar *)data+1, size-1); + + /* Replace any NUL bytes with spaces to allow fuzzing of full data */ + for (size_t i = 0; i < size-1; i++) { + if (copy[i] == '\0') copy[i] = ' '; + } + + switch (dcc_type) { + case 0: /* DCC SEND - file transfer offer */ + ctcp_line = g_strdup_printf("DCC SEND %s", copy); + break; + case 1: /* DCC CHAT - chat request */ + ctcp_line = g_strdup_printf("DCC CHAT %s", copy); + break; + case 2: /* DCC RESUME - resume file transfer */ + ctcp_line = g_strdup_printf("DCC RESUME %s", copy); + break; + case 3: /* DCC ACCEPT - accept resume */ + ctcp_line = g_strdup_printf("DCC ACCEPT %s", copy); + break; + case 4: /* DCC GET command parsing */ + /* Test the command parsing path via "dcc get" command */ + signal_emit("command dcc get", 3, copy, server, NULL); + g_free(copy); + goto cleanup; + case 5: /* DCC CLOSE command parsing */ + /* Test the command parsing path via "dcc close" command */ + signal_emit("command dcc close", 3, copy, server, NULL); + g_free(copy); + goto cleanup; + default: /* Raw CTCP DCC message */ + ctcp_line = g_strdup_printf("DCC %s", copy); + break; + } + + /* Emit the DCC CTCP message signal directly + * This is what happens when a CTCP message is received: + * server, data, nick, addr, target, chat */ + server_ref(server); + signal_emit("ctcp msg dcc", 6, server, ctcp_line, + "fuzzernick", "fuzzer@host.example.com", "testnick", NULL); + disconnected = server->disconnected; + server_unref(server); + + g_free(ctcp_line); + g_free(copy); + +cleanup: + if (server->disconnected) { + test_server(); + } else { + server_disconnect(server); + } + return 0; +} diff --git a/src/fe-fuzz/meson.build b/src/fe-fuzz/meson.build index 5ab19651..fa7f26da 100644 --- a/src/fe-fuzz/meson.build +++ b/src/fe-fuzz/meson.build @@ -45,6 +45,29 @@ executable('server-fuzz', dependencies : dep ) +executable('dcc-fuzz', + files( + 'null-logger.c', + 'dcc.c', + '../fe-text/module-formats.c', + ), + link_with : [ + libconfig_a, + libcore_a, + libfuzzer_fe_common_core_a, + libirc_core_a, + libfe_common_irc_a, + libfe_irc_dcc_a, + libfe_irc_notifylist_a, + ], + link_args : [fuzzer_lib], + link_language : fuzzer_link_language, + include_directories : rootinc, + implicit_include_directories : false, + install : true, + dependencies : dep +) + # noinst_headers = files( # 'null-logger.h', # '../fe-text/module-formats.h', diff --git a/src/fe-fuzz/server.c b/src/fe-fuzz/server.c index 25579c8c..e22d073f 100644 --- a/src/fe-fuzz/server.c +++ b/src/fe-fuzz/server.c @@ -103,9 +103,8 @@ void event_connected(IRC_SERVER_REC *server, const char *data, const char *from) void irc_server_init_bare_minimum(IRC_SERVER_REC *server) { server->rawlog = rawlog_create(); - server->isupport = g_hash_table_new((GHashFunc) i_istr_hash, (GCompareFunc) i_istr_equal); - /* set the standards */ + /* isupport is already created by server_init_connect, just populate it */ g_hash_table_insert(server->isupport, g_strdup("CHANMODES"), g_strdup("beI,k,l,imnpst")); g_hash_table_insert(server->isupport, g_strdup("PREFIX"), g_strdup("(ohv)@%+")); }