Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/cli/scan.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
37 changes: 30 additions & 7 deletions src/io/hidraw.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
25 changes: 21 additions & 4 deletions src/io/uinput.zig
Original file line number Diff line number Diff line change
@@ -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");
Expand Down Expand Up @@ -30,17 +31,17 @@ 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),
};
}

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),
Expand Down Expand Up @@ -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)));
}
Loading