Files
linux/drivers/net/mctp/mctp-serial.c
Doruk Tan Ozturk 793b9b729f mctp: serial: handle zero-length frames to prevent rx buffer overflow
The MCTP serial receive state machine reads a frame length byte in
mctp_serial_push_header() case 2 and validates it upper-bound-only:

	if (c > MCTP_SERIAL_FRAME_MTU) {
		dev->rxstate = STATE_ERR;
	} else {
		dev->rxlen = c;
		dev->rxpos = 0;
		dev->rxstate = STATE_DATA;
		...
	}

A length of zero passes this check, so rxlen is set to 0 and the state
machine advances to STATE_DATA. In mctp_serial_push() STATE_DATA, the
incoming byte is stored and rxpos incremented before the terminator is
tested:

	dev->rxbuf[dev->rxpos] = c;
	dev->rxpos++;
	dev->rxstate = STATE_DATA;
	if (dev->rxpos == dev->rxlen) {
		dev->rxpos = 0;
		dev->rxstate = STATE_TRAILER;
	}

With rxlen == 0 the "rxpos == rxlen" terminator can never fire (rxpos is
already 1 on the first data byte), so subsequent bytes are written past
the end of the fixed 74-byte rxbuf, which is the last member of the
netdev private area. Every following data byte is an attacker-controlled
1-byte out-of-bounds heap write, and the overflow continues until a
frame (0x7e) or escape byte resets the parser -- effectively unbounded.

Reaching this requires CAP_NET_ADMIN to attach the N_MCTP line
discipline and bring the resulting mctpserialN netdev up, after which
the bytes arrive via the tty receive path.

Route a zero-length frame straight to STATE_TRAILER instead of
STATE_DATA. The trailer/framing bytes are still consumed, and the frame
resolves to a zero-length skb that the MCTP core rejects; the parser
never enters STATE_DATA with rxlen == 0, so the out-of-bounds write can
no longer occur.

KASAN, on a frame of 0x7e 0x01 0x00 followed by data bytes (before this
change):

  UBSAN: array-index-out-of-bounds in drivers/net/mctp/mctp-serial.c:370
  index 74 is out of range for type 'u8 [74]'
  BUG: KASAN: slab-out-of-bounds in mctp_serial_tty_receive_buf
  Write of size 1 at addr ... by task kworker/u16:0
   mctp_serial_tty_receive_buf
   tty_ldisc_receive_buf
   flush_to_ldisc
  Allocated by task 152:
   alloc_netdev_mqs
   mctp_serial_open

v2: route zero-length frames to STATE_TRAILER instead of STATE_ERR so
    the trailer/framing bytes are still consumed (Jeremy Kerr).

