From c6637f1a0d5bb29dcd54c8cea2846d766753d21e Mon Sep 17 00:00:00 2001 From: Hina Date: Wed, 10 Jun 2026 00:21:33 +0300 Subject: [PATCH 1/6] Stop asking glibc 4 times like a slobbering mutt --- Hauyne.Injector/linux/linux.zig | 9 ++++---- Hauyne.Injector/linux/symbols.zig | 35 ++++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/Hauyne.Injector/linux/linux.zig b/Hauyne.Injector/linux/linux.zig index ebac7f3..788256e 100644 --- a/Hauyne.Injector/linux/linux.zig +++ b/Hauyne.Injector/linux/linux.zig @@ -114,10 +114,11 @@ pub fn inject( const victim = try victim_mod.pickVictimThread(io, allocator, tgid); - const dlopen_addr = try symbols.findSymbolInTarget(io, allocator, tgid, "dlopen"); - const dlsym_addr = try symbols.findSymbolInTarget(io, allocator, tgid, "dlsym"); - const pthread_create_addr = try symbols.findSymbolInTarget(io, allocator, tgid, "pthread_create"); - const pthread_detach_addr = try symbols.findSymbolInTarget(io, allocator, tgid, "pthread_detach"); + const sym_addrs = try symbols.findSymbolsInTarget(io, allocator, tgid, &.{ "dlopen", "dlsym", "pthread_create", "pthread_detach" }); + const dlopen_addr = sym_addrs[0]; + const dlsym_addr = sym_addrs[1]; + const pthread_create_addr = sym_addrs[2]; + const pthread_detach_addr = sym_addrs[3]; std.debug.print("[hauyne] victim tid={d} (tgid={d})\n", .{ victim, tgid }); if (debug) { diff --git a/Hauyne.Injector/linux/symbols.zig b/Hauyne.Injector/linux/symbols.zig index b82510d..42ced17 100644 --- a/Hauyne.Injector/linux/symbols.zig +++ b/Hauyne.Injector/linux/symbols.zig @@ -16,7 +16,15 @@ const MapsRow = struct { const exacts = [_][]const u8{ "libc.so.6", "libpthread.so.0", "libdl.so.2" }; const prefix = [_][]const u8{"libc.musl-"}; -pub fn findSymbolInTarget(io: std.Io, allocator: std.mem.Allocator, pid: i32, symbol: []const u8) !usize { +pub fn findSymbolsInTarget( + io: std.Io, + allocator: std.mem.Allocator, + pid: i32, + comptime names: []const []const u8, +) ![names.len]usize { + var results = std.mem.zeroes([names.len]usize); + var found: usize = 0; + const maps_path = try std.fmt.allocPrint(allocator, "/proc/{d}/maps", .{pid}); defer allocator.free(maps_path); @@ -25,6 +33,8 @@ pub fn findSymbolInTarget(io: std.Io, allocator: std.mem.Allocator, pid: i32, sy var lines = std.mem.splitScalar(u8, maps_text, '\n'); while (lines.next()) |line| { + if (found == names.len) break; + const row = parseMapsRow(line) orelse continue; if (!std.mem.eql(u8, row.offset, "00000000")) continue; if (!isLibcCandidate(std.fs.path.basename(row.path))) continue; @@ -32,13 +42,24 @@ pub fn findSymbolInTarget(io: std.Io, allocator: std.mem.Allocator, pid: i32, sy const data = readTargetFile(io, allocator, pid, row.path) catch continue; defer allocator.free(data); - const sym_off = lookupDynsym(data, symbol) catch |err| switch (err) { - error.SymbolNotFound => continue, - else => return err, - }; - return row.start + sym_off; + for (0..names.len) |i| { + if (results[i] != 0) continue; + const sym_off = lookupDynsym(data, names[i]) catch |err| switch (err) { + error.SymbolNotFound => continue, + else => return err, + }; + results[i] = row.start + sym_off; + found += 1; + } } - return error.SymbolNotFound; + + if (found < names.len) return error.SymbolNotFound; + return results; +} + +pub fn findSymbolInTarget(io: std.Io, allocator: std.mem.Allocator, pid: i32, comptime symbol: []const u8) !usize { + const result = try findSymbolsInTarget(io, allocator, pid, &.{symbol}); + return result[0]; } fn isLibcCandidate(basename: []const u8) bool { From 7bb31f075957a98f88f3d221b0c87b6f21297eba Mon Sep 17 00:00:00 2001 From: Hina Date: Wed, 10 Jun 2026 00:27:29 +0300 Subject: [PATCH 2/6] Shared arch --- Hauyne.Injector/linux/arch.zig | 19 +++++++++++++++++++ Hauyne.Injector/linux/linux.zig | 6 +----- Hauyne.Injector/linux/ptrace.zig | 6 +----- Hauyne.Injector/linux/shim.zig | 6 +----- Hauyne.Injector/linux/victim.zig | 6 +----- 5 files changed, 23 insertions(+), 20 deletions(-) create mode 100644 Hauyne.Injector/linux/arch.zig diff --git a/Hauyne.Injector/linux/arch.zig b/Hauyne.Injector/linux/arch.zig new file mode 100644 index 0000000..dac149c --- /dev/null +++ b/Hauyne.Injector/linux/arch.zig @@ -0,0 +1,19 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// This Source Code Form is "Incompatible With Secondary Licenses", as defined by the +// Mozilla Public License, v. 2.0. + +const builtin = @import("builtin"); + +pub const cpu = switch (builtin.cpu.arch) { + .x86_64 => @import("arch/x86_64.zig"), + .aarch64 => @import("arch/aarch64.zig"), + else => @compileError("unsupported architecture"), +}; + +pub const emitter = switch (builtin.cpu.arch) { + .x86_64 => @import("shim/x86_64.zig"), + .aarch64 => @import("shim/aarch64.zig"), + else => @compileError("unsupported architecture"), +}; diff --git a/Hauyne.Injector/linux/linux.zig b/Hauyne.Injector/linux/linux.zig index 788256e..e0f3533 100644 --- a/Hauyne.Injector/linux/linux.zig +++ b/Hauyne.Injector/linux/linux.zig @@ -12,11 +12,7 @@ const shim = @import("shim.zig"); const symbols = @import("symbols.zig"); const victim_mod = @import("victim.zig"); -const arch = switch (builtin.cpu.arch) { - .x86_64 => @import("arch/x86_64.zig"), - .aarch64 => @import("arch/aarch64.zig"), - else => @compileError("unsupported architecture"), -}; +const arch = @import("arch.zig").cpu; const UserRegsStruct = ptrace_mod.UserRegsStruct; diff --git a/Hauyne.Injector/linux/ptrace.zig b/Hauyne.Injector/linux/ptrace.zig index 68a58ee..96cefce 100644 --- a/Hauyne.Injector/linux/ptrace.zig +++ b/Hauyne.Injector/linux/ptrace.zig @@ -7,11 +7,7 @@ const std = @import("std"); const builtin = @import("builtin"); -const arch = switch (builtin.cpu.arch) { - .x86_64 => @import("arch/x86_64.zig"), - .aarch64 => @import("arch/aarch64.zig"), - else => @compileError("unsupported architecture"), -}; +const arch = @import("arch.zig").cpu; pub const UserRegsStruct = arch.UserRegsStruct; diff --git a/Hauyne.Injector/linux/shim.zig b/Hauyne.Injector/linux/shim.zig index 8eb6da0..78bfc43 100644 --- a/Hauyne.Injector/linux/shim.zig +++ b/Hauyne.Injector/linux/shim.zig @@ -14,11 +14,7 @@ pub const SymbolOffset: usize = 0x1800; // "hauyne_start\0" (256) pub const VictimShimOff: usize = 0x1900; // pthread_create + pthread_detach (256) pub const PayloadShimOff: usize = 0x1A00; // dlopen + dlsym + hauyne_start (1536) -const arch = switch (builtin.cpu.arch) { - .x86_64 => @import("shim/x86_64.zig"), - .aarch64 => @import("shim/aarch64.zig"), - else => @compileError("unsupported architecture"), -}; +const arch = @import("arch.zig").emitter; pub const InputError = error{ BootstrapPathTooLong, diff --git a/Hauyne.Injector/linux/victim.zig b/Hauyne.Injector/linux/victim.zig index dacb33e..3888510 100644 --- a/Hauyne.Injector/linux/victim.zig +++ b/Hauyne.Injector/linux/victim.zig @@ -7,11 +7,7 @@ const std = @import("std"); const procfs = @import("procfs.zig"); -const arch = switch (@import("builtin").cpu.arch) { - .x86_64 => @import("arch/x86_64.zig"), - .aarch64 => @import("arch/aarch64.zig"), - else => @compileError("unsupported architecture"), -}; +const arch = @import("arch.zig").cpu; // Falls back to the main thread, but main thread holds EE locks, // and will probably just suicide bomb if hijacked From 07c5d5b3530188e55f56996a550c01619c95e8c0 Mon Sep 17 00:00:00 2001 From: Hina Date: Wed, 10 Jun 2026 00:35:43 +0300 Subject: [PATCH 3/6] What if we made build not suck --- .github/workflows/test.yml | 7 ++++- Hauyne.Bootstrap/build.zig | 36 ----------------------- Hauyne.Injector/build.zig | 34 ---------------------- build.sh | 36 ++++++----------------- build.zig | 59 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 99 deletions(-) delete mode 100644 Hauyne.Bootstrap/build.zig delete mode 100644 Hauyne.Injector/build.zig create mode 100644 build.zig diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b648fb..a3ed15d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,7 +36,12 @@ jobs: version: 0.16.0 - name: Build - run: ./build.sh --zig-targets "${{ matrix.zig-target }}" --no-dotnet + run: >- + zig build + -Dtarget=${{ matrix.zig-target }} + -Doptimize=ReleaseFast + -Dinjector-optimize=ReleaseSmall + --prefix bin - name: Build payload run: dotnet build Hauyne.Payload -c Release -p:TargetFramework=net${{ matrix.dotnet }} --nologo diff --git a/Hauyne.Bootstrap/build.zig b/Hauyne.Bootstrap/build.zig deleted file mode 100644 index 5909b62..0000000 --- a/Hauyne.Bootstrap/build.zig +++ /dev/null @@ -1,36 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. -// -// This Source Code Form is "Incompatible With Secondary Licenses", as defined by the -// Mozilla Public License, v. 2.0. - -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - const mod = b.createModule(.{ - .root_source_file = b.path("bootstrap.zig"), - .target = target, - .optimize = optimize, - }); - - mod.link_libc = true; - - if (target.result.os.tag != .windows) { - mod.linkSystemLibrary("dl", .{}); - mod.linkSystemLibrary("pthread", .{}); - } - - const lib = b.addLibrary(.{ - .name = "Hauyne.Bootstrap", - .root_module = mod, - .linkage = .dynamic, - }); - - const install_step = b.addInstallArtifact(lib, .{ - .dest_dir = .{ .override = .{ .custom = "../../bin" } }, - }); - b.getInstallStep().dependOn(&install_step.step); -} diff --git a/Hauyne.Injector/build.zig b/Hauyne.Injector/build.zig deleted file mode 100644 index fa082fa..0000000 --- a/Hauyne.Injector/build.zig +++ /dev/null @@ -1,34 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. -// -// This Source Code Form is "Incompatible With Secondary Licenses", as defined by the -// Mozilla Public License, v. 2.0. - -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - const mod = b.createModule(.{ - .root_source_file = b.path("injector.zig"), - .target = target, - .optimize = optimize, - }); - - mod.link_libc = true; - - if (target.result.os.tag != .windows) { - mod.linkSystemLibrary("dl", .{}); - } - - const exe = b.addExecutable(.{ - .name = "Hauyne.Injector", - .root_module = mod, - }); - - const install_step = b.addInstallArtifact(exe, .{ - .dest_dir = .{ .override = .{ .custom = "../../bin" } }, - }); - b.getInstallStep().dependOn(&install_step.step); -} diff --git a/build.sh b/build.sh index ffae620..f4cab12 100755 --- a/build.sh +++ b/build.sh @@ -11,11 +11,8 @@ set -euo pipefail cd "$(dirname "$(readlink -f "$0")")" -REPO_ROOT="$PWD" -BOOTSTRAP_DIR="$REPO_ROOT/Hauyne.Bootstrap" -INJECTOR_DIR="$REPO_ROOT/Hauyne.Injector" -PAYLOAD_CSPROJ="$REPO_ROOT/Hauyne.Payload/Hauyne.Payload.csproj" -BIN_DIR="$REPO_ROOT/bin" +BIN_DIR="$PWD/bin" +PAYLOAD_CSPROJ="$PWD/Hauyne.Payload/Hauyne.Payload.csproj" CONFIG="${CONFIG:-Release}" OPTIMIZE="${OPTIMIZE:-ReleaseFast}" @@ -75,27 +72,13 @@ injector_artifact_for() { } build_zig() { - mkdir -p "$BIN_DIR" - rm -f "$BIN_DIR/libHauyne.Bootstrap.so" "$BIN_DIR/Hauyne.Bootstrap.dll" - rm -f "$BIN_DIR/Hauyne.Injector" "$BIN_DIR/Hauyne.Injector.exe" - for target in "${ZIG_TARGETS[@]}"; do - echo "==> zig bootstrap $target ($OPTIMIZE)" - ( cd "$BOOTSTRAP_DIR" && zig build -Dtarget="$target" -Doptimize="$OPTIMIZE" ) - - echo "==> zig injector $target ($INJECTOR_OPTIMIZE)" - ( cd "$INJECTOR_DIR" && zig build -Dtarget="$target" -Doptimize="$INJECTOR_OPTIMIZE" ) - - local dest_dir - dest_dir="$BIN_DIR/$target" - mkdir -p "$dest_dir" - - local bs_artifact inj_artifact - bs_artifact="$(bootstrap_artifact_for "$target")" - inj_artifact="$(injector_artifact_for "$target")" - - mv "$BIN_DIR/$bs_artifact" "$dest_dir/$bs_artifact" - mv "$BIN_DIR/$inj_artifact" "$dest_dir/$inj_artifact" + echo "==> zig $target (bootstrap=$OPTIMIZE, injector=$INJECTOR_OPTIMIZE)" + zig build \ + -Dtarget="$target" \ + -Doptimize="$OPTIMIZE" \ + -Dinjector-optimize="$INJECTOR_OPTIMIZE" \ + --prefix "$BIN_DIR/$target" done link_canonical() { @@ -119,9 +102,6 @@ build_dotnet() { dotnet build "$PAYLOAD_CSPROJ" -c "$CONFIG" --nologo } -# Bootstrap resolves Hauyne.Payload.dll relative to its own .so directory (dladdr), -# so symlink the payload into each per-target subdir. -# Multi-target build puts payloads in bin/net9.0/ and bin/net10.0/ payload() { (( DO_ZIG )) || return 0 local payload="" diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..23c6693 --- /dev/null +++ b/build.zig @@ -0,0 +1,59 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// This Source Code Form is "Incompatible With Secondary Licenses", as defined by the +// Mozilla Public License, v. 2.0. + +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + const injector_optimize = b.option( + std.builtin.OptimizeMode, + "injector-optimize", + "Optimization for the injector (defaults to -Doptimize)", + ) orelse optimize; + + { + const mod = b.createModule(.{ + .root_source_file = b.path("Hauyne.Bootstrap/bootstrap.zig"), + .target = target, + .optimize = optimize, + }); + mod.link_libc = true; + if (target.result.os.tag != .windows) { + mod.linkSystemLibrary("dl", .{}); + mod.linkSystemLibrary("pthread", .{}); + } + const lib = b.addLibrary(.{ + .name = "Hauyne.Bootstrap", + .root_module = mod, + .linkage = .dynamic, + }); + const install = b.addInstallArtifact(lib, .{ + .dest_dir = .{ .override = .{ .custom = "" } }, + }); + b.getInstallStep().dependOn(&install.step); + } + + { + const mod = b.createModule(.{ + .root_source_file = b.path("Hauyne.Injector/injector.zig"), + .target = target, + .optimize = injector_optimize, + }); + mod.link_libc = true; + if (target.result.os.tag != .windows) { + mod.linkSystemLibrary("dl", .{}); + } + const exe = b.addExecutable(.{ + .name = "Hauyne.Injector", + .root_module = mod, + }); + const install = b.addInstallArtifact(exe, .{ + .dest_dir = .{ .override = .{ .custom = "" } }, + }); + b.getInstallStep().dependOn(&install.step); + } +} From b03a5b7c5cb95713fb83d2e87b85550320e3f408 Mon Sep 17 00:00:00 2001 From: Hina Date: Wed, 10 Jun 2026 01:38:06 +0300 Subject: [PATCH 4/6] this and that --- Hauyne.Injector/injector.zig | 55 +++++++++++++++---------------- Hauyne.Injector/linux/procfs.zig | 21 ------------ Hauyne.Injector/linux/shim.zig | 1 - Hauyne.Injector/linux/symbols.zig | 3 +- Hauyne.Injector/linux/victim.zig | 5 ++- 5 files changed, 30 insertions(+), 55 deletions(-) delete mode 100644 Hauyne.Injector/linux/procfs.zig diff --git a/Hauyne.Injector/injector.zig b/Hauyne.Injector/injector.zig index 0d50646..f24c569 100644 --- a/Hauyne.Injector/injector.zig +++ b/Hauyne.Injector/injector.zig @@ -9,8 +9,6 @@ const builtin = @import("builtin"); const is_windows = builtin.os.tag == .windows; -const max_matches = 64; - fn println(allocator: std.mem.Allocator, comptime fmt: []const u8, args: anytype) void { const msg = std.fmt.allocPrint(allocator, fmt, args) catch return; defer allocator.free(msg); @@ -23,7 +21,11 @@ pub fn main(init: std.process.Init) u8 { const args = init.minimal.args.toSlice(allocator) catch return 1; - if (args.len < 2 or std.mem.eql(u8, args[1], "--help") or std.mem.eql(u8, args[1], "-h")) { + const wants_help = for (args) |a| { + if (std.mem.eql(u8, a, "--help") or std.mem.eql(u8, a, "-h")) break true; + } else false; + + if (args.len < 2 or wants_help) { const usage = \\Usage: {s} [payload-path] [options] \\ @@ -36,7 +38,7 @@ pub fn main(init: std.process.Init) u8 { \\Options: \\ --type Fully qualified type name (default: Hauyne.Payload.Entrypoint, Hauyne.Payload) \\ --method Entry method name (default: Initialize) - \\ -h, --help Hello + \\ -h, --help Show this help \\ ; println(allocator, usage, .{args[0]}); @@ -65,6 +67,9 @@ pub fn main(init: std.process.Init) u8 { return 1; } method_name = args[i]; + } else if (std.mem.startsWith(u8, a, "--")) { + std.debug.print("Unknown option: {s}\n", .{a}); + return 1; } else if (payload_path == null) { payload_path = a; } else { @@ -144,11 +149,9 @@ fn resolveTarget(io: std.Io, allocator: std.mem.Allocator, spec: []const u8) !u3 return pid; } else |_| {} - var matches: [max_matches]u32 = undefined; - var n: usize = 0; - try collectMatches(io, spec, &matches, &n, &inaccessible); + const matches = try collectMatches(io, allocator, spec, &inaccessible); - if (n == 0) { + if (matches.len == 0) { if (inaccessible > 0) { std.debug.print("No process matches '{s}' ({d} process(es) unreadable — try root or ptrace_scope=0)\n", .{ spec, inaccessible }); } else { @@ -157,10 +160,8 @@ fn resolveTarget(io: std.Io, allocator: std.mem.Allocator, spec: []const u8) !u3 return error.NotFound; } - // Compact .NET-valid PIDs over the front of `matches`. If vn == 0 no writes - // happen and matches[0..n] stays intact for the "none loaded hostfxr" list. var vn: usize = 0; - for (matches[0..n]) |pid| { + for (matches) |pid| { if (isDotNetProcess(io, allocator, pid, &inaccessible) catch false) { matches[vn] = pid; vn += 1; @@ -168,8 +169,8 @@ fn resolveTarget(io: std.Io, allocator: std.mem.Allocator, spec: []const u8) !u3 } if (vn == 0) { - std.debug.print("'{s}' matched {d} process(es) but none loaded hostfxr: ", .{ spec, n }); - printPidList(matches[0..n]); + std.debug.print("'{s}' matched {d} process(es) but none loaded hostfxr: ", .{ spec, matches.len }); + printPidList(matches); return error.NoDotNetMatch; } if (vn > 1) { @@ -188,12 +189,13 @@ fn printPidList(pids: []const u32) void { std.debug.print("\n", .{}); } -fn collectMatches(io: std.Io, name: []const u8, out: []u32, count: *usize, inaccessible: *usize) !void { - if (is_windows) return collectMatchesWindows(name, out, count); - return collectMatchesLinux(io, name, out, count, inaccessible); +fn collectMatches(io: std.Io, allocator: std.mem.Allocator, name: []const u8, inaccessible: *usize) ![]u32 { + if (is_windows) return collectMatchesWindows(allocator, name); + return collectMatchesLinux(io, allocator, name, inaccessible); } -fn collectMatchesLinux(io: std.Io, name: []const u8, out: []u32, count: *usize, inaccessible: *usize) !void { +fn collectMatchesLinux(io: std.Io, allocator: std.mem.Allocator, name: []const u8, inaccessible: *usize) ![]u32 { + var matches = std.ArrayList(u32).init(allocator); const self_pid: u32 = @intCast(std.posix.system.getpid()); var proc_dir = try std.Io.Dir.openDirAbsolute(io, "/proc", .{ .iterate = true }); @@ -207,11 +209,10 @@ fn collectMatchesLinux(io: std.Io, name: []const u8, out: []u32, count: *usize, if (pid == self_pid) continue; if (pidMatchesName(io, pid, name, inaccessible)) { - if (count.* >= out.len) return; - out[count.*] = pid; - count.* += 1; + try matches.append(pid); } } + return matches.items; } fn pidMatchesName(io: std.Io, pid: u32, name: []const u8, inaccessible: *usize) bool { @@ -257,7 +258,8 @@ fn nameMatches(candidate: []const u8, name: []const u8) bool { return false; } -fn collectMatchesWindows(name: []const u8, out: []u32, count: *usize) !void { +fn collectMatchesWindows(allocator: std.mem.Allocator, name: []const u8) ![]u32 { + var matches = std.ArrayList(u32).init(allocator); const windows = std.os.windows; const TH32CS_SNAPPROCESS: windows.DWORD = 0x00000002; @@ -301,7 +303,7 @@ fn collectMatchesWindows(name: []const u8, out: []u32, count: *usize) !void { .library_name = "kernel32", }); - if (Process32FirstW(snapshot, &entry) == .FALSE) return; + if (Process32FirstW(snapshot, &entry) == .FALSE) return matches.items; while (true) { if (entry.th32ProcessID == self_pid) { @@ -320,13 +322,12 @@ fn collectMatchesWindows(name: []const u8, out: []u32, count: *usize) !void { exe_name; if (std.ascii.eqlIgnoreCase(stem, name) or std.ascii.eqlIgnoreCase(exe_name, name)) { - if (count.* >= out.len) return; - out[count.*] = entry.th32ProcessID; - count.* += 1; + try matches.append(entry.th32ProcessID); } if (Process32NextW(snapshot, &entry) == .FALSE) break; } + return matches.items; } fn isDotNetProcess(io: std.Io, allocator: std.mem.Allocator, pid: u32, inaccessible: *usize) !bool { @@ -335,12 +336,10 @@ fn isDotNetProcess(io: std.Io, allocator: std.mem.Allocator, pid: u32, inaccessi } fn isDotNetProcessLinux(io: std.Io, allocator: std.mem.Allocator, pid: u32, inaccessible: *usize) !bool { - _ = io; const maps_path = try std.fmt.allocPrint(allocator, "/proc/{}/maps", .{pid}); defer allocator.free(maps_path); - const procfs = @import("linux/procfs.zig"); - const data = procfs.readFileAlloc(allocator, maps_path) catch |err| { + const data = std.Io.Dir.cwd().readFileAlloc(io, maps_path, allocator, std.Io.Limit.limited(8 * 1024 * 1024)) catch |err| { switch (err) { error.AccessDenied, error.PermissionDenied => inaccessible.* += 1, else => {}, diff --git a/Hauyne.Injector/linux/procfs.zig b/Hauyne.Injector/linux/procfs.zig deleted file mode 100644 index 05d0c60..0000000 --- a/Hauyne.Injector/linux/procfs.zig +++ /dev/null @@ -1,21 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. -// -// This Source Code Form is "Incompatible With Secondary Licenses", as defined by the -// Mozilla Public License, v. 2.0. - -const std = @import("std"); - -pub fn readFileAlloc(allocator: std.mem.Allocator, path: []const u8) ![]u8 { - const fd = try std.posix.openat(std.posix.AT.FDCWD, path, .{ .ACCMODE = .RDONLY }, 0); - defer _ = std.c.close(fd); - var buf = try allocator.alloc(u8, 4096); - var n: usize = 0; - while (true) { - const r = try std.posix.read(fd, buf[n..]); - if (r == 0) break; - n += r; - if (n == buf.len) buf = try allocator.realloc(buf, buf.len * 2); - } - return buf[0..n]; -} diff --git a/Hauyne.Injector/linux/shim.zig b/Hauyne.Injector/linux/shim.zig index 78bfc43..4db32ec 100644 --- a/Hauyne.Injector/linux/shim.zig +++ b/Hauyne.Injector/linux/shim.zig @@ -5,7 +5,6 @@ // Mozilla Public License, v. 2.0. const std = @import("std"); -const builtin = @import("builtin"); pub const ScratchSize: usize = 0x2000; // 8 KiB (two pages) pub const PathOffset: usize = 0x40; // bootstrap .so path (1984) diff --git a/Hauyne.Injector/linux/symbols.zig b/Hauyne.Injector/linux/symbols.zig index 42ced17..cf8770f 100644 --- a/Hauyne.Injector/linux/symbols.zig +++ b/Hauyne.Injector/linux/symbols.zig @@ -28,8 +28,7 @@ pub fn findSymbolsInTarget( const maps_path = try std.fmt.allocPrint(allocator, "/proc/{d}/maps", .{pid}); defer allocator.free(maps_path); - const procfs = @import("procfs.zig"); - const maps_text = try procfs.readFileAlloc(allocator, maps_path); + const maps_text = try std.Io.Dir.cwd().readFileAlloc(io, maps_path, allocator, std.Io.Limit.limited(8 * 1024 * 1024)); var lines = std.mem.splitScalar(u8, maps_text, '\n'); while (lines.next()) |line| { diff --git a/Hauyne.Injector/linux/victim.zig b/Hauyne.Injector/linux/victim.zig index 3888510..0c65c75 100644 --- a/Hauyne.Injector/linux/victim.zig +++ b/Hauyne.Injector/linux/victim.zig @@ -5,7 +5,6 @@ // Mozilla Public License, v. 2.0. const std = @import("std"); -const procfs = @import("procfs.zig"); const arch = @import("arch.zig").cpu; @@ -28,7 +27,7 @@ pub fn pickVictimThread(io: std.Io, allocator: std.mem.Allocator, tgid: i32) !i3 const syscall_path = std.fmt.allocPrint(allocator, "/proc/{d}/task/{d}/syscall", .{ tgid, tid }) catch continue; defer allocator.free(syscall_path); - const syscall_text = procfs.readFileAlloc(allocator, syscall_path) catch continue; + const syscall_text = std.Io.Dir.cwd().readFileAlloc(io, syscall_path, allocator, std.Io.Limit.limited(4096)) catch continue; const trimmed = std.mem.trimEnd(u8, syscall_text, "\n\r \t"); if (std.mem.eql(u8, trimmed, "running")) continue; @@ -49,7 +48,7 @@ pub fn pickVictimThread(io: std.Io, allocator: std.mem.Allocator, tgid: i32) !i3 const stat_path = std.fmt.allocPrint(allocator, "/proc/{d}/task/{d}/stat", .{ tgid, tid }) catch continue; defer allocator.free(stat_path); - const stat_text = procfs.readFileAlloc(allocator, stat_path) catch continue; + const stat_text = std.Io.Dir.cwd().readFileAlloc(io, stat_path, allocator, std.Io.Limit.limited(4096)) catch continue; const last_paren = std.mem.lastIndexOfScalar(u8, stat_text, ')') orelse continue; const rest = stat_text[last_paren + 1 ..]; From b1aaf4f8d79de93362f880b24869b9a2dabf4208 Mon Sep 17 00:00:00 2001 From: Hina Date: Wed, 10 Jun 2026 01:45:46 +0300 Subject: [PATCH 5/6] true! --- Hauyne.Injector/injector.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Hauyne.Injector/injector.zig b/Hauyne.Injector/injector.zig index f24c569..42c694d 100644 --- a/Hauyne.Injector/injector.zig +++ b/Hauyne.Injector/injector.zig @@ -195,7 +195,7 @@ fn collectMatches(io: std.Io, allocator: std.mem.Allocator, name: []const u8, in } fn collectMatchesLinux(io: std.Io, allocator: std.mem.Allocator, name: []const u8, inaccessible: *usize) ![]u32 { - var matches = std.ArrayList(u32).init(allocator); + var matches: std.ArrayList(u32) = .empty; const self_pid: u32 = @intCast(std.posix.system.getpid()); var proc_dir = try std.Io.Dir.openDirAbsolute(io, "/proc", .{ .iterate = true }); @@ -209,7 +209,7 @@ fn collectMatchesLinux(io: std.Io, allocator: std.mem.Allocator, name: []const u if (pid == self_pid) continue; if (pidMatchesName(io, pid, name, inaccessible)) { - try matches.append(pid); + try matches.append(allocator, pid); } } return matches.items; @@ -259,7 +259,7 @@ fn nameMatches(candidate: []const u8, name: []const u8) bool { } fn collectMatchesWindows(allocator: std.mem.Allocator, name: []const u8) ![]u32 { - var matches = std.ArrayList(u32).init(allocator); + var matches: std.ArrayList(u32) = .empty; const windows = std.os.windows; const TH32CS_SNAPPROCESS: windows.DWORD = 0x00000002; @@ -322,7 +322,7 @@ fn collectMatchesWindows(allocator: std.mem.Allocator, name: []const u8) ![]u32 exe_name; if (std.ascii.eqlIgnoreCase(stem, name) or std.ascii.eqlIgnoreCase(exe_name, name)) { - try matches.append(entry.th32ProcessID); + try matches.append(allocator, entry.th32ProcessID); } if (Process32NextW(snapshot, &entry) == .FALSE) break; From 8efbc786eb76addb7b2f336ae7ae13377d7f2126 Mon Sep 17 00:00:00 2001 From: Hina Date: Wed, 10 Jun 2026 01:56:55 +0300 Subject: [PATCH 6/6] fail --- Hauyne.Injector/injector.zig | 4 +++- Hauyne.Injector/linux/procfs.zig | 22 ++++++++++++++++++++++ Hauyne.Injector/linux/symbols.zig | 3 ++- Hauyne.Injector/linux/victim.zig | 5 +++-- 4 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 Hauyne.Injector/linux/procfs.zig diff --git a/Hauyne.Injector/injector.zig b/Hauyne.Injector/injector.zig index 42c694d..22904cb 100644 --- a/Hauyne.Injector/injector.zig +++ b/Hauyne.Injector/injector.zig @@ -336,10 +336,12 @@ fn isDotNetProcess(io: std.Io, allocator: std.mem.Allocator, pid: u32, inaccessi } fn isDotNetProcessLinux(io: std.Io, allocator: std.mem.Allocator, pid: u32, inaccessible: *usize) !bool { + _ = io; const maps_path = try std.fmt.allocPrint(allocator, "/proc/{}/maps", .{pid}); defer allocator.free(maps_path); - const data = std.Io.Dir.cwd().readFileAlloc(io, maps_path, allocator, std.Io.Limit.limited(8 * 1024 * 1024)) catch |err| { + const procfs = @import("linux/procfs.zig"); + const data = procfs.readFileAlloc(allocator, maps_path) catch |err| { switch (err) { error.AccessDenied, error.PermissionDenied => inaccessible.* += 1, else => {}, diff --git a/Hauyne.Injector/linux/procfs.zig b/Hauyne.Injector/linux/procfs.zig new file mode 100644 index 0000000..03c4356 --- /dev/null +++ b/Hauyne.Injector/linux/procfs.zig @@ -0,0 +1,22 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// This Source Code Form is "Incompatible With Secondary Licenses", as defined by the +// Mozilla Public License, v. 2.0. + +const std = @import("std"); + +// /proc pseudo-files report st_size=0, so std.Io readFileAlloc returns empty. +pub fn readFileAlloc(allocator: std.mem.Allocator, path: []const u8) ![]u8 { + const fd = try std.posix.openat(std.posix.AT.FDCWD, path, .{ .ACCMODE = .RDONLY }, 0); + defer _ = std.c.close(fd); + var buf = try allocator.alloc(u8, 4096); + var n: usize = 0; + while (true) { + const r = try std.posix.read(fd, buf[n..]); + if (r == 0) break; + n += r; + if (n == buf.len) buf = try allocator.realloc(buf, buf.len * 2); + } + return buf[0..n]; +} diff --git a/Hauyne.Injector/linux/symbols.zig b/Hauyne.Injector/linux/symbols.zig index cf8770f..42ced17 100644 --- a/Hauyne.Injector/linux/symbols.zig +++ b/Hauyne.Injector/linux/symbols.zig @@ -28,7 +28,8 @@ pub fn findSymbolsInTarget( const maps_path = try std.fmt.allocPrint(allocator, "/proc/{d}/maps", .{pid}); defer allocator.free(maps_path); - const maps_text = try std.Io.Dir.cwd().readFileAlloc(io, maps_path, allocator, std.Io.Limit.limited(8 * 1024 * 1024)); + const procfs = @import("procfs.zig"); + const maps_text = try procfs.readFileAlloc(allocator, maps_path); var lines = std.mem.splitScalar(u8, maps_text, '\n'); while (lines.next()) |line| { diff --git a/Hauyne.Injector/linux/victim.zig b/Hauyne.Injector/linux/victim.zig index 0c65c75..3888510 100644 --- a/Hauyne.Injector/linux/victim.zig +++ b/Hauyne.Injector/linux/victim.zig @@ -5,6 +5,7 @@ // Mozilla Public License, v. 2.0. const std = @import("std"); +const procfs = @import("procfs.zig"); const arch = @import("arch.zig").cpu; @@ -27,7 +28,7 @@ pub fn pickVictimThread(io: std.Io, allocator: std.mem.Allocator, tgid: i32) !i3 const syscall_path = std.fmt.allocPrint(allocator, "/proc/{d}/task/{d}/syscall", .{ tgid, tid }) catch continue; defer allocator.free(syscall_path); - const syscall_text = std.Io.Dir.cwd().readFileAlloc(io, syscall_path, allocator, std.Io.Limit.limited(4096)) catch continue; + const syscall_text = procfs.readFileAlloc(allocator, syscall_path) catch continue; const trimmed = std.mem.trimEnd(u8, syscall_text, "\n\r \t"); if (std.mem.eql(u8, trimmed, "running")) continue; @@ -48,7 +49,7 @@ pub fn pickVictimThread(io: std.Io, allocator: std.mem.Allocator, tgid: i32) !i3 const stat_path = std.fmt.allocPrint(allocator, "/proc/{d}/task/{d}/stat", .{ tgid, tid }) catch continue; defer allocator.free(stat_path); - const stat_text = std.Io.Dir.cwd().readFileAlloc(io, stat_path, allocator, std.Io.Limit.limited(4096)) catch continue; + const stat_text = procfs.readFileAlloc(allocator, stat_path) catch continue; const last_paren = std.mem.lastIndexOfScalar(u8, stat_text, ')') orelse continue; const rest = stat_text[last_paren + 1 ..];