From 0fb8ed6a34159d68fc24d10a9e656e6788d0d058 Mon Sep 17 00:00:00 2001 From: Chris Allen Date: Sat, 27 Dec 2025 13:01:03 -0600 Subject: [PATCH 1/7] flake.nix --- flake.lock | 61 +++++++++++++++++++++++++++ flake.nix | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..f8964420 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1766651565, + "narHash": "sha256-QEhk0eXgyIqTpJ/ehZKg9IKS7EtlWxF3N7DXy42zPfU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3e2499d5539c16d0d173ba53552a4ff8547f4539", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..a616b077 --- /dev/null +++ b/flake.nix @@ -0,0 +1,119 @@ +{ + description = "Irssi - A modular text mode chat client with IRC support"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + + # Common build inputs for both the package and dev shell + buildInputs = with pkgs; [ + glib + openssl + ncurses + perl + ] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ + utf8proc + ]; + + # Native build inputs (tools needed at build time) + nativeBuildInputs = with pkgs; [ + meson + ninja + pkg-config + perl + ]; + + in + { + packages = { + default = self.packages.${system}.irssi; + + irssi = pkgs.stdenv.mkDerivation { + pname = "irssi"; + version = "1.5-head"; + + src = ./.; + + inherit nativeBuildInputs buildInputs; + + mesonFlags = [ + "-Dwith-perl=yes" + "-Dwith-proxy=yes" + ]; + + meta = with pkgs.lib; { + description = "A modular text mode chat client with IRC support"; + homepage = "https://irssi.org/"; + license = licenses.gpl2Plus; + maintainers = []; + platforms = platforms.unix; + }; + }; + + # Variant without Perl scripting support + irssi-minimal = pkgs.stdenv.mkDerivation { + pname = "irssi-minimal"; + version = "1.5-head"; + + src = ./.; + + nativeBuildInputs = with pkgs; [ + meson + ninja + pkg-config + perl # Perl is needed at build time for generating help files + ]; + + buildInputs = with pkgs; [ + glib + openssl + ncurses + ]; + + mesonFlags = [ + "-Dwith-perl=no" + ]; + + meta = with pkgs.lib; { + description = "A modular text mode chat client with IRC support (minimal build)"; + homepage = "https://irssi.org/"; + license = licenses.gpl2Plus; + platforms = platforms.unix; + }; + }; + }; + + devShells.default = pkgs.mkShell { + name = "irssi-dev"; + + inherit buildInputs; + + nativeBuildInputs = nativeBuildInputs ++ (with pkgs; [ + # Additional development tools + gdb + valgrind + ] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ + strace + ]); + + shellHook = '' + echo "Irssi development environment" + echo "" + echo "Build commands:" + echo " meson setup Build" + echo " ninja -C Build" + echo "" + echo "Run tests:" + echo " ninja -C Build test" + echo "" + ''; + }; + } + ); +} From 3a5a2f8204fcd10f71343a0cc5f4320654deca30 Mon Sep 17 00:00:00 2001 From: Chris Allen Date: Sat, 27 Dec 2025 13:44:28 -0600 Subject: [PATCH 2/7] fuzzers --- .gitignore | 1 + flake.nix | 163 ++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 143 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index ea2cd7ba..f626a59b 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,4 @@ subprojects/* Irssi-Dist setup.cfg *.egg-info +result diff --git a/flake.nix b/flake.nix index a616b077..93edcc41 100644 --- a/flake.nix +++ b/flake.nix @@ -29,6 +29,72 @@ perl ]; + # Build fuzzers with clang and libfuzzer + # sanitizers: list of sanitizers to enable (e.g., ["address" "undefined"]) + mkFuzzerPackage = { sanitizers ? [ "address" "undefined" ] }: + let + # Use clang's stdenv for libfuzzer support + clangStdenv = pkgs.llvmPackages.stdenv; + + # Build sanitizer flags for linking (includes fuzzer) + sanitizerFlags = pkgs.lib.concatMapStringsSep "," (s: s) sanitizers; + fullSanitizerFlags = + if sanitizers == [] then "-fsanitize=fuzzer" + else "-fsanitize=fuzzer,${sanitizerFlags}"; + + # Compile-time sanitizer flags (without fuzzer - meson adds fuzzer-no-link) + compileSanitizerFlags = + if sanitizers == [] then "" + else "-fsanitize=${sanitizerFlags}"; + in + clangStdenv.mkDerivation { + pname = "irssi-fuzz"; + version = "1.5-head"; + + src = ./.; + + nativeBuildInputs = with pkgs; [ + meson + ninja + pkg-config + perl # Needed at build time for generating help files + ]; + + buildInputs = with pkgs; [ + glib + openssl + ncurses + ]; + + # Use preConfigure to set CFLAGS/LDFLAGS since meson's -Dc_args + # breaks the initial compiler test when sanitizers are involved + preConfigure = pkgs.lib.optionalString (sanitizers != []) '' + export CFLAGS="-g -O1 -fno-omit-frame-pointer ${compileSanitizerFlags}" + export LDFLAGS="${compileSanitizerFlags}" + ''; + + mesonFlags = [ + "-Dwith-perl=no" + "-Dwithout-textui=yes" + "-Dwith-fuzzer=yes" + "-Dwith-fuzzer-lib=${fullSanitizerFlags}" + "-Dfuzzer-link-language=c" + ]; + + # Only install the fuzzer binaries + postInstall = '' + # Remove non-fuzzer files if any were installed + rm -rf $out/share $out/include $out/lib/pkgconfig || true + ''; + + meta = with pkgs.lib; { + description = "Irssi fuzz testing targets built with libFuzzer"; + homepage = "https://irssi.org/"; + license = licenses.gpl2Plus; + platforms = platforms.unix; + }; + }; + in { packages = { @@ -87,32 +153,87 @@ platforms = platforms.unix; }; }; + + # Fuzzers with AddressSanitizer + UndefinedBehaviorSanitizer (recommended) + fuzz = mkFuzzerPackage { + sanitizers = [ "address" "undefined" ]; + }; + + # Fuzzers with only AddressSanitizer (faster, catches memory errors) + fuzz-asan = mkFuzzerPackage { + sanitizers = [ "address" ]; + }; + + # Fuzzers with only UndefinedBehaviorSanitizer + fuzz-ubsan = mkFuzzerPackage { + sanitizers = [ "undefined" ]; + }; }; - devShells.default = pkgs.mkShell { - name = "irssi-dev"; + devShells = { + default = pkgs.mkShell { + name = "irssi-dev"; - inherit buildInputs; + inherit buildInputs; - nativeBuildInputs = nativeBuildInputs ++ (with pkgs; [ - # Additional development tools - gdb - valgrind - ] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ - strace - ]); + nativeBuildInputs = nativeBuildInputs ++ (with pkgs; [ + # Additional development tools + gdb + valgrind + ] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ + strace + ]); - shellHook = '' - echo "Irssi development environment" - echo "" - echo "Build commands:" - echo " meson setup Build" - echo " ninja -C Build" - echo "" - echo "Run tests:" - echo " ninja -C Build test" - echo "" - ''; + shellHook = '' + echo "Irssi development environment" + echo "" + echo "Build commands:" + echo " meson setup Build" + echo " ninja -C Build" + echo "" + echo "Run tests:" + echo " ninja -C Build test" + echo "" + ''; + }; + + # Development shell for fuzzing + fuzz = pkgs.mkShell.override { stdenv = pkgs.llvmPackages.stdenv; } { + name = "irssi-fuzz-dev"; + + buildInputs = with pkgs; [ + glib + openssl + ncurses + ]; + + nativeBuildInputs = with pkgs; [ + meson + ninja + pkg-config + perl + # Fuzzing tools + llvmPackages.llvm # For llvm-symbolizer, llvm-cov, etc. + ]; + + shellHook = '' + echo "Irssi fuzzing development environment (clang + libFuzzer)" + echo "" + echo "Build fuzzers:" + echo " meson setup Build-fuzz -Dwith-perl=no -Dwithout-textui=yes -Dwith-fuzzer=yes" + echo " ninja -C Build-fuzz" + echo "" + echo "Fuzz targets will be in Build-fuzz/src/fe-fuzz/:" + echo " - irssi-fuzz" + echo " - server-fuzz" + echo " - event-get-params-fuzz (in irc/core/)" + echo " - theme-load-fuzz (in fe-common/core/)" + echo "" + echo "Run a fuzzer:" + echo " ./Build-fuzz/src/fe-fuzz/irssi-fuzz corpus/" + echo "" + ''; + }; }; } ); From 40594d9eeecf0ad4d19424c17c5eb02ad98ca7c6 Mon Sep 17 00:00:00 2001 From: Chris Allen Date: Sat, 27 Dec 2025 15:10:10 -0600 Subject: [PATCH 3/7] README-NIX.md, .gitignore for corpus gen --- .gitignore | 1 + README-NIX.md | 175 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 README-NIX.md diff --git a/.gitignore b/.gitignore index f626a59b..724f6157 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,4 @@ Irssi-Dist setup.cfg *.egg-info result +corpus/ diff --git a/README-NIX.md b/README-NIX.md new file mode 100644 index 00000000..fd609704 --- /dev/null +++ b/README-NIX.md @@ -0,0 +1,175 @@ +# Building Irssi with Nix + +This project includes a Nix flake for reproducible builds and development environments. + +## Prerequisites + +- [Nix](https://nixos.org/download.html) with flakes enabled + +To enable flakes, add to `~/.config/nix/nix.conf`: +``` +experimental-features = nix-command flakes +``` + +## Quick Start + +```bash +# Build irssi +nix build + +# Run irssi +./result/bin/irssi + +# Enter development shell +nix develop +``` + +## Available Packages + +| Package | Description | +|---------|-------------| +| `irssi` (default) | Full build with Perl scripting and proxy support | +| `irssi-minimal` | Build without Perl scripting support | +| `fuzz` | Fuzz targets with AddressSanitizer + UndefinedBehaviorSanitizer | +| `fuzz-asan` | Fuzz targets with AddressSanitizer only | +| `fuzz-ubsan` | Fuzz targets with UndefinedBehaviorSanitizer only | + +### Building Packages + +```bash +# Build default (full irssi) +nix build + +# Build minimal variant +nix build .#irssi-minimal + +# Build fuzzers (recommended: ASan + UBSan) +nix build .#fuzz + +# Build fuzzers with only ASan (faster) +nix build .#fuzz-asan +``` + +## Development Shells + +| Shell | Description | +|-------|-------------| +| `default` | Standard development with gcc, gdb, valgrind | +| `fuzz` | Fuzzing development with clang + libFuzzer | + +### Using Development Shells + +```bash +# Standard development +nix develop +meson setup Build +ninja -C Build +ninja -C Build test + +# Fuzzing development +nix develop .#fuzz +meson setup Build-fuzz -Dwith-perl=no -Dwithout-textui=yes -Dwith-fuzzer=yes +ninja -C Build-fuzz +``` + +## Fuzzing + +The project includes four 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 | +| `event-get-params-fuzz` | IRC event parameter parsing | +| `theme-load-fuzz` | Theme file loading | + +### Building Fuzzers + +```bash +# Build with ASan + UBSan (recommended for finding bugs) +nix build .#fuzz + +# Build with ASan only (faster execution) +nix build .#fuzz-asan +``` + +### Running Fuzzers + +```bash +# Basic fuzzing (creates corpus automatically) +./result/bin/irssi-fuzz corpus/irssi-fuzz/ + +# With dictionary (recommended for server-fuzz) +./result/bin/server-fuzz -dict=src/fe-fuzz/tokens.txt corpus/server-fuzz/ + +# Limit number of runs +./result/bin/irssi-fuzz -runs=10000 corpus/irssi-fuzz/ + +# Parallel fuzzing (use multiple cores) +./result/bin/server-fuzz -fork=4 -dict=src/fe-fuzz/tokens.txt corpus/server-fuzz/ + +# Ignore memory leaks to focus on crashes +./result/bin/server-fuzz -detect_leaks=0 corpus/server-fuzz/ +``` + +### Seed Corpus + +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 +cp fuzz-corpora/irssi-fuzz/* corpus/irssi-fuzz/ +cp fuzz-corpora/server-fuzz/* corpus/server-fuzz/ +cp fuzz-corpora/event-get-params-fuzz/* corpus/event-get-params-fuzz/ +cp fuzz-corpora/theme-load-fuzz/* corpus/theme-load-fuzz/ +``` + +### Reproducing Crashes + +When a fuzzer finds a crash, it saves the input to a file: + +```bash +# Reproduce a crash +./result/bin/server-fuzz crash- + +# Get more details with symbolized stack trace +ASAN_OPTIONS=symbolize=1 ./result/bin/server-fuzz crash- +``` + +### Fuzzer Input Formats + +- **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 +- **event-get-params-fuzz**: Byte 0 selects parsing mode (0-7), remaining bytes are parameters +- **theme-load-fuzz**: irssi theme file format + +## Continuous Integration + +To check that everything builds: + +```bash +nix flake check +``` + +## Troubleshooting + +### UBSan warnings about function pointer types + +The warning about `signals.c` function pointer types is expected: +``` +runtime error: call to function through pointer to incorrect function type +``` + +This is due to irssi's dynamic signal dispatch system and is a known pattern in the codebase. + +### Fuzzer stops immediately + +Ensure the corpus directory exists: +```bash +mkdir -p corpus/irssi-fuzz +``` + +### Build fails with "perl not found" + +Perl is required even for minimal/fuzzer builds (for generating help files). The Nix flake handles this automatically. From d1144bb50bc55bb3f6b18fbcdbb0fa3352028050 Mon Sep 17 00:00:00 2001 From: Chris Allen Date: Sat, 27 Dec 2025 15:11:42 -0600 Subject: [PATCH 4/7] fuzzer seeds --- .gitignore | 8 ++++++++ fuzz-corpora/event-get-params-fuzz/seed1 | Bin 0 -> 23 bytes fuzz-corpora/event-get-params-fuzz/seed2 | 1 + fuzz-corpora/event-get-params-fuzz/seed3 | 1 + fuzz-corpora/event-get-params-fuzz/seed4 | 1 + fuzz-corpora/irssi-fuzz/seed1 | 1 + fuzz-corpora/irssi-fuzz/seed2 | 1 + fuzz-corpora/irssi-fuzz/seed3 | 1 + fuzz-corpora/server-fuzz/seed1 | Bin 0 -> 35 bytes fuzz-corpora/server-fuzz/seed2 | Bin 0 -> 15 bytes fuzz-corpora/server-fuzz/seed3 | Bin 0 -> 48 bytes fuzz-corpora/server-fuzz/seed4 | 1 + fuzz-corpora/server-fuzz/seed5 | Bin 0 -> 50 bytes fuzz-corpora/server-fuzz/seed6 | Bin 0 -> 32 bytes fuzz-corpora/theme-load-fuzz/seed1 | 5 +++++ fuzz-corpora/theme-load-fuzz/seed2 | 2 ++ 16 files changed, 22 insertions(+) create mode 100644 fuzz-corpora/event-get-params-fuzz/seed1 create mode 100644 fuzz-corpora/event-get-params-fuzz/seed2 create mode 100644 fuzz-corpora/event-get-params-fuzz/seed3 create mode 100644 fuzz-corpora/event-get-params-fuzz/seed4 create mode 100644 fuzz-corpora/irssi-fuzz/seed1 create mode 100644 fuzz-corpora/irssi-fuzz/seed2 create mode 100644 fuzz-corpora/irssi-fuzz/seed3 create mode 100644 fuzz-corpora/server-fuzz/seed1 create mode 100644 fuzz-corpora/server-fuzz/seed2 create mode 100644 fuzz-corpora/server-fuzz/seed3 create mode 100644 fuzz-corpora/server-fuzz/seed4 create mode 100644 fuzz-corpora/server-fuzz/seed5 create mode 100644 fuzz-corpora/server-fuzz/seed6 create mode 100644 fuzz-corpora/theme-load-fuzz/seed1 create mode 100644 fuzz-corpora/theme-load-fuzz/seed2 diff --git a/.gitignore b/.gitignore index 724f6157..cb8d0dad 100644 --- a/.gitignore +++ b/.gitignore @@ -90,4 +90,12 @@ Irssi-Dist setup.cfg *.egg-info result +Build-fuzz + +# Fuzzing artifacts (runtime corpus and crash/leak files) corpus/ +crash-* +leak-* +oom-* +timeout-* +slow-unit-* diff --git a/fuzz-corpora/event-get-params-fuzz/seed1 b/fuzz-corpora/event-get-params-fuzz/seed1 new file mode 100644 index 0000000000000000000000000000000000000000..b6c6a129ab564414f06d71865fcd99f7d7656dce GIT binary patch literal 23 ecmZR`%S_HzP)^QB%*#v7QLxHQEiO(>PXz#3^#}m~ literal 0 HcmV?d00001 diff --git a/fuzz-corpora/event-get-params-fuzz/seed2 b/fuzz-corpora/event-get-params-fuzz/seed2 new file mode 100644 index 00000000..a32448a0 --- /dev/null +++ b/fuzz-corpora/event-get-params-fuzz/seed2 @@ -0,0 +1 @@ +user1 user2 user3 \ No newline at end of file diff --git a/fuzz-corpora/event-get-params-fuzz/seed3 b/fuzz-corpora/event-get-params-fuzz/seed3 new file mode 100644 index 00000000..803414c8 --- /dev/null +++ b/fuzz-corpora/event-get-params-fuzz/seed3 @@ -0,0 +1 @@ +:server PRIVMSG #channel :test \ No newline at end of file diff --git a/fuzz-corpora/event-get-params-fuzz/seed4 b/fuzz-corpora/event-get-params-fuzz/seed4 new file mode 100644 index 00000000..6dcb0711 --- /dev/null +++ b/fuzz-corpora/event-get-params-fuzz/seed4 @@ -0,0 +1 @@ +one two three four \ No newline at end of file diff --git a/fuzz-corpora/irssi-fuzz/seed1 b/fuzz-corpora/irssi-fuzz/seed1 new file mode 100644 index 00000000..802992c4 --- /dev/null +++ b/fuzz-corpora/irssi-fuzz/seed1 @@ -0,0 +1 @@ +Hello world diff --git a/fuzz-corpora/irssi-fuzz/seed2 b/fuzz-corpora/irssi-fuzz/seed2 new file mode 100644 index 00000000..5ce9b8d9 --- /dev/null +++ b/fuzz-corpora/irssi-fuzz/seed2 @@ -0,0 +1 @@ +Test with colors diff --git a/fuzz-corpora/irssi-fuzz/seed3 b/fuzz-corpora/irssi-fuzz/seed3 new file mode 100644 index 00000000..3cdaeedf --- /dev/null +++ b/fuzz-corpora/irssi-fuzz/seed3 @@ -0,0 +1 @@ +%Bbold%n %Uunderline%n %9reverse%n diff --git a/fuzz-corpora/server-fuzz/seed1 b/fuzz-corpora/server-fuzz/seed1 new file mode 100644 index 0000000000000000000000000000000000000000..a6945aee18418d97144d999402bb6d72231fb611 GIT binary patch literal 35 qcmZRuDo!mbOD$3`Ffdfe%S_HzunJGjNzTtrRVc|<@CZo%S+8su<}UF$;nqJ&o9bJ;pGAV DhN}y@G&@Ri*OiooON=+}#Ni5>! F0sxCc56%Dp literal 0 HcmV?d00001 diff --git a/fuzz-corpora/server-fuzz/seed6 b/fuzz-corpora/server-fuzz/seed6 new file mode 100644 index 0000000000000000000000000000000000000000..bd4512c021cb89df3309220888f2e75fc46bbcf1 GIT binary patch literal 32 ncmZRu%F9g7RxB+}Epo`nFD_9C4D}3Au<}VwEX&MG=j8$buIvg} literal 0 HcmV?d00001 diff --git a/fuzz-corpora/theme-load-fuzz/seed1 b/fuzz-corpora/theme-load-fuzz/seed1 new file mode 100644 index 00000000..e5833d51 --- /dev/null +++ b/fuzz-corpora/theme-load-fuzz/seed1 @@ -0,0 +1,5 @@ +abstracts = { + line_start = "%B-%W!%B-%n "; + timestamp = "%K$0-%n "; + hilight = "%_$*%_"; +}; diff --git a/fuzz-corpora/theme-load-fuzz/seed2 b/fuzz-corpora/theme-load-fuzz/seed2 new file mode 100644 index 00000000..ae19c434 --- /dev/null +++ b/fuzz-corpora/theme-load-fuzz/seed2 @@ -0,0 +1,2 @@ +default_color = "-1"; +replaces = { "[]=" = "%K$*%n"; }; From f575595ae92a4dc8ca1cba71a0e1d23ff94944b9 Mon Sep 17 00:00:00 2001 From: Chris Allen Date: Sat, 27 Dec 2025 17:01:51 -0600 Subject: [PATCH 5/7] dcc-fuzz --- README-NIX.md | 7 +- fuzz-corpora/dcc-fuzz/dcc_accept_basic.seed | 1 + fuzz-corpora/dcc-fuzz/dcc_chat_basic.seed | 1 + fuzz-corpora/dcc-fuzz/dcc_close_cmd.seed | 1 + fuzz-corpora/dcc-fuzz/dcc_get_cmd.seed | 1 + fuzz-corpora/dcc-fuzz/dcc_raw_ctcp.seed | 1 + fuzz-corpora/dcc-fuzz/dcc_resume_basic.seed | 1 + fuzz-corpora/dcc-fuzz/dcc_send_basic.seed | Bin 0 -> 30 bytes fuzz-corpora/dcc-fuzz/dcc_send_ipv6.seed | Bin 0 -> 23 bytes fuzz-corpora/dcc-fuzz/dcc_send_minimal.seed | Bin 0 -> 8 bytes fuzz-corpora/dcc-fuzz/dcc_send_multiword.seed | Bin 0 -> 45 bytes fuzz-corpora/dcc-fuzz/dcc_send_passive.seed | Bin 0 -> 28 bytes fuzz-corpora/dcc-fuzz/dcc_send_quoted.seed | Bin 0 -> 44 bytes src/fe-fuzz/dcc.c | 242 ++++++++++++++++++ src/fe-fuzz/meson.build | 23 ++ src/fe-fuzz/server.c | 3 +- 16 files changed, 277 insertions(+), 4 deletions(-) create mode 100644 fuzz-corpora/dcc-fuzz/dcc_accept_basic.seed create mode 100644 fuzz-corpora/dcc-fuzz/dcc_chat_basic.seed create mode 100644 fuzz-corpora/dcc-fuzz/dcc_close_cmd.seed create mode 100644 fuzz-corpora/dcc-fuzz/dcc_get_cmd.seed create mode 100644 fuzz-corpora/dcc-fuzz/dcc_raw_ctcp.seed create mode 100644 fuzz-corpora/dcc-fuzz/dcc_resume_basic.seed create mode 100644 fuzz-corpora/dcc-fuzz/dcc_send_basic.seed create mode 100644 fuzz-corpora/dcc-fuzz/dcc_send_ipv6.seed create mode 100644 fuzz-corpora/dcc-fuzz/dcc_send_minimal.seed create mode 100644 fuzz-corpora/dcc-fuzz/dcc_send_multiword.seed create mode 100644 fuzz-corpora/dcc-fuzz/dcc_send_passive.seed create mode 100644 fuzz-corpora/dcc-fuzz/dcc_send_quoted.seed create mode 100644 src/fe-fuzz/dcc.c 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 0000000000000000000000000000000000000000..449e610f0582ed93d3ebb686ebc49f51130f72ce GIT binary patch literal 30 lcmZQ5Ni8nXE2$_^Fg7waGBP$bH#b)>G%_|(Ff=eQ004jZ2SWe= literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0019725f0abfd7181519d099ee875de73485d31e GIT binary patch literal 23 ecmZQ5Ni8nXE2$_^u(C2#Ff=kYQ7|+xFaQ8hTLs?$ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..388cb715debff309abf82c3858cfaef64c547646 GIT binary patch literal 8 McmZQbR4@cV00rm)RR910 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ea175227f053d4d1efec432f1efffd79d171d575 GIT binary patch literal 45 zcmZR`EzK#(R4C6cN>ND5%t_5l%uUrRsVGq}HZnFcGB!0gH&-w;GB!~#G%zp#08^k0 A>i_@% literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ad890eb9e99baf7c429bc8775db103aeb7072c4f GIT binary patch literal 28 jcmZQ5Ni8nXE2$_^Ff_9;F*Y!;R4`C5G%zqwFfjrEaH$5x literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9be7fcd580363750608a2d46ee7f2936c4eeec58 GIT binary patch literal 44 zcmZQjO3TbiRVdFa$xtXRNK8&G)+?zfQBp8AGBz?YHZ?alS1>d(Hc>D%FfafBEHMk> literal 0 HcmV?d00001 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)@%+")); } From fd496acb10f4fb70b230a0b73a2bca9aca7a153f Mon Sep 17 00:00:00 2001 From: Chris Allen Date: Sat, 27 Dec 2025 17:08:22 -0600 Subject: [PATCH 6/7] .gitignore --- .gitignore | 1 + fuzz-corpora/.gitignore | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 fuzz-corpora/.gitignore diff --git a/.gitignore b/.gitignore index cb8d0dad..2d410cf6 100644 --- a/.gitignore +++ b/.gitignore @@ -90,6 +90,7 @@ Irssi-Dist setup.cfg *.egg-info result +result-* Build-fuzz # Fuzzing artifacts (runtime corpus and crash/leak files) diff --git a/fuzz-corpora/.gitignore b/fuzz-corpora/.gitignore new file mode 100644 index 00000000..89012106 --- /dev/null +++ b/fuzz-corpora/.gitignore @@ -0,0 +1,4 @@ +# Ignore libFuzzer-generated corpus files (40-char SHA1 hashes) +# Only manually-created seed files with descriptive names should be committed here +# Runtime corpus should go in corpus/ (which is gitignored at the repo root) +[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f] From 06d1c2b1408d75fabb4f9abb00cd39afa14746da Mon Sep 17 00:00:00 2001 From: Chris Allen Date: Mon, 29 Dec 2025 22:07:45 -0600 Subject: [PATCH 7/7] dcc-fuzz docs, dcc-tokens --- README-NIX.md | 3 +- src/fe-fuzz/dcc-tokens.txt | 89 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/fe-fuzz/dcc-tokens.txt diff --git a/README-NIX.md b/README-NIX.md index 0701daa1..753d0105 100644 --- a/README-NIX.md +++ b/README-NIX.md @@ -100,8 +100,9 @@ nix build .#fuzz-asan # Basic fuzzing (creates corpus automatically) ./result/bin/irssi-fuzz corpus/irssi-fuzz/ -# With dictionary (recommended for server-fuzz) +# With dictionary (recommended for server-fuzz and dcc-fuzz) ./result/bin/server-fuzz -dict=src/fe-fuzz/tokens.txt corpus/server-fuzz/ +./result/bin/dcc-fuzz -dict=src/fe-fuzz/dcc-tokens.txt corpus/dcc-fuzz/ # Limit number of runs ./result/bin/irssi-fuzz -runs=10000 corpus/irssi-fuzz/ diff --git a/src/fe-fuzz/dcc-tokens.txt b/src/fe-fuzz/dcc-tokens.txt new file mode 100644 index 00000000..b9a10f3b --- /dev/null +++ b/src/fe-fuzz/dcc-tokens.txt @@ -0,0 +1,89 @@ +# DCC protocol fuzzer dictionary +# DCC command types +"SEND" +"GET" +"CHAT" +"RESUME" +"ACCEPT" +"REJECT" +"SERVER" + +# DCC server protocol numbers +"100" +"101" +"110" +"120" +"121" + +# CTCP markers +"\x01" +"\x01DCC" +"\x01DCC SEND" +"\x01DCC CHAT" + +# Special IP values +"16843009" +"3232235777" +"127.0.0.1" +"::1" + +# Port values +"0" +"1234" +"6667" +"65535" + +# File sizes +"1" +"100" +"1000" +"4294967295" + +# Passive IDs +"42" +"63" + +# Delimiters and quoting +"\"" +" " +":" + +# Protocol prefixes +"DCC " +"CTCP_MESSAGE " +"CTCP_REPLY " +"ACTION " + +# DCC server flags +"+" +"-" +"s" +"c" +"f" +"S" +"C" +"F" +"+s" +"+c" +"+f" +"-s" +"-c" +"-f" + +# Common filename patterns +"test.txt" +"file.txt" +"\"file with spaces.txt\"" +"file_with_underscores.txt" + +# Chat argument +"chat" +"CHAT" + +# Query prefix for DCC chat +"=" + +# Common nicks +"nick" +"testnick" +"fuzzernick"