Found by 0sec automated security-research tooling (https://0sec.ai).
Fixes: a0c2ccd9b5 ("mctp: Add MCTP-over-serial transport binding")
Cc: stable@vger.kernel.org
Suggested-by: Jeremy Kerr <jk@codeconstruct.com.au>
Assisted-by: 0sec:multi-model
Signed-off-by: Doruk Tan Ozturk <doruk@0sec.ai>
Reviewed-by: Simon Horman <horms@kernel.org>
Link: https://patch.msgid.link/20260715082021.46315-1-doruk@0sec.ai
Signed-off-by: Paolo Abeni <pabeni@redhat.com>
2026-07-23 15:34:00 +02:00

635 lines
13 KiB
C

// SPDX-License-Identifier: GPL-2.0
/*
* Management Component Transport Protocol (MCTP) - serial transport
* binding. This driver is an implementation of the DMTF specificiation
* "DSP0253 - Management Component Transport Protocol (MCTP) Serial Transport
* Binding", available at:
*
* https://www.dmtf.org/sites/default/files/standards/documents/DSP0253_1.0.0.pdf
*
* This driver provides DSP0253-type MCTP-over-serial transport using a Linux
* tty device, by setting the N_MCTP line discipline on the tty.
*
* Copyright (c) 2021 Code Construct
*/
#include <linux/idr.h>
#include <linux/if_arp.h>
#include <linux/module.h>
#include <linux/skbuff.h>
#include <linux/tty.h>
#include <linux/workqueue.h>
#include <linux/crc-ccitt.h>
#include <linux/mctp.h>
#include <net/mctp.h>
#include <net/mctpdevice.h>
#include <net/pkt_sched.h>
#define MCTP_SERIAL_MTU 68 /* base mtu (64) + mctp header */
#define MCTP_SERIAL_FRAME_MTU (MCTP_SERIAL_MTU + 6) /* + serial framing */
#define MCTP_SERIAL_VERSION 0x1 /* DSP0253 defines a single version: 1 */
#define BUFSIZE MCTP_SERIAL_FRAME_MTU
#define BYTE_FRAME 0x7e
#define BYTE_ESC 0x7d
#define FCS_INIT 0xffff
static DEFINE_IDA(mctp_serial_ida);
enum mctp_serial_state {
STATE_IDLE,
STATE_START,
STATE_HEADER,
STATE_DATA,
STATE_ESCAPE,
STATE_TRAILER,
STATE_DONE,
STATE_ERR,
};
struct mctp_serial {
struct net_device *netdev;
struct tty_struct *tty;
int idx;
/* protects our rx & tx state machines; held during both paths */
spinlock_t lock;
struct work_struct tx_work;
enum mctp_serial_state txstate, rxstate;
u16 txfcs, rxfcs, rxfcs_rcvd;
unsigned int txlen, rxlen;
unsigned int txpos, rxpos;
u8 txbuf[BUFSIZE],
rxbuf[BUFSIZE];
};
static bool needs_escape(u8 c)
{
return c == BYTE_ESC || c == BYTE_FRAME;
}
static unsigned int next_chunk_len(struct mctp_serial *dev)
{
unsigned int i;
/* either we have no bytes to send ... */
if (dev->txpos == dev->txlen)
return 0;
/* ... or the next byte to send is an escaped byte; requiring a
* single-byte chunk...
*/
if (needs_escape(dev->txbuf[dev->txpos]))
return 1;
/* ... or we have one or more bytes up to the next escape - this chunk
* will be those non-escaped bytes, and does not include the escaped
* byte.
*/
for (i = 1; i + dev->txpos < dev->txlen; i++) {
if (needs_escape(dev->txbuf[dev->txpos + i]))
break;
}
return i;
}
static ssize_t write_chunk(struct mctp_serial *dev, u8 *buf, size_t len)
{
return dev->tty->ops->write(dev->tty, buf, len);
}
static void mctp_serial_tx_work(struct work_struct *work)
{
struct mctp_serial *dev = container_of(work, struct mctp_serial,
tx_work);
unsigned long flags;
ssize_t txlen;
unsigned int len;
u8 c, buf[3];
spin_lock_irqsave(&dev->lock, flags);
/* txstate represents the next thing to send */
switch (dev->txstate) {
case STATE_START:
dev->txpos = 0;
fallthrough;
case STATE_HEADER:
buf[0] = BYTE_FRAME;
buf[1] = MCTP_SERIAL_VERSION;
buf[2] = dev->txlen;
if (!dev->txpos)
dev->txfcs = crc_ccitt(FCS_INIT, buf + 1, 2);
txlen = write_chunk(dev, buf + dev->txpos, 3 - dev->txpos);
if (txlen <= 0) {
dev->txstate = STATE_ERR;
} else {
dev->txpos += txlen;
if (dev->txpos == 3) {
dev->txstate = STATE_DATA;
dev->txpos = 0;
}
}
break;
case STATE_ESCAPE:
buf[0] = dev->txbuf[dev->txpos] & ~0x20;
txlen = write_chunk(dev, buf, 1);
if (txlen <= 0) {
dev->txstate = STATE_ERR;
} else {
dev->txpos += txlen;
if (dev->txpos == dev->txlen) {
dev->txstate = STATE_TRAILER;
dev->txpos = 0;
}
}
break;
case STATE_DATA:
len = next_chunk_len(dev);
if (len) {
c = dev->txbuf[dev->txpos];
if (len == 1 && needs_escape(c)) {
buf[0] = BYTE_ESC;
buf[1] = c & ~0x20;
dev->txfcs = crc_ccitt_byte(dev->txfcs, c);
txlen = write_chunk(dev, buf, 2);
if (txlen == 2)
dev->txpos++;
else if (txlen == 1)
dev->txstate = STATE_ESCAPE;
else
dev->txstate = STATE_ERR;
} else {
txlen = write_chunk(dev,
dev->txbuf + dev->txpos,
len);
if (txlen <= 0) {
dev->txstate = STATE_ERR;
} else {
dev->txfcs = crc_ccitt(dev->txfcs,
dev->txbuf +
dev->txpos,
txlen);
dev->txpos += txlen;
}
}
if (dev->txstate == STATE_DATA &&
dev->txpos == dev->txlen) {
dev->txstate = STATE_TRAILER;
dev->txpos = 0;
}
break;
}
dev->txstate = STATE_TRAILER;
dev->txpos = 0;
fallthrough;
case STATE_TRAILER:
buf[0] = dev->txfcs >> 8;
buf[1] = dev->txfcs & 0xff;
buf[2] = BYTE_FRAME;
txlen = write_chunk(dev, buf + dev->txpos, 3 - dev->txpos);
if (txlen <= 0) {
dev->txstate = STATE_ERR;
} else {
dev->txpos += txlen;
if (dev->txpos == 3) {
dev->txstate = STATE_DONE;
dev->txpos = 0;
}
}
break;
default:
netdev_err_once(dev->netdev, "invalid tx state %d\n",
dev->txstate);
}
if (dev->txstate == STATE_DONE) {
dev->netdev->stats.tx_packets++;
dev->netdev->stats.tx_bytes += dev->txlen;
dev->txlen = 0;
dev->txpos = 0;
clear_bit(TTY_DO_WRITE_WAKEUP, &dev->tty->flags);
dev->txstate = STATE_IDLE;
spin_unlock_irqrestore(&dev->lock, flags);
netif_wake_queue(dev->netdev);
} else {
spin_unlock_irqrestore(&dev->lock, flags);
}
}
static netdev_tx_t mctp_serial_tx(struct sk_buff *skb, struct net_device *ndev)
{
struct mctp_serial *dev = netdev_priv(ndev);
unsigned long flags;
WARN_ON(dev->txstate != STATE_IDLE);
if (skb->len > MCTP_SERIAL_MTU) {
dev->netdev->stats.tx_dropped++;
goto out;
}
spin_lock_irqsave(&dev->lock, flags);
netif_stop_queue(dev->netdev);
skb_copy_bits(skb, 0, dev->txbuf, skb->len);
dev->txpos = 0;
dev->txlen = skb->len;
dev->txstate = STATE_START;
spin_unlock_irqrestore(&dev->lock, flags);
set_bit(TTY_DO_WRITE_WAKEUP, &dev->tty->flags);
schedule_work(&dev->tx_work);
out:
kfree_skb(skb);
return NETDEV_TX_OK;
}
static void mctp_serial_tty_write_wakeup(struct tty_struct *tty)
{
struct mctp_serial *dev = tty->disc_data;
schedule_work(&dev->tx_work);
}
static void mctp_serial_rx(struct mctp_serial *dev)
{
struct mctp_skb_cb *cb;
struct sk_buff *skb;
if (dev->rxfcs != dev->rxfcs_rcvd) {
dev->netdev->stats.rx_dropped++;
dev->netdev->stats.rx_crc_errors++;
return;
}
skb = netdev_alloc_skb(dev->netdev, dev->rxlen);
if (!skb) {
dev->netdev->stats.rx_dropped++;
return;
}
skb->protocol = htons(ETH_P_MCTP);
skb_put_data(skb, dev->rxbuf, dev->rxlen);
skb_reset_network_header(skb);
cb = __mctp_cb(skb);
cb->halen = 0;
netif_rx(skb);
dev->netdev->stats.rx_packets++;
dev->netdev->stats.rx_bytes += dev->rxlen;
}
static void mctp_serial_push_header(struct mctp_serial *dev, u8 c)
{
switch (dev->rxpos) {
case 0:
if (c == BYTE_FRAME)
dev->rxpos++;
else
dev->rxstate = STATE_ERR;
break;
case 1:
if (c == MCTP_SERIAL_VERSION) {
dev->rxpos++;
dev->rxfcs = crc_ccitt_byte(FCS_INIT, c);
} else {
dev->rxstate = STATE_ERR;
}
break;
case 2:
if (c > MCTP_SERIAL_FRAME_MTU) {
dev->rxstate = STATE_ERR;
} else {
dev->rxlen = c;
dev->rxpos = 0;
dev->rxstate = c > 0 ? STATE_DATA : STATE_TRAILER;
dev->rxfcs = crc_ccitt_byte(dev->rxfcs, c);
}
break;
}
}
static void mctp_serial_push_trailer(struct mctp_serial *dev, u8 c)
{
switch (dev->rxpos) {
case 0:
dev->rxfcs_rcvd = c << 8;
dev->rxpos++;
break;
case 1:
dev->rxfcs_rcvd |= c;
dev->rxpos++;
break;
case 2:
if (c != BYTE_FRAME) {
dev->rxstate = STATE_ERR;
} else {
mctp_serial_rx(dev);
dev->rxlen = 0;
dev->rxpos = 0;
dev->rxstate = STATE_IDLE;
}
break;
}
}
static void mctp_serial_push(struct mctp_serial *dev, u8 c)
{
switch (dev->rxstate) {
case STATE_IDLE:
dev->rxstate = STATE_HEADER;
fallthrough;
case STATE_HEADER:
mctp_serial_push_header(dev, c);
break;
case STATE_ESCAPE:
c |= 0x20;
fallthrough;
case STATE_DATA:
if (dev->rxstate != STATE_ESCAPE && c == BYTE_ESC) {
dev->rxstate = STATE_ESCAPE;
} else {
dev->rxfcs = crc_ccitt_byte(dev->rxfcs, c);
dev->rxbuf[dev->rxpos] = c;
dev->rxpos++;
dev->rxstate = STATE_DATA;
if (dev->rxpos == dev->rxlen) {
dev->rxpos = 0;
dev->rxstate = STATE_TRAILER;
}
}
break;
case STATE_TRAILER:
mctp_serial_push_trailer(dev, c);
break;
case STATE_ERR:
if (c == BYTE_FRAME)
dev->rxstate = STATE_IDLE;
break;
default:
netdev_err_once(dev->netdev, "invalid rx state %d\n",
dev->rxstate);
}
}
static void mctp_serial_tty_receive_buf(struct tty_struct *tty, const u8 *c,
const u8 *f, size_t len)
{
struct mctp_serial *dev = tty->disc_data;
size_t i;
if (!netif_running(dev->netdev))
return;
/* we don't (currently) use the flag bytes, just data. */
for (i = 0; i < len; i++)
mctp_serial_push(dev, c[i]);
}
static void mctp_serial_uninit(struct net_device *ndev)
{
struct mctp_serial *dev = netdev_priv(ndev);
cancel_work_sync(&dev->tx_work);
}
static const struct net_device_ops mctp_serial_netdev_ops = {
.ndo_start_xmit = mctp_serial_tx,
.ndo_uninit = mctp_serial_uninit,
};
static void mctp_serial_setup(struct net_device *ndev)
{
ndev->type = ARPHRD_MCTP;
/* we limit at the fixed MTU, which is also the MCTP-standard
* baseline MTU, so is also our minimum
*/
ndev->mtu = MCTP_SERIAL_MTU;
ndev->max_mtu = MCTP_SERIAL_MTU;
ndev->min_mtu = MCTP_SERIAL_MTU;
ndev->hard_header_len = 0;
ndev->addr_len = 0;
ndev->tx_queue_len = DEFAULT_TX_QUEUE_LEN;
ndev->flags = IFF_NOARP;
ndev->netdev_ops = &mctp_serial_netdev_ops;
ndev->needs_free_netdev = true;
}
static int mctp_serial_open(struct tty_struct *tty)
{
struct mctp_serial *dev;
struct net_device *ndev;
char name[32];
int idx, rc;
if (!capable(CAP_NET_ADMIN))
return -EPERM;
if (!tty->ops->write)
return -EOPNOTSUPP;
idx = ida_alloc(&mctp_serial_ida, GFP_KERNEL);
if (idx < 0)
return idx;
snprintf(name, sizeof(name), "mctpserial%d", idx);
ndev = alloc_netdev(sizeof(*dev), name, NET_NAME_ENUM,
mctp_serial_setup);
if (!ndev) {
rc = -ENOMEM;
goto free_ida;
}
dev = netdev_priv(ndev);
dev->idx = idx;
dev->tty = tty;
dev->netdev = ndev;
dev->txstate = STATE_IDLE;
dev->rxstate = STATE_IDLE;
spin_lock_init(&dev->lock);
INIT_WORK(&dev->tx_work, mctp_serial_tx_work);
rc = mctp_register_netdev(ndev, NULL, MCTP_PHYS_BINDING_SERIAL);
if (rc)
goto free_netdev;
tty->receive_room = 64 * 1024;
tty->disc_data = dev;
return 0;
free_netdev:
free_netdev(ndev);
free_ida:
ida_free(&mctp_serial_ida, idx);
return rc;
}
static void mctp_serial_close(struct tty_struct *tty)
{
struct mctp_serial *dev = tty->disc_data;
int idx = dev->idx;
mctp_unregister_netdev(dev->netdev);
ida_free(&mctp_serial_ida, idx);
}
static struct tty_ldisc_ops mctp_ldisc = {
.owner = THIS_MODULE,
.num = N_MCTP,
.name = "mctp",
.open = mctp_serial_open,
.close = mctp_serial_close,
.receive_buf = mctp_serial_tty_receive_buf,
.write_wakeup = mctp_serial_tty_write_wakeup,
};
static int __init mctp_serial_init(void)
{
return tty_register_ldisc(&mctp_ldisc);
}
static void __exit mctp_serial_exit(void)
{
tty_unregister_ldisc(&mctp_ldisc);
}
module_init(mctp_serial_init);
module_exit(mctp_serial_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Jeremy Kerr <jk@codeconstruct.com.au>");
MODULE_DESCRIPTION("MCTP Serial transport");
#if IS_ENABLED(CONFIG_MCTP_SERIAL_TEST)
#include <kunit/test.h>
#define MAX_CHUNKS 6
struct test_chunk_tx {
u8 input_len;
u8 input[MCTP_SERIAL_MTU];
u8 chunks[MAX_CHUNKS];
};
static void test_next_chunk_len(struct kunit *test)
{
struct mctp_serial devx;
struct mctp_serial *dev = &devx;
int next;
const struct test_chunk_tx *params = test->param_value;
memset(dev, 0x0, sizeof(*dev));
memcpy(dev->txbuf, params->input, params->input_len);
dev->txlen = params->input_len;
for (size_t i = 0; i < MAX_CHUNKS; i++) {
next = next_chunk_len(dev);
dev->txpos += next;
KUNIT_EXPECT_EQ(test, next, params->chunks[i]);
if (next == 0) {
KUNIT_EXPECT_EQ(test, dev->txpos, dev->txlen);
return;
}
}
KUNIT_FAIL_AND_ABORT(test, "Ran out of chunks");
}
static struct test_chunk_tx chunk_tx_tests[] = {
{
.input_len = 5,
.input = { 0x00, 0x11, 0x22, 0x7e, 0x80 },
.chunks = { 3, 1, 1, 0},
},
{
.input_len = 5,
.input = { 0x00, 0x11, 0x22, 0x7e, 0x7d },
.chunks = { 3, 1, 1, 0},
},
{
.input_len = 3,
.input = { 0x7e, 0x11, 0x22, },
.chunks = { 1, 2, 0},
},
{
.input_len = 3,
.input = { 0x7e, 0x7e, 0x7d, },
.chunks = { 1, 1, 1, 0},
},
{
.input_len = 4,
.input = { 0x7e, 0x7e, 0x00, 0x7d, },
.chunks = { 1, 1, 1, 1, 0},
},
{
.input_len = 6,
.input = { 0x7e, 0x7e, 0x00, 0x7d, 0x10, 0x10},
.chunks = { 1, 1, 1, 1, 2, 0},
},
{
.input_len = 1,
.input = { 0x7e },
.chunks = { 1, 0 },
},
{
.input_len = 1,
.input = { 0x80 },
.chunks = { 1, 0 },
},
{
.input_len = 3,
.input = { 0x80, 0x80, 0x00 },
.chunks = { 3, 0 },
},
{
.input_len = 7,
.input = { 0x01, 0x00, 0x08, 0xc8, 0x00, 0x80, 0x02 },
.chunks = { 7, 0 },
},
{
.input_len = 7,
.input = { 0x01, 0x00, 0x08, 0xc8, 0x7e, 0x80, 0x02 },
.chunks = { 4, 1, 2, 0 },
},
};
KUNIT_ARRAY_PARAM(chunk_tx, chunk_tx_tests, NULL);
static struct kunit_case mctp_serial_test_cases[] = {
KUNIT_CASE_PARAM(test_next_chunk_len, chunk_tx_gen_params),
};
static struct kunit_suite mctp_serial_test_suite = {
.name = "mctp_serial",
.test_cases = mctp_serial_test_cases,
};
kunit_test_suite(mctp_serial_test_suite);
#endif /* CONFIG_MCTP_SERIAL_TEST */