[PATCH net v3] can: j1939: validate the EDPO packet number

From: Quchaosheng

Date: Fri Oct 09 2026 - 07:02:41 EST


From: Chaosheng Qu <quchaosheng000406@xxxxxxx>

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.

Reaching the dereference needs two things, and this patch keeps them as
two separate changes because neither is sufficient alone:

- the range check in j1939_xtp_rx_dpo_one(), which stops pkt.dpo from
leaving the message
- the NULL guard in j1939_session_completed(), which covers the offset
lookup coming up empty for any other reason

The reproducer in the link was run three times in each configuration: the
unpatched kernel 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 point --
it shows the guard does not close this hole on its own, so the range check
is not redundant with it.

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. The vcan interface used by the reproducer has to be created
first, which normally needs privileges; given the interface, 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

J1939_XTP_ABORT_BAD_EDPO_OFFSET and its "Bad EDPO offset" string are
already in the tree and have no caller; the range check uses that abort.

Reported-by: Sashiko <sashiko-bot@xxxxxxxxxx>
Link: https://sashiko.dev/#/patchset/20260928193312.553632-1-mkl@xxxxxxxxxxxxxx?part=3
Assisted-by: LLM
Signed-off-by: Chaosheng Qu <quchaosheng000406@xxxxxxx>
---
net/can/j1939/transport.c | 37 +++++++++++++++++++++++++++++++++----
1 file changed, 33 insertions(+), 4 deletions(-)

diff --git a/net/can/j1939/transport.c b/net/can/j1939/transport.c
index 8fcfd13e5e6f..065b4f71e9af 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);

--
2.43.0