Linux USB
 help / color / mirror / Atom feed
* [PATCH] net: usb: asix: fix off-by-one in rx_fixup bounds check
@ 2026-09-07  7:52 Aamir Ahmed
  2026-09-08  3:35 ` Aamir Ahmed
  0 siblings, 1 reply; 3+ messages in thread
From: Aamir Ahmed @ 2026-09-07  7:52 UTC (permalink / raw)
  To: Andrew Lunn, David S . Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni
  Cc: Peter Korsgaard, Dean Jenkins, John Stultz, linux-usb, netdev,
	stable, Aamir Ahmed

The sync check in asix_rx_fixup_internal() validates:

  rx->remaining + sizeof(u32) <= skb->len

then reads at:

  offset = ((rx->remaining + 1) & 0xfffe);
  rx->header = get_unaligned_le32(skb->data + offset);

When rx->remaining is odd, the alignment rounding makes offset larger
than rx->remaining. For example with rx->remaining=3 and skb->len=7:
check passes (3+4<=7) but offset becomes ((3+1)&0xfffe)=4, and the
4-byte read at offset 4 needs 8 bytes total, causing a 1-byte
heap OOB read.

Fix the bounds check to use the actual aligned offset that will be
used for the read.

Fixes: 3f30b158eba5 ("asix: On RX avoid creating bad Ethernet frames")
Cc: stable@vger.kernel.org
Assisted-by: Claude (Anthropic)
Signed-off-by: Aamir Ahmed <elb12345@hotmail.co.uk>
---
 drivers/net/usb/asix_common.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/net/usb/asix_common.c b/drivers/net/usb/asix_common.c
index 4f03f4e57655d..c50646ea35c7e 100644
--- a/drivers/net/usb/asix_common.c
+++ b/drivers/net/usb/asix_common.c
@@ -148,7 +148,7 @@ int asix_rx_fixup_internal(struct usbnet *dev, struct sk_buff *skb,
 	 * Also avoid unnecessarily discarding a good current netdev socket
 	 * buffer.
 	 */
-	if (rx->remaining && (rx->remaining + sizeof(u32) <= skb->len)) {
+	if (rx->remaining && (((rx->remaining + 1) & 0xfffe) + sizeof(u32) <= skb->len)) {
 		offset = ((rx->remaining + 1) & 0xfffe);
 		rx->header = get_unaligned_le32(skb->data + offset);
 		offset = 0;
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 3+ messages in thread

* Re: [PATCH] net: usb: asix: fix off-by-one in rx_fixup bounds check
  2026-09-07  7:52 Aamir Ahmed
@ 2026-09-08  3:35 ` Aamir Ahmed
  0 siblings, 0 replies; 3+ messages in thread
From: Aamir Ahmed @ 2026-09-08  3:35 UTC (permalink / raw)
  To: Andrew Lunn, David S . Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni
  Cc: Aamir Ahmed, Peter Korsgaard, Dean Jenkins, John Stultz,
	linux-usb, netdev, stable

Further revision is needed - the check is in the wrong place.

