diff --git a/src/cli/scan.zig b/src/cli/scan.zig index 17a4a3c..a6db723 100644 --- a/src/cli/scan.zig +++ b/src/cli/scan.zig @@ -75,8 +75,9 @@ pub fn scan(allocator: std.mem.Allocator, config_dir: []const u8) ![]ScanEntry { var name_buf: [NAME_BUF_LEN]u8 = std.mem.zeroes([NAME_BUF_LEN]u8); const name_rc = linux.ioctl(fd, HIDIOCGRAWNAME, @intFromPtr(&name_buf)); - if (std.posix.errno(name_rc) != .SUCCESS) { - std.log.warn("scan: HIDIOCGRAWNAME failed for {s}: {}", .{ path, std.posix.errno(name_rc) }); + const name_errno = linux.E.init(name_rc); + if (name_errno != .SUCCESS) { + std.log.warn("scan: HIDIOCGRAWNAME failed for {s}: {s}", .{ path, @tagName(name_errno) }); } const name_raw = std.mem.sliceTo(&name_buf, 0); diff --git a/src/io/hidraw.zig b/src/io/hidraw.zig index 67cd0cd..9dd9fe0 100644 --- a/src/io/hidraw.zig +++ b/src/io/hidraw.zig @@ -27,6 +27,15 @@ fn BoundedArray(comptime T: type, comptime cap: usize) type { }; } +/// EVIOCGRAB the evdev fd and decode the raw `linux.ioctl` return with +/// `linux.E.init`. Under libc, `std.posix.errno` reads the C errno global +/// (always SUCCESS for a raw syscall that never literally returns -1), so a +/// failing grab was silently swallowed and the un-grabbed fd was still kept. +fn evdevGrabErrno(evfd: posix.fd_t) linux.E { + const rc = linux.ioctl(evfd, ioctl.EVIOCGRAB, 1); + return linux.E.init(rc); +} + pub const HidrawDevice = struct { fd: posix.fd_t, evdev_fds: BoundedArray(posix.fd_t, MAX_EVDEV_GRABS), @@ -71,7 +80,8 @@ pub const HidrawDevice = struct { var i: u8 = 0; while (i < 64) : (i += 1) { const path = try std.fmt.allocPrint(allocator, "{s}/hidraw{d}", .{ dev_root, i }); - defer allocator.free(path); + var keep_path = false; + defer if (!keep_path) allocator.free(path); const fd = posix.open(path, .{ .ACCMODE = .RDONLY, .NONBLOCK = true }, 0) catch continue; defer posix.close(fd); @@ -89,7 +99,8 @@ pub const HidrawDevice = struct { if (iface != required_iface) continue; } - return try std.fmt.allocPrint(allocator, "{s}/hidraw{d}", .{ dev_root, i }); + keep_path = true; + return path; } return error.NotFound; } @@ -193,8 +204,7 @@ pub const HidrawDevice = struct { std.log.warn("evdev grab: open {s} failed: {}", .{ dev_path, err }); continue; }; - const grab_rc = linux.ioctl(evfd, ioctl.EVIOCGRAB, 1); - const grab_errno = posix.errno(grab_rc); + const grab_errno = evdevGrabErrno(evfd); if (grab_errno != .SUCCESS) { std.log.warn("evdev grab: EVIOCGRAB {s} failed: {s}", .{ dev_path, @tagName(grab_errno) }); posix.close(evfd); @@ -527,10 +537,11 @@ test "hidraw: grabAssociatedEvdev: matches event by phys prefix" { var dev = HidrawDevice.init(allocator); // Only event7 matches the phys prefix; event9 and event11 are excluded. + // event7 is a regular file, so its EVIOCGRAB returns ENOTTY — now correctly + // surfaced via linux.E.init, so the un-grabbed fd is NOT appended (count 0). + // The phys-prefix matching is still exercised: only event7 reaches the grab. dev.grabAssociatedEvdevWithRoot("/dev/hidraw3", tmp_path, input_dev_root) catch {}; - // On regular files with O_RDWR, EVIOCGRAB returns 0 (harmless no-op), - // so exactly the 1 matching event (event7) is grabbed. - try std.testing.expectEqual(@as(usize, 1), dev.evdev_fds.len); + try std.testing.expectEqual(@as(usize, 0), dev.evdev_fds.len); for (dev.evdev_fds.constSlice()) |fd| posix.close(fd); dev.evdev_fds.len = 0; } @@ -588,3 +599,15 @@ test "hidraw: featureReport surfaces ioctl errno as error" { const payload = [_]u8{ 0x01, 0x02, 0x03 }; try std.testing.expectError(DeviceIO.WriteError.Io, dev_io.featureReport(&payload)); } + +// EVIOCGRAB over a bad fd (-1) returns EBADF. evdevGrabErrno must decode this +// via linux.E.init and surface it as a non-SUCCESS errno, so the caller skips +// appending the un-grabbed fd. +// Falsifiability: with the old `posix.errno(grab_rc)` decode this returns +// .SUCCESS under libc (C errno is untouched by the raw syscall), so the +// assertion below fails. +test "hidraw: evdevGrabErrno surfaces ioctl errno on bad fd" { + const e = evdevGrabErrno(-1); + try std.testing.expect(e != .SUCCESS); + try std.testing.expectEqual(linux.E.BADF, e); +} diff --git a/src/io/uinput.zig b/src/io/uinput.zig index 2eb67b4..3b76928 100644 --- a/src/io/uinput.zig +++ b/src/io/uinput.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const linux = std.os.linux; const state = @import("../core/state.zig"); const device = @import("../config/device.zig"); const input_codes = @import("../config/input_codes.zig"); @@ -30,8 +31,8 @@ const UI_END_FF_ERASE = ioctl_constants.UI_END_FF_ERASE; const MAX_EVENTS = 64; fn ioctlInt(fd: std.posix.fd_t, request: u32, val: c_int) !void { - const rc = std.os.linux.ioctl(fd, request, @intCast(val)); - return switch (std.posix.errno(rc)) { + const rc = linux.ioctl(fd, request, @intCast(val)); + return switch (linux.E.init(rc)) { .SUCCESS => {}, .PERM => error.PermissionDenied, else => |e| std.posix.unexpectedErrno(e), @@ -39,8 +40,8 @@ fn ioctlInt(fd: std.posix.fd_t, request: u32, val: c_int) !void { } fn ioctlPtr(fd: std.posix.fd_t, request: u32, ptr: usize) !void { - const rc = std.os.linux.ioctl(fd, request, ptr); - return switch (std.posix.errno(rc)) { + const rc = linux.ioctl(fd, request, ptr); + return switch (linux.E.init(rc)) { .SUCCESS => {}, .PERM => error.PermissionDenied, else => |e| std.posix.unexpectedErrno(e), @@ -1744,3 +1745,19 @@ test "uinput: pollFf returns identical FfEvent regardless of dump_enabled" { try std.testing.expectEqual(r1.weak, r2.weak); try std.testing.expectEqual(r1.duration_ms, r2.duration_ms); } + +// ioctlInt/ioctlPtr decode the raw `linux.ioctl` return with `linux.E.init`, +// not `std.posix.errno`. Under libc, `posix.errno` reads the C errno global +// (always SUCCESS for a raw syscall that never literally returns -1), so a +// failing ioctl was silently swallowed and the function returned void. +// Driving the ioctl over fd=-1 yields EBADF, which must now surface as error. +// Falsifiability: restoring `std.posix.errno(rc)` makes both tests fail — +// the ioctl returns void instead of the expected error. +test "uinput: ioctlInt surfaces ioctl errno as error (EBADF on bad fd)" { + try std.testing.expectError(error.Unexpected, ioctlInt(-1, UI_SET_EVBIT, c.EV_KEY)); +} + +test "uinput: ioctlPtr surfaces ioctl errno as error (EBADF on bad fd)" { + var setup = std.mem.zeroes(c.uinput_setup); + try std.testing.expectError(error.Unexpected, ioctlPtr(-1, UI_DEV_SETUP, @intFromPtr(&setup))); +}