* [PATCH RFC v2 RESEND] HID: BPF: add keyboard behavioral anomaly detection
@ 2026-07-22 11:48 Krish Gulati
2026-07-30 8:12 ` Benjamin Tissoires
0 siblings, 1 reply; 3+ messages in thread
From: Krish Gulati @ 2026-07-22 11:48 UTC (permalink / raw)
To: bentiss, jikos; +Cc: linux-input, linux-kernel, krishgulati7
This patch implements a HID-BPF struct_ops program that detects automated
HID injection attacks. It does this by measuring post-enumeration delay
and tracking inter-keystroke timing using Welford's online variance algorithm.
State is stored in a hash map keyed by the HID ID.
Detection results are currently surfaced via bpf_printk(). A
BPF_MAP_TYPE_RINGBUF interface with configurable userspace daemon
is planned; deferred pending validation of detection heuristics.
Signal design is grounded in: Neuner et al., "USBlock: Blocking
USB-based Keylogger Attacks", DBSec 2018.
Link: https://lore.kernel.org/linux-input/adSxXidgeWF0-Ewn@beelink/
Signed-off-by: Krish Gulati <krishgulati7@gmail.com>
Suggested-by: Sashiko-bot <sashiko-bot@kernel.org>
---
Changes since v1:
- Added bpf_spin_lock/unlock around the Welford compound update in
kbd_hook() to close a data race on concurrent execution (was
lockless read-modify-write).
- Switched dev_details from LRU_HASH to plain HASH so eviction
under map pressure fails the insert instead of silently
displacing an existing tracked device.
- Store report_id in dev_info and gate kbd_hook() on it, so
composite devices no longer misattribute other report types'
bytes as keystrokes.
- Zero dev_info with __builtin_memset before populating in probe()
to satisfy the verifier's stack-init tracking (was relying on a
partial designated initializer).
- Replaced active-byte-count heuristic with bytewise diff/mask
(standard_boot_keypress/nkro_keypress) so consecutive keystrokes
without an intervening empty report are still detected.
- Verified bucket-size rounding (8/16/32/64) does not drop events
or read out-of-bounds, tested via hid-replay with synthetic
report sizes (9, 14 bytes, etc.).
Known limitations, not addressed in this version:
- No cleanup on device disconnect: hid_bpf_ops has no disconnect
hook, so dev_details entries persist until the map fills.
Deferred pending maintainer guidance on the right approach.
- find_keyboard_field() returns on the first Keyboard-typed
(GenericDesktop/Keyboard) Application Collection found; a device
with multiple such collections within a single report descriptor
(as opposed to multiple HID interfaces, which are already handled
correctly and verified on real composite hardware) will only have
the first one tracked. Untested; the only hardware available for
testing exhibits the multiple-interface pattern rather than the
single-descriptor multi-collection pattern, so this path has not
been exercised.
Resending as a top-level thread (v2 was sent as a reply to v1 on July 9).
Original thread: https://lore.kernel.org/linux-input/20260709104958.38303-1-krishgulati7@gmail.com
src/bpf/testing/0010-Generic__keyboard.bpf.c | 311 +++++++++++++------
src/bpf/testing/meson.build | 1 +
2 files changed, 217 insertions(+), 95 deletions(-)
diff --git a/src/bpf/testing/0010-Generic__keyboard.bpf.c b/src/bpf/testing/0010-Generic__keyboard.bpf.c
index 9114587..c3f2364 100644
--- a/src/bpf/testing/0010-Generic__keyboard.bpf.c
+++ b/src/bpf/testing/0010-Generic__keyboard.bpf.c
@@ -24,11 +24,11 @@
#define HID_GUARD_PED_SUSPICIOUS_THRESH (50 * HID_GUARD_NSEC_PER_MSEC)
#define HID_GUARD_PED_WARNING_THRESH (300 * HID_GUARD_NSEC_PER_MSEC)
-/*not derived from real typing data yet*/
+/*not derived from real typing raw_buffer yet*/
#define HID_GUARD_MIN_SAMPLES 5
/*
- * Variance is "too metronomic
+ * Variance is "too metronomic"
* to be a human," expressed in ms^2 so we never need sqrt().
*/
#define HID_GUARD_VARIANCE_THRESH_MS2 (40ULL * 40ULL)
@@ -57,9 +57,17 @@ enum hid_guard_ped_flag {
};
struct dev_info {
- __u64 connection_time;
+ struct bpf_spin_lock lock;
+ __u8 report_id;
+ __u8 field_type;
+ __u8 prev_report[64];
+ __u16 bits_start;
+ __u16 bits_end;
+ __u16 usage_id;
__u32 report_size;
- __u64 prev_report[32];
+ __u32 keyboard_offset;
+ __u64 connection_time;
+ __u64 raw_buffer_length;
/*welford's variables*/
__u64 prev_keydown_ts;
__u64 count;
@@ -71,22 +79,8 @@ struct dev_info {
*/
};
-/*
- * BPF_MAP_TYPE_LRU_HASH:
- * Using an LRU map automatically prevents exhaustion by silently evicting
- * the oldest idle devices. It requires no syntax changes to the rest of the
- * code (lookup/update helpers work identically), but introduces behavioral
- * trade-offs:
- *
- * 1. Eviction wipes Welford variance history. An attacker
- * could theoretically flood the map to flush their device and reset their
- * score.
- * 2. PED Blindspot: Eviction deletes the 'connection_time' set during probe().
- * If an evicted device wakes up, it will bypass post-enumeration delay
- * checks.
- */
struct {
- __uint(type, BPF_MAP_TYPE_LRU_HASH);
+ __uint(type, BPF_MAP_TYPE_HASH);
__type(key, __u32);
__type(value, struct dev_info);
__uint(max_entries, 128);
@@ -138,11 +132,29 @@ static __always_inline void welford(struct dev_info *dev_state,
delta2 = x - dev_state->mean;
dev_state->M2 += (__u64)(delta * delta2);
+}
- bpf_printk("W[Count:%llu] Int:%llu ms, scaled_x:%lld\n",
- dev_state->count, interval_ms, x);
- bpf_printk(" -> delta1:%lld, mean:%lld\n", delta, dev_state->mean);
- bpf_printk(" -> delta2:%lld, M2:%llu\n", delta2, dev_state->M2);
+static __always_inline bool standard_boot_keypress(__u8 *current_report,
+ __u8 *prev_report)
+{
+ for (int i = 2; i < 8; i++) {
+ if (current_report[i] != prev_report[i])
+ return 1;
+ }
+ return 0;
+}
+
+static __always_inline bool
+nkro_keypress(__u8 *current_report, __u8 *prev_report, __u64 buffer_length)
+{
+ for (int i = 0; i < 64; i++) {
+ if (i >= buffer_length)
+ break;
+
+ if (current_report[i] & ~prev_report[i])
+ return 1;
+ }
+ return 0;
}
HID_BPF_CONFIG(HID_DEVICE(BUS_USB, HID_GROUP_ANY, HID_VID_ANY, HID_PID_ANY),
@@ -150,7 +162,7 @@ HID_BPF_CONFIG(HID_DEVICE(BUS_USB, HID_GROUP_ANY, HID_VID_ANY, HID_PID_ANY),
HID_PID_ANY));
SEC(HID_BPF_DEVICE_EVENT)
-int BPF_PROG(kdb_hook, struct hid_bpf_ctx *hctx)
+int BPF_PROG(kbd_hook, struct hid_bpf_ctx *hctx)
{
__u32 hid_id = hctx->hid->id;
@@ -161,124 +173,233 @@ int BPF_PROG(kdb_hook, struct hid_bpf_ctx *hctx)
if (!info)
return 0;
- __u32 size = info->report_size;
- __u32 fetch_size;
- __u8 *data;
+ __u8 *raw_buffer;
- if (size <= 8)
- fetch_size = 8;
- else if (size <= 16)
- fetch_size = 16;
- else
- fetch_size = 32;
+ __u8 *report_id_data = hid_bpf_get_data(hctx, 0, 1);
- data = hid_bpf_get_data(hctx, 0, fetch_size);
+ if (!report_id_data)
+ return 0;
- if (!data)
+ if (info->report_id != 0 && report_id_data[0] != info->report_id)
return 0;
- int now_active_ks = 0, was_active_ks = 0;
-#pragma unroll
- for (int i = 0; i < 32; i++) {
- if (i >= fetch_size)
- break;
- now_active_ks += ((__u8)data[i] + 255) >> 8;
- was_active_ks += ((__u8)info->prev_report[i] + 255) >> 8;
- info->prev_report[i] = data[i];
+ __u32 offset = info->keyboard_offset + (info->report_id != 0 ? 1 : 0);
+
+ __u64 buffer_length = info->raw_buffer_length;
+
+ if (buffer_length <= 8)
+ raw_buffer = hid_bpf_get_data(hctx, offset, 8);
+ else if (buffer_length <= 16)
+ raw_buffer = hid_bpf_get_data(hctx, offset, 16);
+ else if (buffer_length <= 32)
+ raw_buffer = hid_bpf_get_data(hctx, offset, 32);
+ else
+ raw_buffer = hid_bpf_get_data(hctx, offset, 64);
+
+ if (!raw_buffer)
+ return 0;
+
+ bool new_key_pressed = false;
+
+ if (info->report_id == 0) {
+ new_key_pressed =
+ standard_boot_keypress(raw_buffer, info->prev_report);
+ } else {
+ new_key_pressed = nkro_keypress(raw_buffer, info->prev_report,
+ buffer_length);
}
- __u64 current_ms = bpf_ktime_get_ns() / HID_GUARD_NSEC_PER_MSEC;
+ __u64 now = bpf_ktime_get_ns();
- if (now_active_ks > was_active_ks) {
+ __u64 interval_ms = 0;
+ __u64 variance_m2 = 0;
+ bool is_idle_gap = false;
+ bool is_suspicious = false;
+ enum hid_guard_ped_flag ped_flag = HID_GUARD_PED_NO_ENTRY;
+ bool run_ped = false;
+
+ bpf_spin_lock(&info->lock);
+
+ if (new_key_pressed) {
if (info->prev_keydown_ts != 0) {
- __u64 interval_ms = current_ms - info->prev_keydown_ts;
+ interval_ms = (now - info->prev_keydown_ts) /
+ HID_GUARD_NSEC_PER_MSEC;
- if (interval_ms < HID_GUARD_IDLE_GAP_THRESH_MS) {
+ if (interval_ms < HID_GUARD_IDLE_GAP_THRESH_MS)
welford(info, interval_ms);
- } else {
- bpf_printk(
- "hid %d: idle gap %llu ms excluded from sample\n",
- hid_id, interval_ms);
- }
+ else
+ is_idle_gap = true;
}
+ /*
+ * Variance check runs after welford() so the current
+ * sample is already folded in before we decide.
+ * Guard on MIN_SAMPLES: Welford's unbiased estimator
+ * (M2 / (count - 1)) is undefined for count < 2, and
+ * unreliable until a few samples have accumulated.
+ */
if (info->count >= HID_GUARD_MIN_SAMPLES) {
- __u64 variance_m2 =
+ variance_m2 =
info->M2 /
((__u64)HID_GUARD_WELFORD_SCALE *
HID_GUARD_WELFORD_SCALE * (info->count - 1));
- /*
- * the initial interval x was multiplied by HID_GUARD_WELFORD_SCALE, both
- * delta and delta2 are also scaled by that factor,
- * thus scale^2 in the denominator
- */
- if (variance_m2 < HID_GUARD_VARIANCE_THRESH_MS2) {
- bpf_printk(
- "hid %d: Suspeciously regular typing, variance=%llu ms^2\n",
- hid_id, variance_m2);
- }
+ if (variance_m2 < HID_GUARD_VARIANCE_THRESH_MS2)
+ is_suspicious = true;
}
- info->prev_keydown_ts = current_ms;
+ info->prev_keydown_ts = now;
}
- if (info->connection_time != 0) {
- enum hid_guard_ped_flag ped_flag =
- post_enumeration_delay(info, bpf_ktime_get_ns());
-
- bpf_printk("PED flag for hid %d: %d\n", hid_id, ped_flag);
+ for (int i = 0; i < 64; i++) {
+ if (i >= buffer_length)
+ break;
+ info->prev_report[i] = raw_buffer[i];
+ }
- /*
- * Prevent re-evaluation on subsequent packets for this device
- */
+ /*
+ * PED: fires exactly once per device lifetime. connection_time
+ * is set at probe() time; we clear it here so subsequent events
+ * skip this branch entirely.
+ */
+ if (info->connection_time != 0) {
+ ped_flag = post_enumeration_delay(info, now);
info->connection_time = 0;
+ run_ped = true;
}
+
+ bpf_spin_unlock(&info->lock);
+
+ if (new_key_pressed) {
+ if (is_idle_gap)
+ bpf_printk(
+ "hid %d: idle gap %llu ms excluded from sample\n",
+ hid_id, interval_ms);
+ if (is_suspicious)
+ bpf_printk(
+ "hid %d: suspiciously regular typing, variance=%llu ms^2\n",
+ hid_id, variance_m2);
+ }
+
+ if (run_ped)
+ bpf_printk("hid %d: PED flag=%d\n", hid_id, ped_flag);
+
return 0;
}
HID_BPF_OPS(hook_keyboard) = {
- .hid_device_event = (void *)kdb_hook,
+ .hid_device_event = (void *)kbd_hook,
};
struct hid_rdesc_descriptor HID_REPORT_DESCRIPTOR;
-SEC("syscall")
-int probe(struct hid_bpf_probe_args *ctx)
+static __always_inline bool find_keyboard_field(__u16 *bits_start,
+ __u16 *bits_end,
+ __u8 *report_id,
+ __u32 *size_in_bytes)
{
struct hid_rdesc_report *input;
struct hid_rdesc_field *field;
struct hid_rdesc_collection *col;
hid_bpf_for_each_input_report(&HID_REPORT_DESCRIPTOR, input) {
- __u32 size_in_bytes = (input->size_in_bits + 7) / 8;
-
- bpf_printk("Report size: %d\n", size_in_bytes);
- if (input->report_id != 0)
- size_in_bytes += 1;
-
- bpf_printk("Report size after report_id: %d\n", size_in_bytes);
-
hid_bpf_for_each_field(input, field) {
hid_bpf_for_each_collection(field, col) {
if (col->usage_page ==
HidUsagePage_GenericDesktop &&
col->usage_id == HidUsage_GD_Keyboard) {
- __u32 key = ctx->hid;
- struct dev_info info = {
- .connection_time =
- bpf_ktime_get_ns(),
- .count = 0,
- .report_size = size_in_bytes
- };
- bpf_map_update_elem(&dev_details, &key,
- &info, BPF_ANY);
- ctx->retval = 0;
- return 0;
+ *size_in_bytes =
+ input->size_in_bits / 8;
+
+ if (input->report_id != 0)
+ *size_in_bytes += 1;
+
+ *bits_start = field->bits_start;
+ *bits_end = field->bits_end;
+ *report_id = input->report_id;
+ return true;
}
}
}
}
- ctx->retval = -EINVAL;
+ return false;
+}
+
+SEC("syscall")
+int probe(struct hid_bpf_probe_args *ctx)
+{
+ __u8 report_id;
+ __u16 bits_start, bits_end;
+ __u32 size_in_bytes;
+ __u32 hid_id = ctx->hid;
+
+ struct dev_info *existing_info =
+ bpf_map_lookup_elem(&dev_details, &hid_id);
+
+ struct dev_info info;
+
+ __builtin_memset(&info, 0, sizeof(info));
+
+ if (!find_keyboard_field(&bits_start, &bits_end, &report_id,
+ &size_in_bytes)) {
+ ctx->retval = -EINVAL;
+ return 0;
+ }
+
+ if (existing_info != NULL) {
+ __u64 now = bpf_ktime_get_ns();
+
+ bpf_spin_lock(&existing_info->lock);
+
+ existing_info->connection_time = now;
+
+ existing_info->bits_start = bits_start;
+ existing_info->bits_end = bits_end;
+ existing_info->keyboard_offset = bits_start / 8;
+ existing_info->report_size = size_in_bytes;
+
+ if (existing_info->keyboard_offset >
+ existing_info->report_size) {
+ bpf_spin_unlock(&existing_info->lock);
+ ctx->retval = -EINVAL;
+ return 0;
+ }
+
+ existing_info->report_id = report_id;
+ existing_info->raw_buffer_length =
+ existing_info->report_size -
+ (existing_info->keyboard_offset -
+ (report_id != 0 ? 1 : 0));
+
+ existing_info->count = 0;
+ existing_info->mean = 0;
+ existing_info->M2 = 0;
+ existing_info->prev_keydown_ts = 0;
+
+ __builtin_memset(existing_info->prev_report, 0,
+ sizeof(existing_info->prev_report));
+
+ bpf_spin_unlock(&existing_info->lock);
+ ctx->retval = 0;
+ return 0;
+ }
+
+ info.connection_time = bpf_ktime_get_ns();
+ info.bits_start = bits_start;
+ info.bits_end = bits_end;
+ info.keyboard_offset = bits_start / 8;
+ info.report_size = size_in_bytes;
+
+ if (info.keyboard_offset > info.report_size) {
+ ctx->retval = -EINVAL;
+ return 0;
+ }
+
+ info.report_id = report_id;
+ info.raw_buffer_length = info.report_size - (info.keyboard_offset -
+ (report_id != 0 ? 1 : 0));
+
+ bpf_map_update_elem(&dev_details, &hid_id, &info, BPF_NOEXIST);
+ ctx->retval = 0;
return 0;
}
diff --git a/src/bpf/testing/meson.build b/src/bpf/testing/meson.build
index 9d1b5d3..2586aea 100644
--- a/src/bpf/testing/meson.build
+++ b/src/bpf/testing/meson.build
@@ -11,6 +11,7 @@ tracing_sources = [
# 'sources' are BPF programs only compatible with
# struct_ops (kernel v6.11+)
sources = [
+ '0010-Generic__keyboard.bpf.c',
]
foreach bpf: tracing_sources
--
2.55.0
^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: [PATCH RFC v2 RESEND] HID: BPF: add keyboard behavioral anomaly detection
2026-07-22 11:48 [PATCH RFC v2 RESEND] HID: BPF: add keyboard behavioral anomaly detection Krish Gulati
@ 2026-07-30 8:12 ` Benjamin Tissoires
2026-07-30 9:57 ` Krish Gulati
0 siblings, 1 reply; 3+ messages in thread
From: Benjamin Tissoires @ 2026-07-30 8:12 UTC (permalink / raw)
To: Krish Gulati; +Cc: jikos, linux-input, linux-kernel
Hi,
On Jul 22 2026, Krish Gulati wrote:
> This patch implements a HID-BPF struct_ops program that detects automated
> HID injection attacks. It does this by measuring post-enumeration delay
> and tracking inter-keystroke timing using Welford's online variance algorithm.
> State is stored in a hash map keyed by the HID ID.
>
> Detection results are currently surfaced via bpf_printk(). A
> BPF_MAP_TYPE_RINGBUF interface with configurable userspace daemon
> is planned; deferred pending validation of detection heuristics.
>
> Signal design is grounded in: Neuner et al., "USBlock: Blocking
> USB-based Keylogger Attacks", DBSec 2018.
>
> Link: https://lore.kernel.org/linux-input/adSxXidgeWF0-Ewn@beelink/
>
> Signed-off-by: Krish Gulati <krishgulati7@gmail.com>
> Suggested-by: Sashiko-bot <sashiko-bot@kernel.org>
> ---
> Changes since v1:
> - Added bpf_spin_lock/unlock around the Welford compound update in
> kbd_hook() to close a data race on concurrent execution (was
> lockless read-modify-write).
> - Switched dev_details from LRU_HASH to plain HASH so eviction
> under map pressure fails the insert instead of silently
> displacing an existing tracked device.
> - Store report_id in dev_info and gate kbd_hook() on it, so
> composite devices no longer misattribute other report types'
> bytes as keystrokes.
> - Zero dev_info with __builtin_memset before populating in probe()
> to satisfy the verifier's stack-init tracking (was relying on a
> partial designated initializer).
> - Replaced active-byte-count heuristic with bytewise diff/mask
> (standard_boot_keypress/nkro_keypress) so consecutive keystrokes
> without an intervening empty report are still detected.
> - Verified bucket-size rounding (8/16/32/64) does not drop events
> or read out-of-bounds, tested via hid-replay with synthetic
> report sizes (9, 14 bytes, etc.).
>
> Known limitations, not addressed in this version:
> - No cleanup on device disconnect: hid_bpf_ops has no disconnect
> hook, so dev_details entries persist until the map fills.
> Deferred pending maintainer guidance on the right approach.
> - find_keyboard_field() returns on the first Keyboard-typed
> (GenericDesktop/Keyboard) Application Collection found; a device
> with multiple such collections within a single report descriptor
> (as opposed to multiple HID interfaces, which are already handled
> correctly and verified on real composite hardware) will only have
> the first one tracked. Untested; the only hardware available for
> testing exhibits the multiple-interface pattern rather than the
> single-descriptor multi-collection pattern, so this path has not
> been exercised.
>
> Resending as a top-level thread (v2 was sent as a reply to v1 on July 9).
> Original thread: https://lore.kernel.org/linux-input/20260709104958.38303-1-krishgulati7@gmail.com
>
> src/bpf/testing/0010-Generic__keyboard.bpf.c | 311 +++++++++++++------
> src/bpf/testing/meson.build | 1 +
These files are part of udev-hid-bpf, not the kernel. They will
eventually make it to the kernel, but the development happens on
gitlab.freedesktop.org.
Also, that patch makes changes to
"src/bpf/testing/0010-Generic__keyboard.bpf.c" which is not known to any
upstream, so we can't do much.
Cheers,
Benjamin
> 2 files changed, 217 insertions(+), 95 deletions(-)
>
> diff --git a/src/bpf/testing/0010-Generic__keyboard.bpf.c b/src/bpf/testing/0010-Generic__keyboard.bpf.c
> index 9114587..c3f2364 100644
> --- a/src/bpf/testing/0010-Generic__keyboard.bpf.c
> +++ b/src/bpf/testing/0010-Generic__keyboard.bpf.c
> @@ -24,11 +24,11 @@
> #define HID_GUARD_PED_SUSPICIOUS_THRESH (50 * HID_GUARD_NSEC_PER_MSEC)
> #define HID_GUARD_PED_WARNING_THRESH (300 * HID_GUARD_NSEC_PER_MSEC)
>
> -/*not derived from real typing data yet*/
> +/*not derived from real typing raw_buffer yet*/
> #define HID_GUARD_MIN_SAMPLES 5
>
> /*
> - * Variance is "too metronomic
> + * Variance is "too metronomic"
> * to be a human," expressed in ms^2 so we never need sqrt().
> */
> #define HID_GUARD_VARIANCE_THRESH_MS2 (40ULL * 40ULL)
> @@ -57,9 +57,17 @@ enum hid_guard_ped_flag {
> };
>
> struct dev_info {
> - __u64 connection_time;
> + struct bpf_spin_lock lock;
> + __u8 report_id;
> + __u8 field_type;
> + __u8 prev_report[64];
> + __u16 bits_start;
> + __u16 bits_end;
> + __u16 usage_id;
> __u32 report_size;
> - __u64 prev_report[32];
> + __u32 keyboard_offset;
> + __u64 connection_time;
> + __u64 raw_buffer_length;
> /*welford's variables*/
> __u64 prev_keydown_ts;
> __u64 count;
> @@ -71,22 +79,8 @@ struct dev_info {
> */
> };
>
> -/*
> - * BPF_MAP_TYPE_LRU_HASH:
> - * Using an LRU map automatically prevents exhaustion by silently evicting
> - * the oldest idle devices. It requires no syntax changes to the rest of the
> - * code (lookup/update helpers work identically), but introduces behavioral
> - * trade-offs:
> - *
> - * 1. Eviction wipes Welford variance history. An attacker
> - * could theoretically flood the map to flush their device and reset their
> - * score.
> - * 2. PED Blindspot: Eviction deletes the 'connection_time' set during probe().
> - * If an evicted device wakes up, it will bypass post-enumeration delay
> - * checks.
> - */
> struct {
> - __uint(type, BPF_MAP_TYPE_LRU_HASH);
> + __uint(type, BPF_MAP_TYPE_HASH);
> __type(key, __u32);
> __type(value, struct dev_info);
> __uint(max_entries, 128);
> @@ -138,11 +132,29 @@ static __always_inline void welford(struct dev_info *dev_state,
> delta2 = x - dev_state->mean;
>
> dev_state->M2 += (__u64)(delta * delta2);
> +}
>
> - bpf_printk("W[Count:%llu] Int:%llu ms, scaled_x:%lld\n",
> - dev_state->count, interval_ms, x);
> - bpf_printk(" -> delta1:%lld, mean:%lld\n", delta, dev_state->mean);
> - bpf_printk(" -> delta2:%lld, M2:%llu\n", delta2, dev_state->M2);
> +static __always_inline bool standard_boot_keypress(__u8 *current_report,
> + __u8 *prev_report)
> +{
> + for (int i = 2; i < 8; i++) {
> + if (current_report[i] != prev_report[i])
> + return 1;
> + }
> + return 0;
> +}
> +
> +static __always_inline bool
> +nkro_keypress(__u8 *current_report, __u8 *prev_report, __u64 buffer_length)
> +{
> + for (int i = 0; i < 64; i++) {
> + if (i >= buffer_length)
> + break;
> +
> + if (current_report[i] & ~prev_report[i])
> + return 1;
> + }
> + return 0;
> }
>
> HID_BPF_CONFIG(HID_DEVICE(BUS_USB, HID_GROUP_ANY, HID_VID_ANY, HID_PID_ANY),
> @@ -150,7 +162,7 @@ HID_BPF_CONFIG(HID_DEVICE(BUS_USB, HID_GROUP_ANY, HID_VID_ANY, HID_PID_ANY),
> HID_PID_ANY));
>
> SEC(HID_BPF_DEVICE_EVENT)
> -int BPF_PROG(kdb_hook, struct hid_bpf_ctx *hctx)
> +int BPF_PROG(kbd_hook, struct hid_bpf_ctx *hctx)
> {
> __u32 hid_id = hctx->hid->id;
>
> @@ -161,124 +173,233 @@ int BPF_PROG(kdb_hook, struct hid_bpf_ctx *hctx)
> if (!info)
> return 0;
>
> - __u32 size = info->report_size;
> - __u32 fetch_size;
> - __u8 *data;
> + __u8 *raw_buffer;
>
> - if (size <= 8)
> - fetch_size = 8;
> - else if (size <= 16)
> - fetch_size = 16;
> - else
> - fetch_size = 32;
> + __u8 *report_id_data = hid_bpf_get_data(hctx, 0, 1);
>
> - data = hid_bpf_get_data(hctx, 0, fetch_size);
> + if (!report_id_data)
> + return 0;
>
> - if (!data)
> + if (info->report_id != 0 && report_id_data[0] != info->report_id)
> return 0;
> - int now_active_ks = 0, was_active_ks = 0;
> -#pragma unroll
> - for (int i = 0; i < 32; i++) {
> - if (i >= fetch_size)
> - break;
> - now_active_ks += ((__u8)data[i] + 255) >> 8;
> - was_active_ks += ((__u8)info->prev_report[i] + 255) >> 8;
>
> - info->prev_report[i] = data[i];
> + __u32 offset = info->keyboard_offset + (info->report_id != 0 ? 1 : 0);
> +
> + __u64 buffer_length = info->raw_buffer_length;
> +
> + if (buffer_length <= 8)
> + raw_buffer = hid_bpf_get_data(hctx, offset, 8);
> + else if (buffer_length <= 16)
> + raw_buffer = hid_bpf_get_data(hctx, offset, 16);
> + else if (buffer_length <= 32)
> + raw_buffer = hid_bpf_get_data(hctx, offset, 32);
> + else
> + raw_buffer = hid_bpf_get_data(hctx, offset, 64);
> +
> + if (!raw_buffer)
> + return 0;
> +
> + bool new_key_pressed = false;
> +
> + if (info->report_id == 0) {
> + new_key_pressed =
> + standard_boot_keypress(raw_buffer, info->prev_report);
> + } else {
> + new_key_pressed = nkro_keypress(raw_buffer, info->prev_report,
> + buffer_length);
> }
>
> - __u64 current_ms = bpf_ktime_get_ns() / HID_GUARD_NSEC_PER_MSEC;
> + __u64 now = bpf_ktime_get_ns();
>
> - if (now_active_ks > was_active_ks) {
> + __u64 interval_ms = 0;
> + __u64 variance_m2 = 0;
> + bool is_idle_gap = false;
> + bool is_suspicious = false;
> + enum hid_guard_ped_flag ped_flag = HID_GUARD_PED_NO_ENTRY;
> + bool run_ped = false;
> +
> + bpf_spin_lock(&info->lock);
> +
> + if (new_key_pressed) {
> if (info->prev_keydown_ts != 0) {
> - __u64 interval_ms = current_ms - info->prev_keydown_ts;
> + interval_ms = (now - info->prev_keydown_ts) /
> + HID_GUARD_NSEC_PER_MSEC;
>
> - if (interval_ms < HID_GUARD_IDLE_GAP_THRESH_MS) {
> + if (interval_ms < HID_GUARD_IDLE_GAP_THRESH_MS)
> welford(info, interval_ms);
> - } else {
> - bpf_printk(
> - "hid %d: idle gap %llu ms excluded from sample\n",
> - hid_id, interval_ms);
> - }
> + else
> + is_idle_gap = true;
> }
>
> + /*
> + * Variance check runs after welford() so the current
> + * sample is already folded in before we decide.
> + * Guard on MIN_SAMPLES: Welford's unbiased estimator
> + * (M2 / (count - 1)) is undefined for count < 2, and
> + * unreliable until a few samples have accumulated.
> + */
> if (info->count >= HID_GUARD_MIN_SAMPLES) {
> - __u64 variance_m2 =
> + variance_m2 =
> info->M2 /
> ((__u64)HID_GUARD_WELFORD_SCALE *
> HID_GUARD_WELFORD_SCALE * (info->count - 1));
> - /*
> - * the initial interval x was multiplied by HID_GUARD_WELFORD_SCALE, both
> - * delta and delta2 are also scaled by that factor,
> - * thus scale^2 in the denominator
> - */
> - if (variance_m2 < HID_GUARD_VARIANCE_THRESH_MS2) {
> - bpf_printk(
> - "hid %d: Suspeciously regular typing, variance=%llu ms^2\n",
> - hid_id, variance_m2);
> - }
> + if (variance_m2 < HID_GUARD_VARIANCE_THRESH_MS2)
> + is_suspicious = true;
> }
>
> - info->prev_keydown_ts = current_ms;
> + info->prev_keydown_ts = now;
> }
>
> - if (info->connection_time != 0) {
> - enum hid_guard_ped_flag ped_flag =
> - post_enumeration_delay(info, bpf_ktime_get_ns());
> -
> - bpf_printk("PED flag for hid %d: %d\n", hid_id, ped_flag);
> + for (int i = 0; i < 64; i++) {
> + if (i >= buffer_length)
> + break;
> + info->prev_report[i] = raw_buffer[i];
> + }
>
> - /*
> - * Prevent re-evaluation on subsequent packets for this device
> - */
> + /*
> + * PED: fires exactly once per device lifetime. connection_time
> + * is set at probe() time; we clear it here so subsequent events
> + * skip this branch entirely.
> + */
> + if (info->connection_time != 0) {
> + ped_flag = post_enumeration_delay(info, now);
> info->connection_time = 0;
> + run_ped = true;
> }
> +
> + bpf_spin_unlock(&info->lock);
> +
> + if (new_key_pressed) {
> + if (is_idle_gap)
> + bpf_printk(
> + "hid %d: idle gap %llu ms excluded from sample\n",
> + hid_id, interval_ms);
> + if (is_suspicious)
> + bpf_printk(
> + "hid %d: suspiciously regular typing, variance=%llu ms^2\n",
> + hid_id, variance_m2);
> + }
> +
> + if (run_ped)
> + bpf_printk("hid %d: PED flag=%d\n", hid_id, ped_flag);
> +
> return 0;
> }
>
> HID_BPF_OPS(hook_keyboard) = {
> - .hid_device_event = (void *)kdb_hook,
> + .hid_device_event = (void *)kbd_hook,
> };
>
> struct hid_rdesc_descriptor HID_REPORT_DESCRIPTOR;
>
> -SEC("syscall")
> -int probe(struct hid_bpf_probe_args *ctx)
> +static __always_inline bool find_keyboard_field(__u16 *bits_start,
> + __u16 *bits_end,
> + __u8 *report_id,
> + __u32 *size_in_bytes)
> {
> struct hid_rdesc_report *input;
> struct hid_rdesc_field *field;
> struct hid_rdesc_collection *col;
>
> hid_bpf_for_each_input_report(&HID_REPORT_DESCRIPTOR, input) {
> - __u32 size_in_bytes = (input->size_in_bits + 7) / 8;
> -
> - bpf_printk("Report size: %d\n", size_in_bytes);
> - if (input->report_id != 0)
> - size_in_bytes += 1;
> -
> - bpf_printk("Report size after report_id: %d\n", size_in_bytes);
> -
> hid_bpf_for_each_field(input, field) {
> hid_bpf_for_each_collection(field, col) {
> if (col->usage_page ==
> HidUsagePage_GenericDesktop &&
> col->usage_id == HidUsage_GD_Keyboard) {
> - __u32 key = ctx->hid;
> - struct dev_info info = {
> - .connection_time =
> - bpf_ktime_get_ns(),
> - .count = 0,
> - .report_size = size_in_bytes
> - };
> - bpf_map_update_elem(&dev_details, &key,
> - &info, BPF_ANY);
> - ctx->retval = 0;
> - return 0;
> + *size_in_bytes =
> + input->size_in_bits / 8;
> +
> + if (input->report_id != 0)
> + *size_in_bytes += 1;
> +
> + *bits_start = field->bits_start;
> + *bits_end = field->bits_end;
> + *report_id = input->report_id;
> + return true;
> }
> }
> }
> }
> - ctx->retval = -EINVAL;
> + return false;
> +}
> +
> +SEC("syscall")
> +int probe(struct hid_bpf_probe_args *ctx)
> +{
> + __u8 report_id;
> + __u16 bits_start, bits_end;
> + __u32 size_in_bytes;
> + __u32 hid_id = ctx->hid;
> +
> + struct dev_info *existing_info =
> + bpf_map_lookup_elem(&dev_details, &hid_id);
> +
> + struct dev_info info;
> +
> + __builtin_memset(&info, 0, sizeof(info));
> +
> + if (!find_keyboard_field(&bits_start, &bits_end, &report_id,
> + &size_in_bytes)) {
> + ctx->retval = -EINVAL;
> + return 0;
> + }
> +
> + if (existing_info != NULL) {
> + __u64 now = bpf_ktime_get_ns();
> +
> + bpf_spin_lock(&existing_info->lock);
> +
> + existing_info->connection_time = now;
> +
> + existing_info->bits_start = bits_start;
> + existing_info->bits_end = bits_end;
> + existing_info->keyboard_offset = bits_start / 8;
> + existing_info->report_size = size_in_bytes;
> +
> + if (existing_info->keyboard_offset >
> + existing_info->report_size) {
> + bpf_spin_unlock(&existing_info->lock);
> + ctx->retval = -EINVAL;
> + return 0;
> + }
> +
> + existing_info->report_id = report_id;
> + existing_info->raw_buffer_length =
> + existing_info->report_size -
> + (existing_info->keyboard_offset -
> + (report_id != 0 ? 1 : 0));
> +
> + existing_info->count = 0;
> + existing_info->mean = 0;
> + existing_info->M2 = 0;
> + existing_info->prev_keydown_ts = 0;
> +
> + __builtin_memset(existing_info->prev_report, 0,
> + sizeof(existing_info->prev_report));
> +
> + bpf_spin_unlock(&existing_info->lock);
> + ctx->retval = 0;
> + return 0;
> + }
> +
> + info.connection_time = bpf_ktime_get_ns();
> + info.bits_start = bits_start;
> + info.bits_end = bits_end;
> + info.keyboard_offset = bits_start / 8;
> + info.report_size = size_in_bytes;
> +
> + if (info.keyboard_offset > info.report_size) {
> + ctx->retval = -EINVAL;
> + return 0;
> + }
> +
> + info.report_id = report_id;
> + info.raw_buffer_length = info.report_size - (info.keyboard_offset -
> + (report_id != 0 ? 1 : 0));
> +
> + bpf_map_update_elem(&dev_details, &hid_id, &info, BPF_NOEXIST);
> + ctx->retval = 0;
> return 0;
> }
>
> diff --git a/src/bpf/testing/meson.build b/src/bpf/testing/meson.build
> index 9d1b5d3..2586aea 100644
> --- a/src/bpf/testing/meson.build
> +++ b/src/bpf/testing/meson.build
> @@ -11,6 +11,7 @@ tracing_sources = [
> # 'sources' are BPF programs only compatible with
> # struct_ops (kernel v6.11+)
> sources = [
> + '0010-Generic__keyboard.bpf.c',
> ]
>
> foreach bpf: tracing_sources
> --
> 2.55.0
>
>
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCH RFC v2 RESEND] HID: BPF: add keyboard behavioral anomaly detection
2026-07-30 8:12 ` Benjamin Tissoires
@ 2026-07-30 9:57 ` Krish Gulati
0 siblings, 0 replies; 3+ messages in thread
From: Krish Gulati @ 2026-07-30 9:57 UTC (permalink / raw)
To: Benjamin Tissoires; +Cc: jikos, linux-input, linux-kernel
Hi Benjamin,
> These files are part of udev-hid-bpf, not the kernel. They will
> eventually make it to the kernel, but the development happens on
> gitlab.freedesktop.org.
>
> Also, that patch makes changes to
> "src/bpf/testing/0010-Generic__keyboard.bpf.c" which is not known to any
> upstream, so we can't do much.
Thanks for the pointer -- I'll move this over to gitlab.freedesktop.org
and open a merge request against udev-hid-bpf directly.
Regards,
Krish
On Thu, Jul 30, 2026 at 1:42 PM Benjamin Tissoires <bentiss@kernel.org> wrote:
>
> Hi,
>
> On Jul 22 2026, Krish Gulati wrote:
> > This patch implements a HID-BPF struct_ops program that detects automated
> > HID injection attacks. It does this by measuring post-enumeration delay
> > and tracking inter-keystroke timing using Welford's online variance algorithm.
> > State is stored in a hash map keyed by the HID ID.
> >
> > Detection results are currently surfaced via bpf_printk(). A
> > BPF_MAP_TYPE_RINGBUF interface with configurable userspace daemon
> > is planned; deferred pending validation of detection heuristics.
> >
> > Signal design is grounded in: Neuner et al., "USBlock: Blocking
> > USB-based Keylogger Attacks", DBSec 2018.
> >
> > Link: https://lore.kernel.org/linux-input/adSxXidgeWF0-Ewn@beelink/
> >
> > Signed-off-by: Krish Gulati <krishgulati7@gmail.com>
> > Suggested-by: Sashiko-bot <sashiko-bot@kernel.org>
> > ---
> > Changes since v1:
> > - Added bpf_spin_lock/unlock around the Welford compound update in
> > kbd_hook() to close a data race on concurrent execution (was
> > lockless read-modify-write).
> > - Switched dev_details from LRU_HASH to plain HASH so eviction
> > under map pressure fails the insert instead of silently
> > displacing an existing tracked device.
> > - Store report_id in dev_info and gate kbd_hook() on it, so
> > composite devices no longer misattribute other report types'
> > bytes as keystrokes.
> > - Zero dev_info with __builtin_memset before populating in probe()
> > to satisfy the verifier's stack-init tracking (was relying on a
> > partial designated initializer).
> > - Replaced active-byte-count heuristic with bytewise diff/mask
> > (standard_boot_keypress/nkro_keypress) so consecutive keystrokes
> > without an intervening empty report are still detected.
> > - Verified bucket-size rounding (8/16/32/64) does not drop events
> > or read out-of-bounds, tested via hid-replay with synthetic
> > report sizes (9, 14 bytes, etc.).
> >
> > Known limitations, not addressed in this version:
> > - No cleanup on device disconnect: hid_bpf_ops has no disconnect
> > hook, so dev_details entries persist until the map fills.
> > Deferred pending maintainer guidance on the right approach.
> > - find_keyboard_field() returns on the first Keyboard-typed
> > (GenericDesktop/Keyboard) Application Collection found; a device
> > with multiple such collections within a single report descriptor
> > (as opposed to multiple HID interfaces, which are already handled
> > correctly and verified on real composite hardware) will only have
> > the first one tracked. Untested; the only hardware available for
> > testing exhibits the multiple-interface pattern rather than the
> > single-descriptor multi-collection pattern, so this path has not
> > been exercised.
> >
> > Resending as a top-level thread (v2 was sent as a reply to v1 on July 9).
> > Original thread: https://lore.kernel.org/linux-input/20260709104958.38303-1-krishgulati7@gmail.com
> >
> > src/bpf/testing/0010-Generic__keyboard.bpf.c | 311 +++++++++++++------
> > src/bpf/testing/meson.build | 1 +
>
> These files are part of udev-hid-bpf, not the kernel. They will
> eventually make it to the kernel, but the development happens on
> gitlab.freedesktop.org.
>
> Also, that patch makes changes to
> "src/bpf/testing/0010-Generic__keyboard.bpf.c" which is not known to any
> upstream, so we can't do much.
>
> Cheers,
> Benjamin
>
> > 2 files changed, 217 insertions(+), 95 deletions(-)
> >
> > diff --git a/src/bpf/testing/0010-Generic__keyboard.bpf.c b/src/bpf/testing/0010-Generic__keyboard.bpf.c
> > index 9114587..c3f2364 100644
> > --- a/src/bpf/testing/0010-Generic__keyboard.bpf.c
> > +++ b/src/bpf/testing/0010-Generic__keyboard.bpf.c
> > @@ -24,11 +24,11 @@
> > #define HID_GUARD_PED_SUSPICIOUS_THRESH (50 * HID_GUARD_NSEC_PER_MSEC)
> > #define HID_GUARD_PED_WARNING_THRESH (300 * HID_GUARD_NSEC_PER_MSEC)
> >
> > -/*not derived from real typing data yet*/
> > +/*not derived from real typing raw_buffer yet*/
> > #define HID_GUARD_MIN_SAMPLES 5
> >
> > /*
> > - * Variance is "too metronomic
> > + * Variance is "too metronomic"
> > * to be a human," expressed in ms^2 so we never need sqrt().
> > */
> > #define HID_GUARD_VARIANCE_THRESH_MS2 (40ULL * 40ULL)
> > @@ -57,9 +57,17 @@ enum hid_guard_ped_flag {
> > };
> >
> > struct dev_info {
> > - __u64 connection_time;
> > + struct bpf_spin_lock lock;
> > + __u8 report_id;
> > + __u8 field_type;
> > + __u8 prev_report[64];
> > + __u16 bits_start;
> > + __u16 bits_end;
> > + __u16 usage_id;
> > __u32 report_size;
> > - __u64 prev_report[32];
> > + __u32 keyboard_offset;
> > + __u64 connection_time;
> > + __u64 raw_buffer_length;
> > /*welford's variables*/
> > __u64 prev_keydown_ts;
> > __u64 count;
> > @@ -71,22 +79,8 @@ struct dev_info {
> > */
> > };
> >
> > -/*
> > - * BPF_MAP_TYPE_LRU_HASH:
> > - * Using an LRU map automatically prevents exhaustion by silently evicting
> > - * the oldest idle devices. It requires no syntax changes to the rest of the
> > - * code (lookup/update helpers work identically), but introduces behavioral
> > - * trade-offs:
> > - *
> > - * 1. Eviction wipes Welford variance history. An attacker
> > - * could theoretically flood the map to flush their device and reset their
> > - * score.
> > - * 2. PED Blindspot: Eviction deletes the 'connection_time' set during probe().
> > - * If an evicted device wakes up, it will bypass post-enumeration delay
> > - * checks.
> > - */
> > struct {
> > - __uint(type, BPF_MAP_TYPE_LRU_HASH);
> > + __uint(type, BPF_MAP_TYPE_HASH);
> > __type(key, __u32);
> > __type(value, struct dev_info);
> > __uint(max_entries, 128);
> > @@ -138,11 +132,29 @@ static __always_inline void welford(struct dev_info *dev_state,
> > delta2 = x - dev_state->mean;
> >
> > dev_state->M2 += (__u64)(delta * delta2);
> > +}
> >
> > - bpf_printk("W[Count:%llu] Int:%llu ms, scaled_x:%lld\n",
> > - dev_state->count, interval_ms, x);
> > - bpf_printk(" -> delta1:%lld, mean:%lld\n", delta, dev_state->mean);
> > - bpf_printk(" -> delta2:%lld, M2:%llu\n", delta2, dev_state->M2);
> > +static __always_inline bool standard_boot_keypress(__u8 *current_report,
> > + __u8 *prev_report)
> > +{
> > + for (int i = 2; i < 8; i++) {
> > + if (current_report[i] != prev_report[i])
> > + return 1;
> > + }
> > + return 0;
> > +}
> > +
> > +static __always_inline bool
> > +nkro_keypress(__u8 *current_report, __u8 *prev_report, __u64 buffer_length)
> > +{
> > + for (int i = 0; i < 64; i++) {
> > + if (i >= buffer_length)
> > + break;
> > +
> > + if (current_report[i] & ~prev_report[i])
> > + return 1;
> > + }
> > + return 0;
> > }
> >
> > HID_BPF_CONFIG(HID_DEVICE(BUS_USB, HID_GROUP_ANY, HID_VID_ANY, HID_PID_ANY),
> > @@ -150,7 +162,7 @@ HID_BPF_CONFIG(HID_DEVICE(BUS_USB, HID_GROUP_ANY, HID_VID_ANY, HID_PID_ANY),
> > HID_PID_ANY));
> >
> > SEC(HID_BPF_DEVICE_EVENT)
> > -int BPF_PROG(kdb_hook, struct hid_bpf_ctx *hctx)
> > +int BPF_PROG(kbd_hook, struct hid_bpf_ctx *hctx)
> > {
> > __u32 hid_id = hctx->hid->id;
> >
> > @@ -161,124 +173,233 @@ int BPF_PROG(kdb_hook, struct hid_bpf_ctx *hctx)
> > if (!info)
> > return 0;
> >
> > - __u32 size = info->report_size;
> > - __u32 fetch_size;
> > - __u8 *data;
> > + __u8 *raw_buffer;
> >
> > - if (size <= 8)
> > - fetch_size = 8;
> > - else if (size <= 16)
> > - fetch_size = 16;
> > - else
> > - fetch_size = 32;
> > + __u8 *report_id_data = hid_bpf_get_data(hctx, 0, 1);
> >
> > - data = hid_bpf_get_data(hctx, 0, fetch_size);
> > + if (!report_id_data)
> > + return 0;
> >
> > - if (!data)
> > + if (info->report_id != 0 && report_id_data[0] != info->report_id)
> > return 0;
> > - int now_active_ks = 0, was_active_ks = 0;
> > -#pragma unroll
> > - for (int i = 0; i < 32; i++) {
> > - if (i >= fetch_size)
> > - break;
> > - now_active_ks += ((__u8)data[i] + 255) >> 8;
> > - was_active_ks += ((__u8)info->prev_report[i] + 255) >> 8;
> >
> > - info->prev_report[i] = data[i];
> > + __u32 offset = info->keyboard_offset + (info->report_id != 0 ? 1 : 0);
> > +
> > + __u64 buffer_length = info->raw_buffer_length;
> > +
> > + if (buffer_length <= 8)
> > + raw_buffer = hid_bpf_get_data(hctx, offset, 8);
> > + else if (buffer_length <= 16)
> > + raw_buffer = hid_bpf_get_data(hctx, offset, 16);
> > + else if (buffer_length <= 32)
> > + raw_buffer = hid_bpf_get_data(hctx, offset, 32);
> > + else
> > + raw_buffer = hid_bpf_get_data(hctx, offset, 64);
> > +
> > + if (!raw_buffer)
> > + return 0;
> > +
> > + bool new_key_pressed = false;
> > +
> > + if (info->report_id == 0) {
> > + new_key_pressed =
> > + standard_boot_keypress(raw_buffer, info->prev_report);
> > + } else {
> > + new_key_pressed = nkro_keypress(raw_buffer, info->prev_report,
> > + buffer_length);
> > }
> >
> > - __u64 current_ms = bpf_ktime_get_ns() / HID_GUARD_NSEC_PER_MSEC;
> > + __u64 now = bpf_ktime_get_ns();
> >
> > - if (now_active_ks > was_active_ks) {
> > + __u64 interval_ms = 0;
> > + __u64 variance_m2 = 0;
> > + bool is_idle_gap = false;
> > + bool is_suspicious = false;
> > + enum hid_guard_ped_flag ped_flag = HID_GUARD_PED_NO_ENTRY;
> > + bool run_ped = false;
> > +
> > + bpf_spin_lock(&info->lock);
> > +
> > + if (new_key_pressed) {
> > if (info->prev_keydown_ts != 0) {
> > - __u64 interval_ms = current_ms - info->prev_keydown_ts;
> > + interval_ms = (now - info->prev_keydown_ts) /
> > + HID_GUARD_NSEC_PER_MSEC;
> >
> > - if (interval_ms < HID_GUARD_IDLE_GAP_THRESH_MS) {
> > + if (interval_ms < HID_GUARD_IDLE_GAP_THRESH_MS)
> > welford(info, interval_ms);
> > - } else {
> > - bpf_printk(
> > - "hid %d: idle gap %llu ms excluded from sample\n",
> > - hid_id, interval_ms);
> > - }
> > + else
> > + is_idle_gap = true;
> > }
> >
> > + /*
> > + * Variance check runs after welford() so the current
> > + * sample is already folded in before we decide.
> > + * Guard on MIN_SAMPLES: Welford's unbiased estimator
> > + * (M2 / (count - 1)) is undefined for count < 2, and
> > + * unreliable until a few samples have accumulated.
> > + */
> > if (info->count >= HID_GUARD_MIN_SAMPLES) {
> > - __u64 variance_m2 =
> > + variance_m2 =
> > info->M2 /
> > ((__u64)HID_GUARD_WELFORD_SCALE *
> > HID_GUARD_WELFORD_SCALE * (info->count - 1));
> > - /*
> > - * the initial interval x was multiplied by HID_GUARD_WELFORD_SCALE, both
> > - * delta and delta2 are also scaled by that factor,
> > - * thus scale^2 in the denominator
> > - */
> > - if (variance_m2 < HID_GUARD_VARIANCE_THRESH_MS2) {
> > - bpf_printk(
> > - "hid %d: Suspeciously regular typing, variance=%llu ms^2\n",
> > - hid_id, variance_m2);
> > - }
> > + if (variance_m2 < HID_GUARD_VARIANCE_THRESH_MS2)
> > + is_suspicious = true;
> > }
> >
> > - info->prev_keydown_ts = current_ms;
> > + info->prev_keydown_ts = now;
> > }
> >
> > - if (info->connection_time != 0) {
> > - enum hid_guard_ped_flag ped_flag =
> > - post_enumeration_delay(info, bpf_ktime_get_ns());
> > -
> > - bpf_printk("PED flag for hid %d: %d\n", hid_id, ped_flag);
> > + for (int i = 0; i < 64; i++) {
> > + if (i >= buffer_length)
> > + break;
> > + info->prev_report[i] = raw_buffer[i];
> > + }
> >
> > - /*
> > - * Prevent re-evaluation on subsequent packets for this device
> > - */
> > + /*
> > + * PED: fires exactly once per device lifetime. connection_time
> > + * is set at probe() time; we clear it here so subsequent events
> > + * skip this branch entirely.
> > + */
> > + if (info->connection_time != 0) {
> > + ped_flag = post_enumeration_delay(info, now);
> > info->connection_time = 0;
> > + run_ped = true;
> > }
> > +
> > + bpf_spin_unlock(&info->lock);
> > +
> > + if (new_key_pressed) {
> > + if (is_idle_gap)
> > + bpf_printk(
> > + "hid %d: idle gap %llu ms excluded from sample\n",
> > + hid_id, interval_ms);
> > + if (is_suspicious)
> > + bpf_printk(
> > + "hid %d: suspiciously regular typing, variance=%llu ms^2\n",
> > + hid_id, variance_m2);
> > + }
> > +
> > + if (run_ped)
> > + bpf_printk("hid %d: PED flag=%d\n", hid_id, ped_flag);
> > +
> > return 0;
> > }
> >
> > HID_BPF_OPS(hook_keyboard) = {
> > - .hid_device_event = (void *)kdb_hook,
> > + .hid_device_event = (void *)kbd_hook,
> > };
> >
> > struct hid_rdesc_descriptor HID_REPORT_DESCRIPTOR;
> >
> > -SEC("syscall")
> > -int probe(struct hid_bpf_probe_args *ctx)
> > +static __always_inline bool find_keyboard_field(__u16 *bits_start,
> > + __u16 *bits_end,
> > + __u8 *report_id,
> > + __u32 *size_in_bytes)
> > {
> > struct hid_rdesc_report *input;
> > struct hid_rdesc_field *field;
> > struct hid_rdesc_collection *col;
> >
> > hid_bpf_for_each_input_report(&HID_REPORT_DESCRIPTOR, input) {
> > - __u32 size_in_bytes = (input->size_in_bits + 7) / 8;
> > -
> > - bpf_printk("Report size: %d\n", size_in_bytes);
> > - if (input->report_id != 0)
> > - size_in_bytes += 1;
> > -
> > - bpf_printk("Report size after report_id: %d\n", size_in_bytes);
> > -
> > hid_bpf_for_each_field(input, field) {
> > hid_bpf_for_each_collection(field, col) {
> > if (col->usage_page ==
> > HidUsagePage_GenericDesktop &&
> > col->usage_id == HidUsage_GD_Keyboard) {
> > - __u32 key = ctx->hid;
> > - struct dev_info info = {
> > - .connection_time =
> > - bpf_ktime_get_ns(),
> > - .count = 0,
> > - .report_size = size_in_bytes
> > - };
> > - bpf_map_update_elem(&dev_details, &key,
> > - &info, BPF_ANY);
> > - ctx->retval = 0;
> > - return 0;
> > + *size_in_bytes =
> > + input->size_in_bits / 8;
> > +
> > + if (input->report_id != 0)
> > + *size_in_bytes += 1;
> > +
> > + *bits_start = field->bits_start;
> > + *bits_end = field->bits_end;
> > + *report_id = input->report_id;
> > + return true;
> > }
> > }
> > }
> > }
> > - ctx->retval = -EINVAL;
> > + return false;
> > +}
> > +
> > +SEC("syscall")
> > +int probe(struct hid_bpf_probe_args *ctx)
> > +{
> > + __u8 report_id;
> > + __u16 bits_start, bits_end;
> > + __u32 size_in_bytes;
> > + __u32 hid_id = ctx->hid;
> > +
> > + struct dev_info *existing_info =
> > + bpf_map_lookup_elem(&dev_details, &hid_id);
> > +
> > + struct dev_info info;
> > +
> > + __builtin_memset(&info, 0, sizeof(info));
> > +
> > + if (!find_keyboard_field(&bits_start, &bits_end, &report_id,
> > + &size_in_bytes)) {
> > + ctx->retval = -EINVAL;
> > + return 0;
> > + }
> > +
> > + if (existing_info != NULL) {
> > + __u64 now = bpf_ktime_get_ns();
> > +
> > + bpf_spin_lock(&existing_info->lock);
> > +
> > + existing_info->connection_time = now;
> > +
> > + existing_info->bits_start = bits_start;
> > + existing_info->bits_end = bits_end;
> > + existing_info->keyboard_offset = bits_start / 8;
> > + existing_info->report_size = size_in_bytes;
> > +
> > + if (existing_info->keyboard_offset >
> > + existing_info->report_size) {
> > + bpf_spin_unlock(&existing_info->lock);
> > + ctx->retval = -EINVAL;
> > + return 0;
> > + }
> > +
> > + existing_info->report_id = report_id;
> > + existing_info->raw_buffer_length =
> > + existing_info->report_size -
> > + (existing_info->keyboard_offset -
> > + (report_id != 0 ? 1 : 0));
> > +
> > + existing_info->count = 0;
> > + existing_info->mean = 0;
> > + existing_info->M2 = 0;
> > + existing_info->prev_keydown_ts = 0;
> > +
> > + __builtin_memset(existing_info->prev_report, 0,
> > + sizeof(existing_info->prev_report));
> > +
> > + bpf_spin_unlock(&existing_info->lock);
> > + ctx->retval = 0;
> > + return 0;
> > + }
> > +
> > + info.connection_time = bpf_ktime_get_ns();
> > + info.bits_start = bits_start;
> > + info.bits_end = bits_end;
> > + info.keyboard_offset = bits_start / 8;
> > + info.report_size = size_in_bytes;
> > +
> > + if (info.keyboard_offset > info.report_size) {
> > + ctx->retval = -EINVAL;
> > + return 0;
> > + }
> > +
> > + info.report_id = report_id;
> > + info.raw_buffer_length = info.report_size - (info.keyboard_offset -
> > + (report_id != 0 ? 1 : 0));
> > +
> > + bpf_map_update_elem(&dev_details, &hid_id, &info, BPF_NOEXIST);
> > + ctx->retval = 0;
> > return 0;
> > }
> >
> > diff --git a/src/bpf/testing/meson.build b/src/bpf/testing/meson.build
> > index 9d1b5d3..2586aea 100644
> > --- a/src/bpf/testing/meson.build
> > +++ b/src/bpf/testing/meson.build
> > @@ -11,6 +11,7 @@ tracing_sources = [
> > # 'sources' are BPF programs only compatible with
> > # struct_ops (kernel v6.11+)
> > sources = [
> > + '0010-Generic__keyboard.bpf.c',
> > ]
> >
> > foreach bpf: tracing_sources
> > --
> > 2.55.0
> >
> >
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-07-30 9:57 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-22 11:48 [PATCH RFC v2 RESEND] HID: BPF: add keyboard behavioral anomaly detection Krish Gulati
2026-07-30 8:12 ` Benjamin Tissoires
2026-07-30 9:57 ` Krish Gulati
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox