Re: Serious bug in HID core

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Tue, Jul 08, 2025 at 05:51:08PM +0200, Benjamin Tissoires wrote:
> Hi Alan,
> 
> On Jul 08 2025, Alan Stern wrote:
> > Jiri and Benjamin:
> > 
> > Investigating a recent bug report from syzbot 
> > (https://lore.kernel.org/linux-usb/686be237.a70a0220.29fe6c.0b0c.GAE@xxxxxxxxxx/)
> > led me to a rather serious error in the HID core.  It could affect a 
> > lot of drivers, and I don't know enough about them or the HID subsystem 
> > to fix it right away.
> > 
> > In short, does the value returned by hid_report_len() count the byte 
> > reserved for the report ID number?
> 
> It depends :)
> 
> See your analysis below...
> 
> > 
> > Some drivers seem to assume that it does and some seem to assume that it 
> > doesn't.  Here's what the actual code from include/linux/hid.h does:
> > 
> > /**
> >  * hid_report_len - calculate the report length
> >  *
> >  * @report: the report we want to know the length
> >  */
> > static inline u32 hid_report_len(struct hid_report *report)
> > {
> > 	return DIV_ROUND_UP(report->size, 8) + (report->id > 0);
> > }
> > 
> > It's somewhere in between -- it includes the ID byte in the count if and 
> > only if the ID is nonzero!  And of course, this behavior isn't mentioned 
> > in the (ungrammatical) kerneldoc.
> 
> Yeah, the docs are bad. But your analysis is correct: for a given
> report, its size might include or not the report ID.
> 
> This is because the report ID is optional in the HID spec, and some
> devices don't make use of it. Those devices are only exporting one
> report type, and can not have another report type in the same HID
> device. If a device needs to have more than one, it then needs to export
> the report ID in all of its reports, and then report_len count that
> extra byte.
> 
> To give you an example, old mice (or inexpensive ones), would export a
> report with:
> - X, Y, Wheel, B0, B1, B2
> 
> When a more advanced one might export:
> - REPORT ID 0, X, Y, Wheel, B0, B1, B2, ... B9
> - REPORT ID 1, SOME_VERY_COMPLEX_CONFIGURATION, ANOTHER ONE, ETC...
> 
> However, a HID device making use of the non report ID version can never
> make use of a report ID (or it's custom proprietary protocol).

So you're saying that while the kerneldoc for hid_report_len() could be 
improved, the code itself is correct.  Fine.  But this merely means that 
the real problem lies elsewhere (and _not_ in syzbot's test scenario!).  
See below.

Also, are you sure that all the other HID drivers realize that 
hid_report_len() behaves this way?  It's an easy thing to get wrong.

> > The particular scenario causing the bug found by syzbot was this: 
> > report->size was 0, report->id was 0, and the lower-level driver (usbhid 
> > in this case) assumed that the length argument (which was 0) did include 
> > the ID byte.  Since the ID was 0, the driver skipped over the first byte 
> > of the buffer and decremented the length, causing the length to 
> > underflow and leading to an invalid memory access.  In a more realistic 
> > setting, this would merely result in data loss and leakage.
> 
> That scenario is bogus (like most of syzbots in the HID space). There
> should be no way the HID subsystem allows a report->size of 0 to be used
> in a set_report or a get_report. So that's where the bug lies: HID
> trusts too much the device, and can lead to that kind of issues.
> 
> > 
> > How should this be fixed?
> 
> We have 2 problems here:
> - and __hid_request or __hid_raw_request should reject a null size
> 	report, and in the __hid_request case, a report of size 1 if there is
> 	a report ID.
> - Why is hidinput_change_resolution_multipliers() even considfering this
> 	report of size 0 when it should at least ensure that there is one
> 	field within the report
> 
> The first one should be easy to fix: add a bunch of checks.

I would normally expect this to checked when the report descriptors are 
read and parsed during probing.  A report of length zero could be 
rejected then (perhaps with an error message in the kernel log).

However, that wouldn't fully fix the problem.  And besides, length-0 
reports are (or should be) harmless.

> __hid_hw_raw_request() seems to not expose the problem actually.
> 
> The second one would need a little bit more understanding of the fake
> report descriptor provided by syzbot.

I suppose we can get the information from syzbot if it's really 
necessary.  But it seems to be a minor point.

> > Related issue: When the lower-level driver's raw_request() routine is 
> > called, can it assume that the first byte of the output buffer always 
> > contains the report ID number, set by the HID core?  If not, should it 
> > assume that the first byte is always reserved for the ID, or that the 
> > first byte is reserved only when the ID is nonzero?
> 
> The first byte should always be reserved to the report ID, and is
> populated by 0 by hid-core when the report ID is not in use.

Then why does hid_output_report() do this:

	if (report->id > 0)
		*data++ = report->id;

?  The first byte is not reserved for the ID when the ID is 0.  
According to what you said, the assignment should be unconditional.  
Isn't that a genuine bug?

And shouldn't the length computed by hid_alloc_report_buf() be one 
larger than it is when the ID is 0?

> So the usbhid driver is doing the right thing. In case there is no
> report ID, it ignores the first byte when making a request to the
> device, because in my example above, the device would not expect the
> first byte to be 0.

When usbhid skips the first byte, it also decrements the length.  Since 
__hid_request() calls it with a length given by hid_report_len(), the 
length has _already_ been decremented (or rather, not incremented -- it 
doesn't account for the ID byte), and consequently the USB transfer will 
leave out the final byte of the report.  This is the data loss I 
mentioned earlier.  It's another bug.

So which is doing the wrong thing: usbhid or __hid_request()?

> > Do __hid_request() and __hid_hw_raw_request() behave the same way in 
> > this regard?
> 
> __hid_hw_raw_request() ensures that there is a minimum length of one, so
> we should be fine. __hid_request apparently is the only one giving too
> much trust in the report.

In principle there's nothing wrong with a zero-length report.  It would 
be pretty useless, but nevertheless we should be able to handle one 
without crashing the kernel.

Alan Stern




[Index of Archives]     [Linux Media Devel]     [Linux USB Devel]     [Video for Linux]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]     [Linux Wireless Networking]     [Linux Omap]

  Powered by Linux