commit 63d3d9f4a80aee35f586cd14ca9f421a92ea73be Author: Jeffrey C. Ollie Date: Sun Sep 29 02:15:26 2024 -0500 first diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc6a357 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.zig-cache +/zig-out + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..623b89f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright © 2004 Jeffrey C. Ollie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..10c8d0b --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# kcp-fuzzer + +Simple Kitty color protocol fuzzer. + +This tries to break/crash terminal emulator by sending plausible, but +random(ish) Kitty color protocol sequences to the terminal. Hopefully your +terminal doesn't crash. + +The last 1 MiB of data that was produced is saved to a file named `/tmp/ksc-fuzzer-0x-0x.txt`. + +A seed can be specified on the command line to replay a particular sequence of +Kitty color protocol sequences. diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..11487a4 --- /dev/null +++ b/build.zig @@ -0,0 +1,43 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const circular_buffer_dep = b.dependency("circular-buffer", .{ + .target = target, + .optimize = optimize, + }); + + const exe = b.addExecutable(.{ + .name = "kcp-fuzzer", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + exe.root_module.addImport("circular-buffer", circular_buffer_dep.module("circular-buffer")); + + b.installArtifact(exe); + const run_cmd = b.addRunArtifact(exe); + + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const exe_unit_tests = b.addTest(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_exe_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..72a2f05 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,22 @@ +.{ + .name = "kcp-fuzzer", + + .version = "0.0.0", + + .minimum_zig_version = "0.13.0", + + .dependencies = .{ + .@"circular-buffer" = .{ + .url = "git+https://git.ocjtech.us/jeff/circular-buffer.git?ref=main#7c2cde8a30d6b99150d64bbca507a857064ee67c", + .hash = "1220b29e891e8c9a6d23ca01e61fcdb128ca40ed2c1961f4153e3709db9958aacc06", + }, + }, + + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + "LICENSE", + "README.md", + }, +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..47b23cb --- /dev/null +++ b/flake.lock @@ -0,0 +1,60 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1727348695, + "narHash": "sha256-J+PeFKSDV+pHL7ukkfpVzCOO7mBSrrpJ3svwBFABbhI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "1925c603f17fc89f4c8f6bf6f631a802ad85d784", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "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 0000000..c0f1205 --- /dev/null +++ b/flake.nix @@ -0,0 +1,32 @@ +{ + inputs = { + nixpkgs = { + url = "nixpkgs/nixos-unstable"; + }; + flake-utils = { + url = "github:numtide/flake-utils"; + }; + }; + + outputs = { + nixpkgs, + flake-utils, + ... + }: + flake-utils.lib.eachDefaultSystem ( + system: let + pkgs = import nixpkgs { + inherit system; + }; + in { + devShells.default = pkgs.mkShell { + nativeBuildInputs = [ + pkgs.zig_0_13 + ]; + shellHook = '' + export name=kcp-fuzzer + ''; + }; + } + ); +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..9c3f8ad --- /dev/null +++ b/src/main.zig @@ -0,0 +1,232 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const CircularBuffer = @import("circular-buffer").CircularBuffer; + +fn sink(fd: std.posix.fd_t, done: *bool) void { + var buf: [1]u8 = undefined; + var count: usize = 0; + while (true) { + const read = std.posix.read(fd, &buf) catch { + done.* = true; + return; + }; + if (read == 0) { + done.* = true; + return; + } + done.* = buf[0] == 0x03; + count +%= read; + } +} + +pub fn main() !void { + const fd = try std.posix.open("/dev/tty", .{ .ACCMODE = .RDWR }, 0); + const state = try std.posix.tcgetattr(fd); + var raw = state; + // see termios(3) + raw.iflag.IGNBRK = false; + raw.iflag.BRKINT = false; + raw.iflag.PARMRK = false; + raw.iflag.ISTRIP = false; + raw.iflag.INLCR = false; + raw.iflag.IGNCR = false; + raw.iflag.ICRNL = false; + raw.iflag.IXON = false; + + raw.oflag.OPOST = false; + + raw.lflag.ECHO = false; + raw.lflag.ECHONL = false; + raw.lflag.ICANON = false; + raw.lflag.ISIG = false; + raw.lflag.IEXTEN = false; + + raw.cflag.CSIZE = .CS8; + raw.cflag.PARENB = false; + + raw.cc[@intFromEnum(std.posix.V.MIN)] = 1; + raw.cc[@intFromEnum(std.posix.V.TIME)] = 0; + try std.posix.tcsetattr(fd, .FLUSH, raw); + + defer { + std.posix.tcsetattr(fd, .FLUSH, state) catch |err| { + std.log.err("couldn't restore terminal: {}", .{err}); + }; + if (builtin.os.tag != .macos) // closing /dev/tty may block indefinitely on macos + std.posix.close(fd); + } + + var done: bool = false; + var thread = try std.Thread.spawn(.{}, sink, .{ fd, &done }); + thread.detach(); + + var cb = CircularBuffer(u20){}; + + const seed = seed: { + var buf: [4096]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&buf); + + var args = try std.process.argsWithAllocator(fba.allocator()); + defer args.deinit(); + + _ = args.next(); + + while (args.next()) |arg| { + break :seed try std.fmt.parseUnsigned(u64, arg, 0); + } + + var seed: u64 = undefined; + try std.posix.getrandom(std.mem.asBytes(&seed)); + break :seed seed; + }; + + var tmp = try std.fs.openDirAbsolute("/tmp", .{}); + defer tmp.close(); + + var prng = std.rand.DefaultPrng.init(seed); + var random = prng.random(); + + var fn_buf: [std.fs.max_path_bytes]u8 = undefined; + const filename = try std.fmt.bufPrint( + &fn_buf, + "kcp-fuzzer-0x{x:0>16}-0x{x:0>16}.txt", + .{ + @abs(std.time.timestamp()), + seed, + }, + ); + + var iteration: usize = 0; + while (!done) : (iteration +%= 1) { + if (iteration & 0x3ff == 0) { + _ = try std.posix.write(fd, "."); + } + + var kcp_buf: [512]u8 = undefined; + var kcp_fbs = std.io.fixedBufferStream(&kcp_buf); + var kcp_writer = kcp_fbs.writer(); + try kcp_writer.print("\x1b]21", .{}); + for (0..random.int(u3)) |_| { + const keys = enum { + foreground, + background, + selection_background, + selection_foreground, + cursor, + cursor_text, + visual_bell, + transparent_background_color1, + transparent_background_color2, + transparent_background_color3, + transparent_background_color4, + transparent_background_color5, + transparent_background_color6, + transparent_background_color7, + transparent_background_color8, + }; + try kcp_writer.print(";{s}=", .{@tagName(random.enumValue(keys))}); + switch (random.enumValue(enum { + color1, + color2, + color3, + color4, + color5, + color6, + color7, + color8, + color9, + query, + empty, + })) { + .color1 => try kcp_writer.print( + "rgb:{x:0>1}/{x:0>1}/{x:0>1}", + .{ + random.int(u4), + random.int(u4), + random.int(u4), + }, + ), + .color2 => try kcp_writer.print( + "rgb:{x:0>2}/{x:0>2}/{x:0>2}", + .{ + random.int(u8), + random.int(u8), + random.int(u8), + }, + ), + .color3 => try kcp_writer.print( + "rgb:{x:0>3}/{x:0>3}/{x:0>3}", + .{ + random.int(u12), + random.int(u12), + random.int(u12), + }, + ), + .color4 => try kcp_writer.print( + "rgb:{x:0>4}/{x:0>4}/{x:0>4}", + .{ + random.int(u16), + random.int(u16), + random.int(u16), + }, + ), + .color5 => try kcp_writer.print( + "#{x:0>1}{x:0>1}{x:0>1}", + .{ + random.int(u4), + random.int(u4), + random.int(u4), + }, + ), + .color6 => try kcp_writer.print( + "#{x:0>2}{x:0>2}{x:0>2}", + .{ + random.int(u8), + random.int(u8), + random.int(u8), + }, + ), + .color7 => try kcp_writer.print( + "#{x:0>3}{x:0>3}{x:0>3}", + .{ + random.int(u12), + random.int(u12), + random.int(u12), + }, + ), + .color8 => try kcp_writer.print( + "#{x:0>4}{x:0>4}{x:0>4}", + .{ + random.int(u16), + random.int(u16), + random.int(u16), + }, + ), + .color9 => try kcp_writer.print( + "rgbi:{d:4.2}/{d:4.2}/{d:4.2}", + .{ + random.float(f32), + random.float(f32), + random.float(f32), + }, + ), + .query => try kcp_writer.writeByte('?'), + .empty => {}, + } + } + try kcp_writer.writeAll("\x1b\\"); + + const kcp = kcp_fbs.getWritten(); + + for (kcp) |c| cb.pushByte(c); + + var file = try tmp.atomicFile(filename, .{}); + defer file.deinit(); + + try cb.write(file.file.writer()); + + try file.finish(); + + _ = try std.posix.write(fd, kcp); + } +}