With rx->remaining = 3 and skb->len = 7 the new check does skip the
sync peek, but the loop below then reaches the same byte:

	while (offset + sizeof(u16) <= skb->len) {
		if (!rx->remaining) {
			...
			rx->header = get_unaligned_le32(skb->data + offset);

It copies the 3 remaining bytes, advances offset to 4, re-enters with
3 bytes left and reads 4 there. The guard only requires 2 bytes while
that branch reads 4; there is a case for exactly 2 bytes (split_head)
and none for 3.

I will send a v2 that guards that read instead.

Reaching 3 bytes left also needs an odd skb->len, which a conforming
device does not produce, so the v2 will drop the Fixes and stable
tags.

pw-bot: changes-requested

Regards,
Aamir Ahmed

^ permalink raw reply	[flat|nested] 3+ messages in thread

* Re: [PATCH] net: usb: asix: fix off-by-one in rx_fixup bounds check
@ 2026-09-10  4:52 netdev-bot+sashiko
  0 siblings, 0 replies; 3+ messages in thread
From: netdev-bot+sashiko @ 2026-09-10  4:52 UTC (permalink / raw)
  To: elb12345
  Cc: andrew, davem, edumazet, kuba, pabeni, peter, Dean_Jenkins,
	jstultz, linux-usb, netdev, stable

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

Critical: 0 · High: 1 (1) · Medium: 1 · Low: 0

- [High] Incomplete fix: the patch only tightens the pre-loop
  synchronisation predicate at drivers/net/usb/asix_common.c:151, but…
- [Medium] The commit message characterises the read at
  drivers/net/usb/asix_common.c:153 as a '1-byte heap OOB read' and adds…

Pre-existing issues:
- [High] Unsigned underflow after the loop's over-read leads to a large
  out-of-bounds copy and a kernel heap info leak to userspace.

--

net: usb: asix: fix off-by-one in rx_fixup bounds check

This tightens the multi-URB Data header synchronisation check in
asix_rx_fixup_internal() so the guard uses the same even-aligned offset
that the following get_unaligned_le32() actually reads from.  It carries
a Fixes tag for 3f30b158eba5 and Cc: stable.

No Link: tags in the commit header.

Quoting the changelog:

>     When rx->remaining is odd, the alignment rounding makes offset larger
>     than rx->remaining. For example with rx->remaining=3 and skb->len=7:
>     check passes (3+4<=7) but offset becomes ((3+1)&0xfffe)=4, and the
>     4-byte read at offset 4 needs 8 bytes total, causing a 1-byte
>     heap OOB read.

[Severity: Medium]
Is "heap OOB read" the right description for this read, and does it
justify Cc: stable on memory-safety grounds?

In the usbnet receive path the rx skb is allocated with dev->rx_urb_size
bytes of linear space and skb->len is only set from urb->actual_length:

drivers/net/usb/usbnet.c:rx_submit() {
	size_t			size = dev->rx_urb_size;
	...
	skb = __netdev_alloc_skb_ip_align(dev->net, size, flags);
	...
}

drivers/net/usb/usbnet.c:rx_complete() {
	...
	skb_put(skb, urb->actual_length);
	...
}

Every asix path sets an even rx_urb_size (2048 for FLAG_FRAMING_AX in
asix_devices.c and ax88172a.c, and 2048/4096/8192/16384 in
ax88178_set_mfb()).  The scenario in the changelog needs an odd skb->len,
so skb->len < rx_urb_size and skb->data + skb->len is still inside the
skb's own allocation, i.e. tailroom.

The overshoot of the read at the patched site is at most one byte
(offset + 4 <= rx->remaining + 5 <= skb->len + 1), and the value is only
used for the size-versus-complement comparison before being discarded, so
the visible effect is a spurious "Data Header synchronisation was lost"
message and a dropped frame.

Would it be more accurate to describe this as a read of stale/uninitialised
tailroom rather than an out-of-allocation access that KASAN would flag?

> diff --git a/drivers/net/usb/asix_common.c b/drivers/net/usb/asix_common.c
> index 4f03f4e57655d..c50646ea35c7e 100644
> --- a/drivers/net/usb/asix_common.c
> +++ b/drivers/net/usb/asix_common.c
> @@ -148,7 +148,7 @@ int asix_rx_fixup_internal(struct usbnet *dev, struct sk_buff *skb,
>  	 * Also avoid unnecessarily discarding a good current netdev socket
>  	 * buffer.
>  	 */
> -	if (rx->remaining && (rx->remaining + sizeof(u32) <= skb->len)) {
> +	if (rx->remaining && (((rx->remaining + 1) & 0xfffe) + sizeof(u32) <= skb->len)) {
>  		offset = ((rx->remaining + 1) & 0xfffe);
>  		rx->header = get_unaligned_le32(skb->data + offset);
>  		offset = 0;

[Severity: High]
Does this change actually remove the read described in the changelog, or
just move it into the main parsing loop?

Replaying the changelog example, rx->remaining = 3 and skb->len = 7:

  ((3 + 1) & 0xfffe) + 4 = 8 <= 7   -> false, sync branch skipped

offset stays 0 and control falls into the loop, which is gated only on two
bytes being present:

drivers/net/usb/asix_common.c:asix_rx_fixup_internal() {
	while (offset + sizeof(u16) <= skb->len) {
	...
			if (skb->len - offset == sizeof(u16)) {
				... rx->split_head = true; break;
			}

			if (rx->split_head == true) {
				...
			} else {
				rx->header = get_unaligned_le32(skb->data +
								offset);
				offset += sizeof(u32);
			}
	...
}

The first iteration copies the 3 remaining payload bytes, clears
rx->remaining and does offset += (copy_length + 1) & 0xfffe, giving
offset = 4.  The loop is re-entered because 4 + 2 <= 7, rx->remaining is 0
and rx->split_head is false, so the else branch runs
get_unaligned_le32(skb->data + 4) on a 7-byte skb, reading skb->data[7] --
the same byte the changelog says is being fixed.

offset is always even here (it starts at 0 and advances by 2, 4, or
(copy_length + 1) & 0xfffe), and only the exact skb->len - offset == 2 case
is special-cased, so any 3-byte tail reaches the 4-byte read.  The same
state is reachable with no prior rx->remaining at all, for example a fresh
rx state with skb->len == 3.

Would guarding that read with offset + sizeof(u32) <= skb->len, or handling
a 3-byte tail the way the 2-byte split_head case is handled, be the fix
instead of re-deriving the pre-loop predicate?

There also looks to be a behaviour change for the inputs the new predicate
excludes.  For odd rx->remaining with skb->len == rx->remaining + 4 the old
check was true (3 + 4 <= 7), so the header was validated and
reset_asix_rx_fixup_info() discarded the frame on a mismatch.  With the new
predicate that branch is skipped entirely, and the trailing bytes are
appended to rx->ax_skb and handed to the stack:

drivers/net/usb/asix_common.c:asix_rx_fixup_internal() {
	...
		if (rx->ax_skb) {
			skb_put_data(rx->ax_skb, skb->data + offset,
				     copy_length);
			if (!rx->remaining) {
				usbnet_skb_return(dev, rx->ax_skb);
	...
}

Does this lose the desynchronisation detection for exactly those lengths?

[Severity: High]
This isn't a bug introduced by this patch, but the same 3-byte-tail path
looks like it goes further than a one-byte over-read, and this patch steers
the changelog's own example into it, so it may be worth handling in the same
change.

Once the else branch has done offset += sizeof(u32) with only 3 bytes left,
offset == skb->len + 1.  The reconstructed header has 3 device-supplied
bytes plus one stale byte, and only bits 24-26 of the complement come from
the stale byte, so the self-check has roughly a one-in-eight chance of
passing and a device can retry:

			size = (u16)(rx->header & 0x7ff);
			if (size != ((~rx->header >> 16) & 0x7ff)) {
				... return 0;
			}

If it passes and size <= mtu + ETH_HLEN + VLAN_HLEN, rx->remaining = size
and the clamp is evaluated with offset > skb->len:

		if (rx->remaining > skb->len - offset) {
			copy_length = skb->len - offset;
			rx->remaining -= copy_length;
		} else {
			copy_length = rx->remaining;
			rx->remaining = 0;
		}

skb->len is unsigned int, so skb->len - offset wraps to 0xffffffff, the
test is false and copy_length becomes the full device-chosen size.  Can
this then copy up to mtu + ETH_HLEN + VLAN_HLEN bytes from one byte past
the received data, beyond the rx_urb_size allocation when skb->len is near
rx_urb_size?

		if (rx->ax_skb) {
			skb_put_data(rx->ax_skb, skb->data + offset,
				     copy_length);
			if (!rx->remaining) {
				usbnet_skb_return(dev, rx->ax_skb);

usbnet_skb_return() delivers the skb before the trailing
skb->len != offset check reports the error, so would adjacent heap contents
be handed to userspace as a normal received frame?  asix sets
FLAG_MULTI_PACKET, so there is no usbnet-level length sanity check ahead of
rx_fixup, and skb->len comes straight from urb->actual_length.

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/AS8P251MB00014BB4591CE6011152DB4FC8B22%40AS8P251MB0001.EURP251.PROD.OUTLOOK.COM

^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-09-10  4:52 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-10  4:52 [PATCH] net: usb: asix: fix off-by-one in rx_fixup bounds check netdev-bot+sashiko
  -- strict thread matches above, loose matches on Subject: below --
2026-09-07  7:52 Aamir Ahmed
2026-09-08  3:35 ` Aamir Ahmed

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox