Skip to contentSkip to conversation

Input: evdev - sanitize event type index when fetching event masks

PATCH In review Dmitry Torokhov 4 participants 5 messages Aug 4 – Aug 5

Subsystems (2)
lore
Share & tools
lore

Dmitry Torokhov

author maintainer
#0 lore raw

PATCH 1/1 Input: evdev - sanitize event type index when fetching event masks

The user-supplied event type index passed to EVIOCGMASK / EVIOCSMASK ioctls is used to index the static counts array in evdev_get_mask_cnt() and client evmasks array in evdev_get_mask().

While the event type is architecturally bounded by EV_CNT, speculative execution may mispredict bounds checks and perform out-of-bounds loads.

Sanitize the event type index in evdev_get_mask_cnt() branchlessly using array_index_mask_nospec(). This clamps the index to 0 for safe array access and forces the returned count to 0 speculatively when the index is out of bounds.

We do not need additional array_index_nospec() calls in evdev_get_mask() because evdev_get_mask_cnt() speculatively forces the count (and resulting xfer_size) to 0 for out-of-bounds types, preventing any speculative memory access to client evmasks array.

Assisted-by Antigravity:gemini-3.6-flash
---
 drivers/input/evdev.c | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)
drivers/input/evdev.c +4 -1
index 5764c98b4f1f..796ac7ac2b8c 100644
@@ -21,6 +21,7 @@
 #include <linux/init.h>
 #include <linux/input/mt.h>
 #include <linux/major.h>
+#include <linux/nospec.h>
 #include <linux/device.h>
 #include <linux/cdev.h>
 #include "input-compat.h"
@@ -67,8 +68,10 @@ static size_t evdev_get_mask_cnt(unsigned int type)
 		[EV_SND]	= SND_CNT,
 		[EV_FF]		= FF_CNT,
 	};
+	unsigned long mask = array_index_mask_nospec(type, EV_CNT);
 
-	return (type < EV_CNT) ? counts[type] : 0;
+	/* Returns 0 for out-of-bounds types, including speculatively */
+	return counts[type & mask] & mask;
 }
 
 /* requires the buffer lock to be held */
Signature · 5 lines
2.55.0.629.g250fe7f194-goog


-- 
Dmitry
#1 lore raw
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:

Pre-existing issues:
- [High] Incomplete Spectre v1 mitigation due to misunderstanding of speculative execution on branches. --

--- Patch [1]: [PATCH] Input: evdev - sanitize event type index when fetching event masks --- commit e9eb78cd8dbba1d32ef31f1eec9a5c00e54b5aad
Author: Dmitry Torokhov <[email protected]>

Input: evdev - sanitize event type index when fetching event masks

This commit uses array_index_mask_nospec() in evdev_get_mask_cnt() to clamp out-of-bounds event types to 0. It asserts that this is sufficient to prevent speculative out-of-bounds memory accesses in functions like evdev_get_mask() because the returned count is speculatively forced to 0.

Quoted 4 lines

We do not need additional array_index_nospec() calls in evdev_get_mask() because evdev_get_mask_cnt() speculatively forces the count (and resulting xfer_size) to 0 for out-of-bounds types, preventing any speculative memory access to client evmasks array.

[Severity: High]
This is a pre-existing issue, but does relying on the returned count being 0 fully prevent speculative out-of-bounds accesses in callers like evdev_get_mask() and evdev_set_mask()?

When CPU branch predictors mispredict the conditions checking the count, they do not wait for the condition to evaluate.

For example, if the if (cnt > 0) branch in evdev_get_mask() is mispredicted:

drivers/input/evdev.c:evdev_get_mask() {
    ...
    cnt = evdev_get_mask_cnt(type);
    ...
    if (cnt > 0) {
        mask = client->evmasks[type];
    ...
}

The branch predictor will speculatively execute the block using the original, unclamped type variable, performing an out-of-bounds read on client->evmasks[type]. The retrieved pointer is then accessed by bits_to_user().

A similar pattern exists in evdev_set_mask(), where a mispredicted if (!cnt) branch can lead to using the unclamped type:

drivers/input/evdev.c:evdev_set_mask() {
    ...
    cnt = evdev_get_mask_cnt(type);
    if (!cnt)
        return 0;
    ...
    oldmask = client->evmasks[type];
    ...
    bitmap_free(oldmask);
}

The speculative read of oldmask is followed by a dereference in bitmap_free(), which creates an exploitable side-channel gadget.

This pattern also appears in __evdev_is_filtered(). A mispredicted bounds check can lead to speculative out-of-bounds accesses for test_bit(type, mask) or mask = client->evmasks[type].

Do we still need to explicitly use array_index_nospec(type, EV_CNT) inside these caller functions to prevent the unclamped type from being used during speculative execution?

Quoted 4 lines
drivers/input/evdev.c +0 -0
index c7325226cb866..32804d6965340 100644

[ ... ]

Quoted 12 lines
drivers/input/evdev.c +3 -1
@@ -67,8 +68,10 @@ static size_t evdev_get_mask_cnt(unsigned int type)
 		[EV_SND]	= SND_CNT,
 		[EV_FF]		= FF_CNT,
 	};
+	unsigned long mask = array_index_mask_nospec(type, EV_CNT);
 
-	return (type < EV_CNT) ? counts[type] : 0;
+	/* Returns 0 for out-of-bounds types, including speculatively */
+	return counts[type & mask] & mask;
 }
 
 /* requires the buffer lock to be held */
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
author maintainer
#2 lore raw
Quoted 41 lines · On Tue, Aug 04, 2026 at 01:54:13AM +0000, [email protected] wrote:

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:

Pre-existing issues:
- [High] Incomplete Spectre v1 mitigation due to misunderstanding of speculative execution on branches. --

--- Patch [1]: [PATCH] Input: evdev - sanitize event type index when fetching event masks --- commit e9eb78cd8dbba1d32ef31f1eec9a5c00e54b5aad
Author: Dmitry Torokhov <[email protected]>

Input: evdev - sanitize event type index when fetching event masks

This commit uses array_index_mask_nospec() in evdev_get_mask_cnt() to clamp out-of-bounds event types to 0. It asserts that this is sufficient to prevent speculative out-of-bounds memory accesses in functions like evdev_get_mask() because the returned count is speculatively forced to 0.

Quoted 4 lines

We do not need additional array_index_nospec() calls in evdev_get_mask() because evdev_get_mask_cnt() speculatively forces the count (and resulting xfer_size) to 0 for out-of-bounds types, preventing any speculative memory access to client evmasks array.

[Severity: High]
This is a pre-existing issue, but does relying on the returned count being 0 fully prevent speculative out-of-bounds accesses in callers like evdev_get_mask() and evdev_set_mask()?

When CPU branch predictors mispredict the conditions checking the count, they do not wait for the condition to evaluate.

For example, if the if (cnt > 0) branch in evdev_get_mask() is mispredicted:

drivers/input/evdev.c:evdev_get_mask() {
    ...
    cnt = evdev_get_mask_cnt(type);
    ...
    if (cnt > 0) {
        mask = client->evmasks[type];
    ...
}
Quoted 4 lines

The branch predictor will speculatively execute the block using the original, unclamped type variable, performing an out-of-bounds read on client->evmasks[type]. The retrieved pointer is then accessed by bits_to_user().

No it will not because xfer_size will be 0 and bits_to_user (copy_to_user) will not dereference the pointer at all.

Quoted 15 lines

A similar pattern exists in evdev_set_mask(), where a mispredicted if (!cnt) branch can lead to using the unclamped type:

drivers/input/evdev.c:evdev_set_mask() {
    ...
    cnt = evdev_get_mask_cnt(type);
    if (!cnt)
        return 0;
    ...
    oldmask = client->evmasks[type];
    ...
    bitmap_free(oldmask);
}
Quoted 2 lines

The speculative read of oldmask is followed by a dereference in bitmap_free(), which creates an exploitable side-channel gadget.

kfree() can not serve as a side-channel gadget. Additionally there are speculation barriers in copy_from_user() and when taking the spinlock.

Quoted 4 lines

This pattern also appears in __evdev_is_filtered(). A mispredicted bounds check can lead to speculative out-of-bounds accesses for test_bit(type, mask) or mask = client->evmasks[type].

This is internal kernel function and is not user-controlled.

Quoted 4 lines

Do we still need to explicitly use array_index_nospec(type, EV_CNT) inside these caller functions to prevent the unclamped type from being used during speculative execution?

So no.

Thanks.

Signature · 1 line
Dmitry
Acked-by on v1 1/1
#3 lore raw
In reply to Dmitry Torokhov
Quoted 55 lines · Dmitry Torokhov

The user-supplied event type index passed to EVIOCGMASK / EVIOCSMASK ioctls is used to index the static counts array in evdev_get_mask_cnt() and client evmasks array in evdev_get_mask().

While the event type is architecturally bounded by EV_CNT, speculative execution may mispredict bounds checks and perform out-of-bounds loads.

Sanitize the event type index in evdev_get_mask_cnt() branchlessly using array_index_mask_nospec(). This clamps the index to 0 for safe array access and forces the returned count to 0 speculatively when the index is out of bounds.

We do not need additional array_index_nospec() calls in evdev_get_mask() because evdev_get_mask_cnt() speculatively forces the count (and resulting xfer_size) to 0 for out-of-bounds types, preventing any speculative memory access to client evmasks array.

Assisted-by Antigravity:gemini-3.6-flash
---
 drivers/input/evdev.c | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)
drivers/input/evdev.c +4 -1
index 5764c98b4f1f..796ac7ac2b8c 100644
@@ -21,6 +21,7 @@
 #include <linux/init.h>
 #include <linux/input/mt.h>
 #include <linux/major.h>
+#include <linux/nospec.h>
 #include <linux/device.h>
 #include <linux/cdev.h>
 #include "input-compat.h"
@@ -67,8 +68,10 @@ static size_t evdev_get_mask_cnt(unsigned int type)
 		[EV_SND]	= SND_CNT,
 		[EV_FF]		= FF_CNT,
 	};
+	unsigned long mask = array_index_mask_nospec(type, EV_CNT);
 
-	return (type < EV_CNT) ? counts[type] : 0;
+	/* Returns 0 for out-of-bounds types, including speculatively */
+	return counts[type & mask] & mask;
 }
 
 /* requires the buffer lock to be held */
Signature · 5 lines
2.55.0.629.g250fe7f194-goog


-- 
Dmitry
#4 lore raw
Quoted 54 lines · Greg KH
Quoted 53 lines · Dmitry Torokhov

The user-supplied event type index passed to EVIOCGMASK / EVIOCSMASK ioctls is used to index the static counts array in evdev_get_mask_cnt() and client evmasks array in evdev_get_mask().

While the event type is architecturally bounded by EV_CNT, speculative execution may mispredict bounds checks and perform out-of-bounds loads.

Sanitize the event type index in evdev_get_mask_cnt() branchlessly using array_index_mask_nospec(). This clamps the index to 0 for safe array access and forces the returned count to 0 speculatively when the index is out of bounds.

We do not need additional array_index_nospec() calls in evdev_get_mask() because evdev_get_mask_cnt() speculatively forces the count (and resulting xfer_size) to 0 for out-of-bounds types, preventing any speculative memory access to client evmasks array.

Assisted-by Antigravity:gemini-3.6-flash
---
  drivers/input/evdev.c | 5 ++++-
  1 file changed, 4 insertions(+), 1 deletion(-)
drivers/input/evdev.c +4 -1
index 5764c98b4f1f..796ac7ac2b8c 100644
@@ -21,6 +21,7 @@
  #include <linux/init.h>
  #include <linux/input/mt.h>
  #include <linux/major.h>
+#include <linux/nospec.h>
  #include <linux/device.h>
  #include <linux/cdev.h>
  #include "input-compat.h"
@@ -67,8 +68,10 @@ static size_t evdev_get_mask_cnt(unsigned int type)
  		[EV_SND]	= SND_CNT,
  		[EV_FF]		= FF_CNT,
  	};
+	unsigned long mask = array_index_mask_nospec(type, EV_CNT);
 
-	return (type < EV_CNT) ? counts[type] : 0;
+	/* Returns 0 for out-of-bounds types, including speculatively */
+	return counts[type & mask] & mask;
  }
 
  /* requires the buffer lock to be held */
Signature · 1 line
2.55.0.629.g250fe7f194-goog
Quoted 4 lines
Quoted 2 lines
Signature · 1 line
Dmitry

Thanks Dmitry and Greg, I appreciate the quick fix and the attribution!