* [PATCH v2 0/2] HID: bpf: fix device destruction races
@ 2026-11-03 2:45 Yifan Zhang
2026-11-03 2:45 ` [PATCH v2 1/2] HID: bpf: synchronize hardware operations with device destruction Yifan Zhang
2026-11-03 2:45 ` [PATCH v2 2/2] HID: bpf: reject struct_ops registration during " Yifan Zhang
0 siblings, 2 replies; 4+ messages in thread
From: Yifan Zhang @ 2026-11-03 2:45 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input
Cc: Greg Kroah-Hartman, Alexei Starovoitov, bpf, linux-kernel
These patches fix two HID-BPF races with device destruction. The first
keeps report metadata and transport-private state alive across hardware
operations. The second prevents struct_ops registration from publishing
an entry after destruction has started.
Changes in v2:
- use a short lifetime admission lock and SRCU instead of holding
prog_list_lock across low-level driver calls
- acquire lifetime protection before looking up the HID report
- clear ops->hdev on registration errors before dropping the device ref
Yifan Zhang (2):
HID: bpf: synchronize hardware operations with device destruction
HID: bpf: reject struct_ops registration during device destruction
drivers/hid/bpf/hid_bpf_dispatch.c | 73 ++++++++++++++++++++--------
drivers/hid/bpf/hid_bpf_struct_ops.c | 12 ++++-
include/linux/hid_bpf.h | 1 +
3 files changed, 65 insertions(+), 21 deletions(-)
--
2.39.5 (Apple Git-154)
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v2 1/2] HID: bpf: synchronize hardware operations with device destruction
2026-11-03 2:45 [PATCH v2 0/2] HID: bpf: fix device destruction races Yifan Zhang
@ 2026-11-03 2:45 ` Yifan Zhang
2026-11-03 3:00 ` Alexei Starovoitov
2026-11-03 2:45 ` [PATCH v2 2/2] HID: bpf: reject struct_ops registration during " Yifan Zhang
1 sibling, 1 reply; 4+ messages in thread
From: Yifan Zhang @ 2026-11-03 2:45 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input
Cc: Greg Kroah-Hartman, Alexei Starovoitov, bpf, linux-kernel
A HID-BPF syscall context pins struct hid_device, but it does not pin the
transport-private data referenced by the low-level driver. A hardware
request can therefore sleep in the UHID report-wait path while closing the
UHID file destroys the HID device, wakes the waiter, and frees struct
uhid_device. The awakened request then accesses the freed object.
KASAN reports:
BUG: KASAN: use-after-free in __uhid_report_queue_and_wait
Read of size 1
uhid_hid_raw_request
__hid_hw_raw_request
hid_bpf_hw_request
bpf_prog_test_run_syscall
Add a lifetime lock that serializes admission of HID-BPF hardware
operations with device destruction. Take an SRCU read lock while holding
the lifetime lock before accessing the report tables or the low-level
driver. Destruction marks the device destroyed under the same lock and
waits for the SRCU readers before cleaning up the SRCU state.
The lifetime lock is released before calling the low-level driver. This
avoids reacquiring the same mutex through a reentrant low-level path and
does not introduce a lock dependency on driver_input_lock.
Fixes: 91a7f802d185 ("HID: bpf: introduce hid_hw_request()")
Closes: https://lore.kernel.org/linux-input/179317460423.87432.1451672332362541157@stu.pku.edu.cn/
Signed-off-by: Yifan Zhang <yfzhang23@stu.pku.edu.cn>
---
drivers/hid/bpf/hid_bpf_dispatch.c | 73 ++++++++++++++++++++++--------
include/linux/hid_bpf.h | 1 +
2 files changed, 55 insertions(+), 19 deletions(-)
diff --git a/drivers/hid/bpf/hid_bpf_dispatch.c b/drivers/hid/bpf/hid_bpf_dispatch.c
index 6b467c74601cb..0c0c8e9939f05 100644
--- a/drivers/hid/bpf/hid_bpf_dispatch.c
+++ b/drivers/hid/bpf/hid_bpf_dispatch.c
@@ -398,6 +398,18 @@ __hid_bpf_hw_check_params(struct hid_bpf_ctx *ctx, __u8 *buf, size_t *buf__sz,
return 0;
}
+static int hid_bpf_hw_operation_lock(struct hid_device *hdev)
+{
+ int idx = -ENODEV;
+
+ mutex_lock(&hdev->bpf.lifetime_lock);
+ if (!READ_ONCE(hdev->bpf.destroyed))
+ idx = srcu_read_lock(&hdev->bpf.srcu);
+ mutex_unlock(&hdev->bpf.lifetime_lock);
+
+ return idx;
+}
+
/**
* hid_bpf_hw_request - Communicate with a HID device
*
@@ -416,17 +428,21 @@ hid_bpf_hw_request(struct hid_bpf_ctx *ctx, __u8 *buf, size_t buf__sz,
struct hid_bpf_ctx_kern *ctx_kern;
size_t size = buf__sz;
u8 *dma_data;
- int ret;
+ int idx, ret;
ctx_kern = container_of(ctx, struct hid_bpf_ctx_kern, ctx);
if (ctx_kern->from_bpf)
return -EDEADLOCK;
+ idx = hid_bpf_hw_operation_lock(ctx->hid);
+ if (idx < 0)
+ return idx;
+
/* check arguments */
ret = __hid_bpf_hw_check_params(ctx, buf, &size, rtype, true);
if (ret)
- return ret;
+ goto out_unlock;
switch (reqtype) {
case HID_REQ_GET_REPORT:
@@ -437,21 +453,24 @@ hid_bpf_hw_request(struct hid_bpf_ctx *ctx, __u8 *buf, size_t buf__sz,
case HID_REQ_SET_PROTOCOL:
break;
default:
- return -EINVAL;
+ ret = -EINVAL;
+ goto out_unlock;
}
dma_data = kmemdup(buf, size, GFP_KERNEL);
- if (!dma_data)
- return -ENOMEM;
+ if (!dma_data) {
+ ret = -ENOMEM;
+ goto out_unlock;
+ }
ret = hid_ops->hid_hw_raw_request(ctx->hid,
- dma_data[0],
- dma_data,
- size,
- rtype,
- reqtype,
- (u64)(long)ctx,
- true); /* prevent infinite recursions */
+ dma_data[0],
+ dma_data,
+ size,
+ rtype,
+ reqtype,
+ (u64)(long)ctx,
+ true); /* prevent infinite recursions */
if (ret > (ssize_t)size)
ret = size;
@@ -459,6 +478,8 @@ hid_bpf_hw_request(struct hid_bpf_ctx *ctx, __u8 *buf, size_t buf__sz,
memcpy(buf, dma_data, ret);
kfree(dma_data);
+out_unlock:
+ srcu_read_unlock(&ctx->hid->bpf.srcu, idx);
return ret;
}
@@ -477,24 +498,33 @@ hid_bpf_hw_output_report(struct hid_bpf_ctx *ctx, __u8 *buf, size_t buf__sz)
struct hid_bpf_ctx_kern *ctx_kern;
size_t size = buf__sz;
u8 *dma_data;
- int ret;
+ int idx, ret;
ctx_kern = container_of(ctx, struct hid_bpf_ctx_kern, ctx);
if (ctx_kern->from_bpf)
return -EDEADLOCK;
+ idx = hid_bpf_hw_operation_lock(ctx->hid);
+ if (idx < 0)
+ return idx;
+
/* check arguments */
ret = __hid_bpf_hw_check_params(ctx, buf, &size, HID_OUTPUT_REPORT, true);
if (ret)
- return ret;
+ goto out_unlock;
dma_data = kmemdup(buf, size, GFP_KERNEL);
- if (!dma_data)
- return -ENOMEM;
+ if (!dma_data) {
+ ret = -ENOMEM;
+ goto out_unlock;
+ }
- ret = hid_ops->hid_hw_output_report(ctx->hid, dma_data, size, (u64)(long)ctx, true);
+ ret = hid_ops->hid_hw_output_report(ctx->hid, dma_data, size,
+ (u64)(long)ctx, true);
kfree(dma_data);
+out_unlock:
+ srcu_read_unlock(&ctx->hid->bpf.srcu, idx);
return ret;
}
@@ -641,8 +671,12 @@ void hid_bpf_destroy_device(struct hid_device *hdev)
if (!hdev)
return;
- /* mark the device as destroyed in bpf so we don't reattach it */
- hdev->bpf.destroyed = true;
+ /* Block new BPF hardware operations before transport teardown. */
+ mutex_lock(&hdev->bpf.prog_list_lock);
+ mutex_lock(&hdev->bpf.lifetime_lock);
+ WRITE_ONCE(hdev->bpf.destroyed, true);
+ mutex_unlock(&hdev->bpf.lifetime_lock);
+ mutex_unlock(&hdev->bpf.prog_list_lock);
__hid_bpf_ops_destroy_device(hdev);
@@ -655,6 +689,7 @@ int hid_bpf_device_init(struct hid_device *hdev)
{
INIT_LIST_HEAD(&hdev->bpf.prog_list);
mutex_init(&hdev->bpf.prog_list_lock);
+ mutex_init(&hdev->bpf.lifetime_lock);
return init_srcu_struct(&hdev->bpf.srcu);
}
EXPORT_SYMBOL_GPL(hid_bpf_device_init);
diff --git a/include/linux/hid_bpf.h b/include/linux/hid_bpf.h
index 19fffa4574a47..3624937d340b3 100644
--- a/include/linux/hid_bpf.h
+++ b/include/linux/hid_bpf.h
@@ -195,6 +195,7 @@ struct hid_bpf {
struct hid_bpf_ops *rdesc_ops;
struct list_head prog_list;
struct mutex prog_list_lock; /* protects prog_list update */
+ struct mutex lifetime_lock; /* protects destroyed and SRCU admission */
struct srcu_struct srcu; /* protects prog_list read-only access */
};
--
2.39.5 (Apple Git-154)
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v2 2/2] HID: bpf: reject struct_ops registration during device destruction
2026-11-03 2:45 [PATCH v2 0/2] HID: bpf: fix device destruction races Yifan Zhang
2026-11-03 2:45 ` [PATCH v2 1/2] HID: bpf: synchronize hardware operations with device destruction Yifan Zhang
@ 2026-11-03 2:45 ` Yifan Zhang
1 sibling, 0 replies; 4+ messages in thread
From: Yifan Zhang @ 2026-11-03 2:45 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input
Cc: Greg Kroah-Hartman, Alexei Starovoitov, bpf, linux-kernel
hid_bpf_reg() can obtain a device reference before hid_destroy_device()
removes the device from the bus, then block on prog_list_lock while HID-BPF
destruction cleans the device's SRCU state. The registration path does not
check the destroyed flag after taking the lock, so it can publish a new
struct_ops entry and call synchronize_srcu() on the cleaned SRCU object.
KASAN reports:
BUG: KASAN: slab-use-after-free in check_init_srcu_struct
Read of size 8
check_init_srcu_struct
synchronize_srcu
hid_bpf_reg
bpf_struct_ops_link_create
Check destroyed while holding prog_list_lock. Device destruction sets the
flag while holding the same lock, so a registration either completes before
destruction starts or fails with -ENODEV without accessing the SRCU state.
Also clear ops->hdev on every registration error before dropping the device
reference. This keeps a failed struct_ops instance reusable and prevents a
dangling device pointer from reaching a later unregister operation.
Fixes: ebc0d8093e8c ("HID: bpf: implement HID-BPF through bpf_struct_ops")
Closes: https://lore.kernel.org/linux-input/179317460937.87455.1799017093083467190@stu.pku.edu.cn/
Signed-off-by: Yifan Zhang <yfzhang23@stu.pku.edu.cn>
---
drivers/hid/bpf/hid_bpf_struct_ops.c | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/drivers/hid/bpf/hid_bpf_struct_ops.c b/drivers/hid/bpf/hid_bpf_struct_ops.c
index c90b68956cb3b..31e2cb146b92c 100644
--- a/drivers/hid/bpf/hid_bpf_struct_ops.c
+++ b/drivers/hid/bpf/hid_bpf_struct_ops.c
@@ -201,6 +201,11 @@ static int hid_bpf_reg(void *kdata, struct bpf_link *link)
mutex_lock(&hdev->bpf.prog_list_lock);
+ if (READ_ONCE(hdev->bpf.destroyed)) {
+ err = -ENODEV;
+ goto out_unlock;
+ }
+
count = list_count_nodes(&hdev->bpf.prog_list);
if (count >= HID_BPF_MAX_PROGS_PER_DEV) {
err = -E2BIG;
@@ -229,11 +234,14 @@ static int hid_bpf_reg(void *kdata, struct bpf_link *link)
synchronize_srcu(&hdev->bpf.srcu);
out_unlock:
- mutex_unlock(&hdev->bpf.prog_list_lock);
-
if (err) {
if (hdev->bpf.rdesc_ops == ops)
hdev->bpf.rdesc_ops = NULL;
+ ops->hdev = NULL;
+ }
+ mutex_unlock(&hdev->bpf.prog_list_lock);
+
+ if (err) {
hid_put_device(hdev);
} else if (ops->hid_rdesc_fixup) {
hid_bpf_reconnect(hdev);
--
2.39.5 (Apple Git-154)
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH v2 1/2] HID: bpf: synchronize hardware operations with device destruction
2026-11-03 2:45 ` [PATCH v2 1/2] HID: bpf: synchronize hardware operations with device destruction Yifan Zhang
@ 2026-11-03 3:00 ` Alexei Starovoitov
0 siblings, 0 replies; 4+ messages in thread
From: Alexei Starovoitov @ 2026-11-03 3:00 UTC (permalink / raw)
To: Yifan Zhang, Jiri Kosina, Benjamin Tissoires, linux-input
Cc: Greg Kroah-Hartman, bpf, linux-kernel
On Mon, Nov 02, 2026 at 06:45 PM Yifan Zhang <yfzhang23@stu.pku.edu.cn> wrote:
> Add a lifetime lock that serializes admission of HID-BPF hardware
> operations with device destruction. Take an SRCU read lock while holding
> the lifetime lock before accessing the report tables or the low-level
> driver. Destruction marks the device destroyed under the same lock and
> waits for the SRCU readers before cleaning up the SRCU state.
The lifetime lock is needed only because hid_bpf_destroy_device() calls
cleanup_srcu_struct(). After that a reference to hid_device is not
enough to call srcu_read_lock().
The splat in patch 2 has the same cause. So does
commit 578e1b96fad7 ("HID: bpf: abort dispatch if device destroyed").
Lets keep hdev->bpf.srcu valid until hiddev_free().
Then the check of 'destroyed' goes inside the SRCU section.
No need for the 2nd mutex.
The dispatchers have the same race and this patch leaves it.
hid_bpf_destroy_device() runs before hidraw_disconnect():
hidraw ioctl hid_destroy_device()
dispatch_hid_bpf_raw_requests()
destroyed == false
destroyed = true
synchronize_srcu()
cleanup_srcu_struct()
srcu_read_lock() <- use after free
hid_close_report() also runs on unbind and on device_reprobe().
SRCU doesn't cover that. Neither this patch nor the diff below fixes it.
Instead of patches 1 and 2:
and Pls wait for Benjamin before sending v3.
diff --git a/drivers/hid/bpf/hid_bpf_dispatch.c b/drivers/hid/bpf/hid_bpf_dispatch.c
index 6b467c7..6c2b136 100644
--- a/drivers/hid/bpf/hid_bpf_dispatch.c
+++ b/drivers/hid/bpf/hid_bpf_dispatch.c
@@ -99,13 +99,15 @@ int dispatch_hid_bpf_raw_requests(struct hid_device *hdev,
struct hid_bpf_ops *e;
int ret, idx;
- if (unlikely(hdev->bpf.destroyed))
- return -ENODEV;
-
if (rtype >= HID_REPORT_TYPES)
return -EINVAL;
idx = srcu_read_lock(&hdev->bpf.srcu);
+ if (unlikely(hdev->bpf.destroyed)) {
+ ret = -ENODEV;
+ goto out;
+ }
+
list_for_each_entry_srcu(e, &hdev->bpf.prog_list, list,
srcu_read_lock_held(&hdev->bpf.srcu)) {
if (!e->hid_hw_request)
@@ -139,10 +141,12 @@ int dispatch_hid_bpf_output_report(struct hid_device *hdev,
struct hid_bpf_ops *e;
int ret, idx;
- if (unlikely(hdev->bpf.destroyed))
- return -ENODEV;
-
idx = srcu_read_lock(&hdev->bpf.srcu);
+ if (unlikely(hdev->bpf.destroyed)) {
+ ret = -ENODEV;
+ goto out;
+ }
+
list_for_each_entry_srcu(e, &hdev->bpf.prog_list, list,
srcu_read_lock_held(&hdev->bpf.srcu)) {
if (!e->hid_hw_output_report)
@@ -423,6 +427,11 @@ hid_bpf_hw_request(struct hid_bpf_ctx *ctx, __u8 *buf, size_t buf__sz,
if (ctx_kern->from_bpf)
return -EDEADLOCK;
+ /* hid_bpf_destroy_device() waits for us before the transport goes away */
+ guard(srcu)(&ctx->hid->bpf.srcu);
+ if (ctx->hid->bpf.destroyed)
+ return -ENODEV;
+
/* check arguments */
ret = __hid_bpf_hw_check_params(ctx, buf, &size, rtype, true);
if (ret)
@@ -483,6 +492,10 @@ hid_bpf_hw_output_report(struct hid_bpf_ctx *ctx, __u8 *buf, size_t buf__sz)
if (ctx_kern->from_bpf)
return -EDEADLOCK;
+ guard(srcu)(&ctx->hid->bpf.srcu);
+ if (ctx->hid->bpf.destroyed)
+ return -ENODEV;
+
/* check arguments */
ret = __hid_bpf_hw_check_params(ctx, buf, &size, HID_OUTPUT_REPORT, true);
if (ret)
@@ -641,13 +654,9 @@ void hid_bpf_destroy_device(struct hid_device *hdev)
if (!hdev)
return;
- /* mark the device as destroyed in bpf so we don't reattach it */
- hdev->bpf.destroyed = true;
-
__hid_bpf_ops_destroy_device(hdev);
synchronize_srcu(&hdev->bpf.srcu);
- cleanup_srcu_struct(&hdev->bpf.srcu);
}
EXPORT_SYMBOL_GPL(hid_bpf_destroy_device);
@@ -659,6 +668,13 @@ int hid_bpf_device_init(struct hid_device *hdev)
}
EXPORT_SYMBOL_GPL(hid_bpf_device_init);
+/* A reference to hdev is enough to use hdev->bpf.srcu */
+void hid_bpf_device_free(struct hid_device *hdev)
+{
+ cleanup_srcu_struct(&hdev->bpf.srcu);
+}
+EXPORT_SYMBOL_GPL(hid_bpf_device_free);
+
static int __init hid_bpf_init(void)
{
int err;
diff --git a/drivers/hid/bpf/hid_bpf_struct_ops.c b/drivers/hid/bpf/hid_bpf_struct_ops.c
index c90b689..f3bbe11 100644
--- a/drivers/hid/bpf/hid_bpf_struct_ops.c
+++ b/drivers/hid/bpf/hid_bpf_struct_ops.c
@@ -197,10 +197,13 @@ static int hid_bpf_reg(void *kdata, struct bpf_link *link)
if (IS_ERR(hdev))
return PTR_ERR(hdev);
- ops->hdev = hdev;
-
mutex_lock(&hdev->bpf.prog_list_lock);
+ if (hdev->bpf.destroyed) {
+ err = -ENODEV;
+ goto out_unlock;
+ }
+
count = list_count_nodes(&hdev->bpf.prog_list);
if (count >= HID_BPF_MAX_PROGS_PER_DEV) {
err = -E2BIG;
@@ -222,6 +225,7 @@ static int hid_bpf_reg(void *kdata, struct bpf_link *link)
goto out_unlock;
}
+ ops->hdev = hdev;
if (ops->flags & BPF_F_BEFORE)
list_add_rcu(&ops->list, &hdev->bpf.prog_list);
else
@@ -324,6 +328,8 @@ void __hid_bpf_ops_destroy_device(struct hid_device *hdev)
int count = 0;
mutex_lock(&hdev->bpf.prog_list_lock);
+ /* mark the device as destroyed in bpf so we don't reattach it */
+ hdev->bpf.destroyed = true;
list_for_each_entry(e, &hdev->bpf.prog_list, list) {
e->hdev = NULL;
count++;
diff --git a/drivers/hid/hid-core.c b/drivers/hid/hid-core.c
index b4fd482..827a0ee 100644
--- a/drivers/hid/hid-core.c
+++ b/drivers/hid/hid-core.c
@@ -768,6 +768,7 @@ void hiddev_free(struct kref *ref)
hid_close_report(hid);
hid_free_bpf_rdesc(hid);
+ hid_bpf_device_free(hid);
kfree(hid->dev_rdesc);
kfree(hid);
}
diff --git a/include/linux/hid_bpf.h b/include/linux/hid_bpf.h
index 19fffa4..0db7f67 100644
--- a/include/linux/hid_bpf.h
+++ b/include/linux/hid_bpf.h
@@ -213,6 +213,7 @@ int hid_bpf_connect_device(struct hid_device *hdev);
void hid_bpf_disconnect_device(struct hid_device *hdev);
void hid_bpf_destroy_device(struct hid_device *hid);
int hid_bpf_device_init(struct hid_device *hid);
+void hid_bpf_device_free(struct hid_device *hid);
const u8 *call_hid_bpf_rdesc_fixup(struct hid_device *hdev, const u8 *rdesc, unsigned int *size);
#else /* CONFIG_HID_BPF */
static inline u8 *dispatch_hid_bpf_device_event(struct hid_device *hid, enum hid_report_type type,
@@ -232,6 +233,7 @@ static inline int hid_bpf_connect_device(struct hid_device *hdev) { return 0; }
static inline void hid_bpf_disconnect_device(struct hid_device *hdev) {}
static inline void hid_bpf_destroy_device(struct hid_device *hid) {}
static inline int hid_bpf_device_init(struct hid_device *hid) { return 0; }
+static inline void hid_bpf_device_free(struct hid_device *hid) {}
static inline const u8 *call_hid_bpf_rdesc_fixup(struct hid_device *hdev, const u8 *rdesc,
unsigned int *size) { return rdesc; }
pw-bot: cr
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-11-03 3:00 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-03 2:45 [PATCH v2 0/2] HID: bpf: fix device destruction races Yifan Zhang
2026-11-03 2:45 ` [PATCH v2 1/2] HID: bpf: synchronize hardware operations with device destruction Yifan Zhang
2026-11-03 3:00 ` Alexei Starovoitov
2026-11-03 2:45 ` [PATCH v2 2/2] HID: bpf: reject struct_ops registration during " Yifan Zhang
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®