commit 4d11ff3bd93052a8e304c242c16a95bce7c29a4b Author: Jeffrey C. Ollie Date: Fri Sep 6 19:49:20 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..ef45695 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# osc-fuzzer + +Simple OSC escape sequence fuzzer. + +This tries to break/crash terminal emulator by sending plausible, but random(ish) OSC +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/osc-fuzzer-0x-0x.txt`. + +A seed can be specified on the command line to replay a particular sequence of OSC sequences. diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..5c64c13 --- /dev/null +++ b/build.zig @@ -0,0 +1,36 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "osc-fuzzer", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + 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..bf01d86 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,72 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save `, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = "osc-fuzzer", + + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + + // This field is optional. + // This is currently advisory only; Zig does not yet do anything + // with this value. + //.minimum_zig_version = "0.11.0", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save ` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. Only files listed here will remain on disk + // when using the zig package manager. As a rule of thumb, one should list + // files required for compilation plus any license(s). + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..da65353 --- /dev/null +++ b/flake.lock @@ -0,0 +1,60 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1725432240, + "narHash": "sha256-+yj+xgsfZaErbfYM3T+QvEE2hU7UuE+Jf0fJCJ8uPS0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ad416d066ca1222956472ab7d0555a6946746a80", + "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..086f4cb --- /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=osc-fuzzer + ''; + }; + } + ); +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..9f8164f --- /dev/null +++ b/src/main.zig @@ -0,0 +1,291 @@ +const std = @import("std"); + +pub fn CircularBuffer(comptime T: type) type { + const info = @typeInfo(T); + std.debug.assert(info == .Int); + std.debug.assert(info.Int.signedness == .unsigned); + + return struct { + data: [std.math.maxInt(T) + 1]u8 = undefined, + head: T = 0, + tail: T = 0, + full: bool = false, + + pub fn isEmpty(self: *CircularBuffer(T)) bool { + return self.head == self.tail and !self.full; + } + + pub fn isFull(self: *CircularBuffer(T)) bool { + return self.full; + } + + pub fn reset(self: *CircularBuffer(T)) void { + self.head = 0; + self.tail = 0; + self.full = false; + } + + pub fn size(self: *CircularBuffer(size)) T { + if (self.full) return std.math.maxInt(T); + if (self.head >= self.tail) return @intCast(self.head - self.tail); + return @intCast(std.math.maxInt(T) + self.head - self.tail); + } + + fn advance(self: *CircularBuffer(T)) void { + self.head +%= 1; + if (self.full) self.tail +%= 1; + self.full = self.head == self.tail; + } + + fn retreat(self: *CircularBuffer(T)) void { + self.full = false; + self.tail -%= 1; + } + + pub fn pushByte(self: *CircularBuffer(T), byte: u8) void { + self.data[self.head] = byte; + self.advance(); + } + + pub fn popByte(self: *CircularBuffer(T)) !u8 { + if (self.isEmpty()) return error.BufferEmpty; + defer self.retreat(); + return self.data[self.tail]; + } + + pub fn pushAll(self: *CircularBuffer(T), data: []const u8) void { + if (data.len >= self.data.len) { + @memcpy(&self.data, data[(data.len - self.data.len)..]); + self.tail = 0; + self.head = 0; + self.full = true; + return; + } + + for (data) |c| self.pushByte(c); + } + + pub fn write(self: *CircularBuffer(T), writer: anytype) !void { + if (self.head <= self.tail) { + try writer.writeAll(self.data[self.tail..]); + try writer.writeAll(self.data[0..self.head]); + } else { + try writer.writeAll(self.data[self.tail..self.head]); + } + } + }; +} + +test "circular buffer 1" { + var cb = CircularBuffer(u2){}; + cb.pushByte('a'); + try std.testing.expect(!cb.isFull()); + try std.testing.expect(!cb.isEmpty()); + try std.testing.expectEqual(0, cb.tail); + try std.testing.expectEqual(1, cb.head); + try std.testing.expectEqual('a', cb.data[0]); + + var tmp: [16]u8 = undefined; + var fbs = std.io.fixedBufferStream(&tmp); + try cb.write(fbs.writer()); + const result = fbs.getWritten(); + try std.testing.expectEqualStrings("a", result); +} + +test "circular buffer 2" { + var cb = CircularBuffer(u2){}; + cb.pushByte('a'); + cb.pushByte('b'); + cb.pushByte('c'); + try std.testing.expect(!cb.isFull()); + try std.testing.expect(!cb.isEmpty()); + try std.testing.expectEqual(0, cb.tail); + try std.testing.expectEqual(3, cb.head); + try std.testing.expectEqual('a', cb.data[0]); + try std.testing.expectEqual('b', cb.data[1]); + try std.testing.expectEqual('c', cb.data[2]); + + var tmp: [16]u8 = undefined; + var fbs = std.io.fixedBufferStream(&tmp); + try cb.write(fbs.writer()); + const result = fbs.getWritten(); + try std.testing.expectEqualStrings("abc", result); +} + +test "circular buffer 3" { + var cb = CircularBuffer(u2){}; + cb.pushByte('a'); + cb.pushByte('b'); + cb.pushByte('c'); + cb.pushByte('d'); + cb.pushByte('e'); + try std.testing.expect(cb.isFull()); + try std.testing.expect(!cb.isEmpty()); + try std.testing.expectEqual(1, cb.tail); + try std.testing.expectEqual(1, cb.head); + try std.testing.expectEqual('e', cb.data[0]); + try std.testing.expectEqual('b', cb.data[1]); + try std.testing.expectEqual('c', cb.data[2]); + try std.testing.expectEqual('d', cb.data[3]); + + var tmp: [16]u8 = undefined; + var fbs = std.io.fixedBufferStream(&tmp); + try cb.write(fbs.writer()); + const result = fbs.getWritten(); + try std.testing.expectEqualStrings("bcde", result); +} + +test "circular buffer 4" { + var cb = CircularBuffer(u2){}; + cb.pushAll("a"); + try std.testing.expect(!cb.isFull()); + try std.testing.expect(!cb.isEmpty()); + try std.testing.expectEqual(0, cb.tail); + try std.testing.expectEqual(1, cb.head); + try std.testing.expectEqual('a', cb.data[0]); + + var tmp: [16]u8 = undefined; + var fbs = std.io.fixedBufferStream(&tmp); + try cb.write(fbs.writer()); + const result = fbs.getWritten(); + try std.testing.expectEqualStrings("a", result); +} + +test "circular buffer 5" { + var cb = CircularBuffer(u2){}; + cb.pushAll("ab"); + try std.testing.expect(!cb.isFull()); + try std.testing.expect(!cb.isEmpty()); + try std.testing.expectEqual(0, cb.tail); + try std.testing.expectEqual(2, cb.head); + try std.testing.expectEqual('a', cb.data[0]); + try std.testing.expectEqual('b', cb.data[1]); + + var tmp: [16]u8 = undefined; + var fbs = std.io.fixedBufferStream(&tmp); + try cb.write(fbs.writer()); + const result = fbs.getWritten(); + try std.testing.expectEqualStrings("ab", result); +} + +test "circular buffer 6" { + var cb = CircularBuffer(u2){}; + cb.pushAll("abcd"); + try std.testing.expect(cb.isFull()); + try std.testing.expect(!cb.isEmpty()); + try std.testing.expectEqual(0, cb.tail); + try std.testing.expectEqual(0, cb.head); + try std.testing.expectEqual('a', cb.data[0]); + try std.testing.expectEqual('b', cb.data[1]); + try std.testing.expectEqual('c', cb.data[2]); + try std.testing.expectEqual('d', cb.data[3]); + + var tmp: [16]u8 = undefined; + var fbs = std.io.fixedBufferStream(&tmp); + try cb.write(fbs.writer()); + const result = fbs.getWritten(); + try std.testing.expectEqualStrings("abcd", result); +} + +test "circular buffer 7" { + var cb = CircularBuffer(u2){}; + cb.pushAll("abcde"); + try std.testing.expect(cb.isFull()); + try std.testing.expect(!cb.isEmpty()); + try std.testing.expectEqual(0, cb.tail); + try std.testing.expectEqual(0, cb.head); + try std.testing.expectEqual('b', cb.data[0]); + try std.testing.expectEqual('c', cb.data[1]); + try std.testing.expectEqual('d', cb.data[2]); + try std.testing.expectEqual('e', cb.data[3]); + + var tmp: [16]u8 = undefined; + var fbs = std.io.fixedBufferStream(&tmp); + try cb.write(fbs.writer()); + const result = fbs.getWritten(); + try std.testing.expectEqualStrings("bcde", result); +} + +pub fn main() !void { + 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 buffer = std.io.bufferedWriter(std.io.getStdOut().writer()); + const stdout = buffer.writer(); + + 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, "osc-fuzzer-0x{x:0>16}-0x{x:0>16}.txt", .{ @abs(std.time.timestamp()), seed }); + + var iteration: usize = 0; + while (true) : (iteration += 1) { + if (iteration & 0x3ff == 0) { + try stdout.writeByte('.'); + try buffer.flush(); + } + + var osc_buf: [256]u8 = undefined; + var osc_fbs = std.io.fixedBufferStream(&osc_buf); + var osc_writer = osc_fbs.writer(); + try osc_writer.print("\x1b]{d};", .{random.int(u12)}); + for (0..random.int(u2)) |_| { + for (0..random.int(u4)) |_| { + try osc_writer.writeByte(byte: { + while (true) { + const b = random.int(u8); + if (b != '\x1b' and b != '=' and b != ';') break :byte b; + } + }); + } + if (random.bool()) { + for (0..random.int(u5)) |_| { + try osc_writer.writeByte(byte: { + while (true) { + const b = random.int(u8); + if (b != '\x1b' and b != ';') break :byte b; + } + }); + } + } + } + if (random.bool()) try osc_writer.writeByte(';'); + try osc_writer.writeAll("\x1b\\"); + + const osc = osc_fbs.getWritten(); + + for (osc) |c| cb.pushByte(c); + + var file = try tmp.atomicFile(filename, .{}); + defer file.deinit(); + + try cb.write(file.file.writer()); + + // try file.file.writeAll(osc); + try file.finish(); + + try stdout.writeAll(osc); + try buffer.flush(); + } +}