* [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; as well as URLs for NNTP newsgroup(s).