summaryrefslogtreecommitdiffstats
path: root/drivers/hid/hid-steelseries-arctis.c
diff options
context:
space:
mode:
authorKumar Kartikeya Dwivedi <memxor@gmail.com>2026-09-04 10:43:19 +0200
committerAlexei Starovoitov <ast@kernel.org>2026-09-04 07:58:36 -0700
commit6668ed271eaefaa63e686bdfbedaeb7b8e492722 (patch)
tree58e8288fead484f6755ad0815fc74c3742663761 /drivers/hid/hid-steelseries-arctis.c
downloadlinux-stable-6668ed271eaefaa63e686bdfbedaeb7b8e492722.tar.gz
linux-stable-6668ed271eaefaa63e686bdfbedaeb7b8e492722.zip
selftests/bpf: Reject graph kptr use after RCU unlockgrafted
Add a sleepable verifier test that loads a graph-node local kptr in an explicit RCU read-side critical section, then passes its node to bpf_rbtree_remove() after the section ends. Before the verifier fix, the stale NON_OWN_REF flag makes the node look like a live borrowed reference and the program is accepted. After the fix, the pointer is demoted without NON_OWN_REF and the graph kfunc argument is rejected. Also exercise a graph kptr loaded while a spin lock provides implicit RCU protection. The pointer must be invalidated when the lock is released, which guards the required ordering between non-owning-reference invalidation and RCU demotion. Update the existing fault-protected load test state description. The post-unlock pointer no longer carries NON_OWN_REF, but remains readable because the load is rewritten to use BPF_PROBE_MEM. Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com> Link: https://lore.kernel.org/r/20260904084325.52250-7-memxor@gmail.com Signed-off-by: Alexei Starovoitov <ast@kernel.org>
Diffstat (limited to 'drivers/hid/hid-steelseries-arctis.c')
-rw-r--r--drivers/hid/hid-steelseries-arctis.c718
1 files changed, 718 insertions, 0 deletions
diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
new file mode 100644
index 000000000..23fb0cebd
--- /dev/null
+++ b/drivers/hid/hid-steelseries-arctis.c
@@ -0,0 +1,718 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * HID driver for Steelseries arctis headsets
+ *
+ * Copyright (c) 2023 Bastien Nocera
+ * Copyright (c) 2026 Sriman Achanta
+ */
+
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/kref.h>
+#include <linux/slab.h>
+#include <linux/module.h>
+#include <linux/power_supply.h>
+#include <linux/spinlock.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+
+#include "hid-ids.h"
+
+#define SS_CAP_BATTERY BIT(0)
+
+struct steelseries_device;
+
+struct steelseries_device_info {
+ unsigned long capabilities;
+
+ u8 sync_interface;
+ u8 async_interface;
+
+ int (*request_status)(struct hid_device *hdev);
+ void (*parse_status)(struct steelseries_device *sd, u8 *data, int size);
+};
+
+struct steelseries_device {
+ struct kref refcnt;
+
+ struct hid_device *hdev;
+ const struct steelseries_device_info *info;
+
+ struct delayed_work status_work;
+
+ struct power_supply_desc battery_desc;
+ struct power_supply *battery;
+ bool headset_connected;
+ u8 battery_capacity;
+ bool battery_charging;
+
+ spinlock_t lock;
+ bool removed;
+};
+
+static void steelseries_device_release(struct kref *ref)
+{
+ struct steelseries_device *sd =
+ container_of(ref, struct steelseries_device, refcnt);
+
+ kfree(sd);
+}
+
+/*
+ * Headset report helpers
+ */
+
+static int steelseries_send_report(struct hid_device *hdev, const u8 *data,
+ int len, enum hid_report_type type)
+{
+ u8 *buf;
+ int ret;
+
+ buf = kmemdup(data, len, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ ret = hid_hw_raw_request(hdev, data[0], buf, len, type,
+ HID_REQ_SET_REPORT);
+ kfree(buf);
+
+ if (ret < 0)
+ return ret;
+ if (ret < len)
+ return -EIO;
+
+ return 0;
+}
+
+static inline int steelseries_send_output_report(struct hid_device *hdev,
+ const u8 *data, int len)
+{
+ return steelseries_send_report(hdev, data, len, HID_OUTPUT_REPORT);
+}
+
+/*
+ * Headset status request functions
+ */
+
+static int steelseries_arctis_1_request_status(struct hid_device *hdev)
+{
+ const u8 data[] = { 0x06, 0x12 };
+
+ return steelseries_send_output_report(hdev, data, sizeof(data));
+}
+
+static int steelseries_arctis_9_request_status(struct hid_device *hdev)
+{
+ const u8 data[] = { 0x00, 0x20 };
+
+ return steelseries_send_output_report(hdev, data, sizeof(data));
+}
+
+static int steelseries_arctis_nova_request_status(struct hid_device *hdev)
+{
+ const u8 data[] = { 0x00, 0xb0 };
+
+ return steelseries_send_output_report(hdev, data, sizeof(data));
+}
+
+/*
+ * Headset battery helpers
+ */
+
+static int battery_capacity_to_level(int capacity)
+{
+ if (capacity >= 50)
+ return POWER_SUPPLY_CAPACITY_LEVEL_NORMAL;
+ if (capacity >= 20)
+ return POWER_SUPPLY_CAPACITY_LEVEL_LOW;
+ return POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL;
+}
+
+static u8 steelseries_map_capacity(u8 capacity, u8 min_in, u8 max_in)
+{
+ if (capacity >= max_in)
+ return 100;
+ if (capacity <= min_in)
+ return 0;
+ return (capacity - min_in) * 100 / (max_in - min_in);
+}
+
+/*
+ * Headset status parse functions
+ */
+
+static void steelseries_arctis_1_parse_status(struct steelseries_device *sd,
+ u8 *data, int size)
+{
+ /* Only the battery status report echoes the request header. */
+ if (size < 8 || data[0] != 0x06 || data[1] != 0x12)
+ return;
+
+ sd->headset_connected = (data[2] != 0x01);
+ sd->battery_capacity = data[3];
+}
+
+static void steelseries_arctis_9_parse_status(struct steelseries_device *sd,
+ u8 *data, int size)
+{
+ if (size < 5)
+ return;
+
+ if (data[0] == 0xaa && data[1] == 0x01) {
+ sd->headset_connected = true;
+ sd->battery_charging = (data[4] == 0x01);
+ sd->battery_capacity = steelseries_map_capacity(data[3], 0x64, 0x9a);
+ } else {
+ /* Device off: 0x55 (no status) or 0x03 (stale status). */
+ sd->headset_connected = false;
+ sd->battery_charging = false;
+ }
+}
+
+static void steelseries_arctis_nova_parse_status(struct steelseries_device *sd,
+ u8 *data, int size)
+{
+ if (size < 2)
+ return;
+
+ switch (data[0]) {
+ case 0xb0:
+ if (size < 4)
+ return;
+ sd->headset_connected = (data[1] == 0x03);
+ sd->battery_capacity = data[2];
+ sd->battery_charging = (data[3] == 0x01);
+ break;
+ case 0xb7:
+ sd->battery_capacity = data[1];
+ break;
+ case 0xb9:
+ sd->headset_connected = (data[1] == 0x03);
+ break;
+ case 0xbb:
+ sd->battery_charging = (data[1] == 0x01);
+ break;
+ }
+}
+
+static void steelseries_arctis_nova_7_parse_status(struct steelseries_device *sd,
+ u8 *data, int size)
+{
+ if (size < 2)
+ return;
+
+ switch (data[0]) {
+ case 0xb0:
+ if (size < 4)
+ return;
+ sd->headset_connected = (data[1] == 0x03);
+ sd->battery_capacity = steelseries_map_capacity(data[2], 0, 4);
+ sd->battery_charging = (data[3] == 0x01);
+ break;
+ case 0xb7:
+ sd->battery_capacity = steelseries_map_capacity(data[1], 0, 4);
+ break;
+ case 0xb9:
+ sd->headset_connected = (data[1] == 0x03);
+ break;
+ case 0xbb:
+ sd->battery_charging = (data[1] == 0x01);
+ break;
+ }
+}
+
+/*
+ * Device info definitions
+ */
+
+static const struct steelseries_device_info arctis_1_info = {
+ .sync_interface = 3,
+ .capabilities = SS_CAP_BATTERY,
+ .request_status = steelseries_arctis_1_request_status,
+ .parse_status = steelseries_arctis_1_parse_status,
+};
+
+static const struct steelseries_device_info arctis_9_info = {
+ .sync_interface = 0,
+ .capabilities = SS_CAP_BATTERY,
+ .request_status = steelseries_arctis_9_request_status,
+ .parse_status = steelseries_arctis_9_parse_status,
+};
+
+static const struct steelseries_device_info arctis_nova_info = {
+ .sync_interface = 3,
+ .async_interface = 5,
+ .capabilities = SS_CAP_BATTERY,
+ .request_status = steelseries_arctis_nova_request_status,
+ .parse_status = steelseries_arctis_nova_parse_status,
+};
+
+static const struct steelseries_device_info arctis_nova_7_info = {
+ .sync_interface = 3,
+ .async_interface = 5,
+ .capabilities = SS_CAP_BATTERY,
+ .request_status = steelseries_arctis_nova_request_status,
+ .parse_status = steelseries_arctis_nova_7_parse_status,
+};
+
+/*
+ * Headset wireless status and battery infrastructure
+ */
+
+#define STEELSERIES_HEADSET_STATUS_TIMEOUT_MS 3000
+
+static void
+steelseries_headset_set_wireless_status(struct hid_device *hdev,
+ bool connected)
+{
+ struct usb_interface *intf;
+
+ if (!hid_is_usb(hdev))
+ return;
+
+ intf = to_usb_interface(hdev->dev.parent);
+ usb_set_wireless_status(intf, connected ?
+ USB_WIRELESS_STATUS_CONNECTED :
+ USB_WIRELESS_STATUS_DISCONNECTED);
+}
+
+#define STEELSERIES_PREFIX "SteelSeries "
+
+static int steelseries_battery_get_property(struct power_supply *psy,
+ enum power_supply_property psp,
+ union power_supply_propval *val)
+{
+ struct steelseries_device *sd = power_supply_get_drvdata(psy);
+ size_t prefix_len;
+ int ret = 0;
+
+ switch (psp) {
+ case POWER_SUPPLY_PROP_MODEL_NAME:
+ val->strval = sd->hdev->name;
+ while ((prefix_len = str_has_prefix(val->strval, STEELSERIES_PREFIX)))
+ val->strval += prefix_len;
+ break;
+ case POWER_SUPPLY_PROP_MANUFACTURER:
+ val->strval = "SteelSeries";
+ break;
+ case POWER_SUPPLY_PROP_PRESENT:
+ val->intval = 1;
+ break;
+ case POWER_SUPPLY_PROP_STATUS:
+ if (!sd->headset_connected)
+ val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
+ else if (sd->battery_charging)
+ val->intval = sd->battery_capacity >= 100 ?
+ POWER_SUPPLY_STATUS_FULL :
+ POWER_SUPPLY_STATUS_CHARGING;
+ else
+ val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
+ break;
+ case POWER_SUPPLY_PROP_SCOPE:
+ val->intval = POWER_SUPPLY_SCOPE_DEVICE;
+ break;
+ case POWER_SUPPLY_PROP_CAPACITY:
+ val->intval = sd->battery_capacity;
+ break;
+ case POWER_SUPPLY_PROP_CAPACITY_LEVEL:
+ val->intval = battery_capacity_to_level(sd->battery_capacity);
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+ return ret;
+}
+
+static enum power_supply_property steelseries_battery_props[] = {
+ POWER_SUPPLY_PROP_MODEL_NAME,
+ POWER_SUPPLY_PROP_MANUFACTURER,
+ POWER_SUPPLY_PROP_PRESENT,
+ POWER_SUPPLY_PROP_STATUS,
+ POWER_SUPPLY_PROP_SCOPE,
+ POWER_SUPPLY_PROP_CAPACITY,
+ POWER_SUPPLY_PROP_CAPACITY_LEVEL,
+};
+
+/*
+ * Delayed work handlers for status polling
+ */
+
+static void steelseries_status_timer_work_handler(struct work_struct *work)
+{
+ struct steelseries_device *sd = container_of(
+ work, struct steelseries_device, status_work.work);
+ unsigned long flags;
+
+ sd->info->request_status(sd->hdev);
+
+ spin_lock_irqsave(&sd->lock, flags);
+ /* Async devices push status events themselves; only poll once. */
+ if (!sd->removed && !sd->info->async_interface)
+ schedule_delayed_work(&sd->status_work,
+ msecs_to_jiffies(STEELSERIES_HEADSET_STATUS_TIMEOUT_MS));
+ spin_unlock_irqrestore(&sd->lock, flags);
+}
+
+static int steelseries_battery_register(struct steelseries_device *sd)
+{
+ struct power_supply_config battery_cfg = { .drv_data = sd, };
+ struct power_supply *battery;
+ int ret;
+
+ sd->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
+ sd->battery_desc.properties = steelseries_battery_props;
+ sd->battery_desc.num_properties = ARRAY_SIZE(steelseries_battery_props);
+ sd->battery_desc.get_property = steelseries_battery_get_property;
+ sd->battery_desc.use_for_apm = 0;
+ sd->battery_desc.name = devm_kasprintf(&sd->hdev->dev, GFP_KERNEL,
+ "steelseries_headset_battery_%s",
+ sd->hdev->uniq[0] ? sd->hdev->uniq :
+ dev_name(&sd->hdev->dev));
+ if (!sd->battery_desc.name)
+ return -ENOMEM;
+
+ /* avoid the warning of 0% battery while waiting for the first info */
+ sd->battery_capacity = 100;
+ sd->battery_charging = false;
+ sd->headset_connected = false;
+ steelseries_headset_set_wireless_status(sd->hdev, false);
+
+ battery = power_supply_register(&sd->hdev->dev,
+ &sd->battery_desc, &battery_cfg);
+ if (IS_ERR(battery)) {
+ ret = PTR_ERR(battery);
+ hid_err(sd->hdev,
+ "%s:power_supply_register failed with error %d\n",
+ __func__, ret);
+ return ret;
+ }
+ power_supply_powers(battery, &sd->hdev->dev);
+
+ /* Assign on success only, so a concurrent raw_event never sees an ERR_PTR. */
+ sd->battery = battery;
+
+ return 0;
+}
+
+static struct hid_driver steelseries_arctis_driver;
+
+static struct steelseries_device *
+steelseries_get_sibling_sd(struct hid_device *hdev, int interface_num)
+{
+ struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
+ struct usb_device *usb_dev = interface_to_usbdev(intf);
+ struct usb_interface *sibling_intf;
+ struct hid_device *sibling_hdev;
+ struct steelseries_device *sd = NULL;
+
+ sibling_intf = usb_ifnum_to_if(usb_dev, interface_num);
+ if (!sibling_intf)
+ return NULL;
+
+ /*
+ * usb_get_intfdata() only yields a hid_device when usbhid is bound;
+ * gate on the descriptor class so a non-HID sibling (e.g. a crafted
+ * device exposing storage or audio here) is never treated as one.
+ */
+ if (sibling_intf->cur_altsetting->desc.bInterfaceClass != USB_INTERFACE_CLASS_HID)
+ return NULL;
+
+ /*
+ * Take the sibling's device lock across the intfdata read and the
+ * kref_get so a concurrent unbind cannot free the hid_device underneath
+ * us; usbhid leaves intfdata dangling on disconnect, so dev.driver is
+ * the reliable "still bound" test under this lock. Use device_trylock()
+ * to stay off the lockdep chain of the interface being probed and let
+ * the caller retry via -EPROBE_DEFER if the sibling is momentarily busy.
+ */
+ if (!device_trylock(&sibling_intf->dev))
+ return NULL;
+ if (sibling_intf->dev.driver) {
+ sibling_hdev = usb_get_intfdata(sibling_intf);
+ if (sibling_hdev &&
+ sibling_hdev->driver == &steelseries_arctis_driver) {
+ sd = hid_get_drvdata(sibling_hdev);
+ if (sd)
+ kref_get(&sd->refcnt);
+ }
+ }
+ device_unlock(&sibling_intf->dev);
+
+ return sd;
+}
+
+static int steelseries_arctis_probe(struct hid_device *hdev,
+ const struct hid_device_id *id)
+{
+ const struct steelseries_device_info *info =
+ (const struct steelseries_device_info *)id->driver_data;
+ struct steelseries_device *sd;
+ struct usb_interface *intf;
+ u8 interface_num;
+ int ret;
+
+ if (hid_is_usb(hdev)) {
+ intf = to_usb_interface(hdev->dev.parent);
+ interface_num = intf->cur_altsetting->desc.bInterfaceNumber;
+ } else {
+ return -ENODEV;
+ }
+
+ ret = hid_parse(hdev);
+ if (ret)
+ return ret;
+
+ /* Let hid-generic handle non-vendor or unknown interfaces */
+ if (interface_num != info->sync_interface &&
+ (!info->async_interface || interface_num != info->async_interface))
+ return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+
+ if (interface_num == info->sync_interface) {
+ sd = kzalloc_obj(*sd, GFP_KERNEL);
+ if (!sd)
+ return -ENOMEM;
+
+ kref_init(&sd->refcnt);
+ sd->hdev = hdev;
+ sd->info = info;
+ spin_lock_init(&sd->lock);
+ INIT_DELAYED_WORK(&sd->status_work, steelseries_status_timer_work_handler);
+
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ if (ret)
+ goto err_free;
+
+ ret = hid_hw_open(hdev);
+ if (ret)
+ goto err_stop;
+
+ if (info->capabilities & SS_CAP_BATTERY) {
+ ret = steelseries_battery_register(sd);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to register battery: %d\n", ret);
+ }
+
+ /*
+ * Publish drvdata only once fully initialised: the async sibling
+ * attaches by reading it, so it must never observe a half-built or
+ * failed instance. A failed probe never gets here, so the error
+ * path below has nothing to unpublish.
+ */
+ hid_set_drvdata(hdev, sd);
+ schedule_delayed_work(&sd->status_work, msecs_to_jiffies(100));
+
+ return 0;
+ }
+
+ /*
+ * The async interface shares the steelseries_device created by the
+ * sync interface. Defer until the sync interface has probed and
+ * published its drvdata.
+ */
+ if (info->async_interface && interface_num == info->async_interface) {
+ sd = steelseries_get_sibling_sd(hdev, info->sync_interface);
+ if (!sd)
+ return -EPROBE_DEFER;
+
+ hid_set_drvdata(hdev, sd);
+
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ if (ret) {
+ kref_put(&sd->refcnt, steelseries_device_release);
+ return ret;
+ }
+
+ ret = hid_hw_open(hdev);
+ if (ret) {
+ hid_hw_stop(hdev);
+ kref_put(&sd->refcnt, steelseries_device_release);
+ return ret;
+ }
+ return 0;
+ }
+
+ return -ENODEV;
+
+err_stop:
+ hid_hw_stop(hdev);
+err_free:
+ /* drvdata is unpublished until full success, so no sibling can hold sd. */
+ kref_put(&sd->refcnt, steelseries_device_release);
+ return ret;
+}
+
+static void steelseries_arctis_remove(struct hid_device *hdev)
+{
+ struct steelseries_device *sd;
+ struct power_supply *battery;
+ unsigned long flags;
+ struct usb_interface *intf;
+ u8 interface_num;
+
+ if (hid_is_usb(hdev)) {
+ intf = to_usb_interface(hdev->dev.parent);
+ interface_num = intf->cur_altsetting->desc.bInterfaceNumber;
+ } else {
+ return;
+ }
+
+ sd = hid_get_drvdata(hdev);
+
+ if (!sd) {
+ hid_hw_stop(hdev);
+ return;
+ }
+
+ if (interface_num == sd->info->sync_interface) {
+ spin_lock_irqsave(&sd->lock, flags);
+ sd->removed = true;
+ battery = sd->battery;
+ sd->battery = NULL;
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ cancel_delayed_work_sync(&sd->status_work);
+
+ if (battery)
+ power_supply_unregister(battery);
+ }
+
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+
+ kref_put(&sd->refcnt, steelseries_device_release);
+}
+
+static int steelseries_arctis_raw_event(struct hid_device *hdev,
+ struct hid_report *report, u8 *data, int size)
+{
+ struct steelseries_device *sd = hid_get_drvdata(hdev);
+ u8 old_capacity;
+ bool old_connected;
+ bool old_charging;
+ bool is_async_interface;
+ unsigned long flags;
+
+ if (!sd)
+ return 0;
+
+ is_async_interface = (hdev != sd->hdev);
+
+ spin_lock_irqsave(&sd->lock, flags);
+
+ if (sd->removed) {
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+ }
+
+ old_capacity = sd->battery_capacity;
+ old_connected = sd->headset_connected;
+ old_charging = sd->battery_charging;
+
+ sd->info->parse_status(sd, data, size);
+
+ if (sd->headset_connected != old_connected) {
+ hid_dbg(hdev,
+ "Connected status changed from %sconnected to %sconnected\n",
+ old_connected ? "" : "not ",
+ sd->headset_connected ? "" : "not ");
+
+ if (sd->headset_connected && !old_connected &&
+ sd->info->async_interface && is_async_interface)
+ schedule_delayed_work(&sd->status_work, 0);
+
+ if (sd->battery) {
+ steelseries_headset_set_wireless_status(sd->hdev,
+ sd->headset_connected);
+ power_supply_changed(sd->battery);
+ }
+ }
+
+ if (sd->battery_capacity != old_capacity) {
+ hid_dbg(hdev, "Battery capacity changed from %d%% to %d%%\n",
+ old_capacity, sd->battery_capacity);
+ if (sd->battery)
+ power_supply_changed(sd->battery);
+ }
+
+ if (sd->battery_charging != old_charging) {
+ hid_dbg(hdev,
+ "Battery charging status changed from %scharging to %scharging\n",
+ old_charging ? "" : "not ",
+ sd->battery_charging ? "" : "not ");
+ if (sd->battery)
+ power_supply_changed(sd->battery);
+ }
+
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ return 0;
+}
+
+static const struct hid_device_id steelseries_arctis_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X),
+ .driver_data = (unsigned long)&arctis_1_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_9),
+ .driver_data = (unsigned long)&arctis_9_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X),
+ .driver_data = (unsigned long)&arctis_nova_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7),
+ .driver_data = (unsigned long)&arctis_nova_7_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X),
+ .driver_data = (unsigned long)&arctis_nova_7_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2),
+ .driver_data = (unsigned long)&arctis_nova_7_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO),
+ .driver_data = (unsigned long)&arctis_nova_7_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW),
+ .driver_data = (unsigned long)&arctis_nova_7_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_2026),
+ .driver_data = (unsigned long)&arctis_nova_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P_2026),
+ .driver_data = (unsigned long)&arctis_nova_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2026),
+ .driver_data = (unsigned long)&arctis_nova_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2026),
+ .driver_data = (unsigned long)&arctis_nova_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2),
+ .driver_data = (unsigned long)&arctis_nova_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2),
+ .driver_data = (unsigned long)&arctis_nova_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_2),
+ .driver_data = (unsigned long)&arctis_nova_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_3),
+ .driver_data = (unsigned long)&arctis_nova_info },
+ {}
+};
+MODULE_DEVICE_TABLE(hid, steelseries_arctis_devices);
+
+static struct hid_driver steelseries_arctis_driver = {
+ .name = "hid-steelseries-arctis",
+ .id_table = steelseries_arctis_devices,
+ .probe = steelseries_arctis_probe,
+ .remove = steelseries_arctis_remove,
+ .raw_event = steelseries_arctis_raw_event,
+};
+
+module_hid_driver(steelseries_arctis_driver);
+MODULE_DESCRIPTION("HID driver for Steelseries arctis headsets");
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Christian Mayer <git@mayer-bgk.de>");
+MODULE_AUTHOR("Bastien Nocera <hadess@hadess.net>");
+MODULE_AUTHOR("Sriman Achanta <srimanachanta@gmail.com>");