mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: "Alexander Hölzl" <alexander.hoelzl@gmx.net>
To: Oleksij Rempel <o.rempel@pengutronix.de>
Cc: robin@protonic.nl, linux-kernel@vger.kernel.org,
	kernel@pengutronix.de, linux-can@vger.kernel.org
Subject: Re: [PATCH v2] Fix J1939 implementation not handling holds correctly
Date: Sun, 17 May 2026 22:57:56 +0200	[thread overview]
Message-ID: <21dbdcce-1355-411b-8be0-32a0dd773853@gmx.net> (raw)
In-Reply-To: <aglyT74qHffw0vRl@pengutronix.de>


Hello,
once again thank you for your thorough review :)
I'll try to send the next patch by tomorrow evening.

On 5/17/26 09:46, Oleksij Rempel wrote:
> Hi Alexander,
> 
> On Sat, May 16, 2026 at 04:35:25PM +0200, Alexander Hölzl wrote:
>> The J1939 protocol allows the receiver of directed segemented messages
>> to hold the data transfer. The kernel implementation did not handle hold
>> messages correctly was not able to resume from a hold.
>>
>> Fix J1939 RTS/CTS session not being able to resume from hold.
>> Replace hardcoded timeout with define.
>> Add CTS hold behavior tests.
>>
>> Signed-off-by: Alexander Hölzl <alexander.hoelzl@gmx.net>
>> ---
>> Compared to the last patch I removed all of the todo comments I had
>> still had in the code. I will implement hold notification in the error
>> queue in different patch.
>>
>> I replaced the hardcoded hold timeout with a define. I can replace the
>> other hardcoded timeouts in a different patch if you want me to, but
>> for now I only touched code related to the holds.
>> I also added a helper function to check if a CTS is a hold.
>>
>> I also added a baseline test case as you wanted. Altough in the future
>> it should probably moved to another file which specfically tests normal
>> RTS/CTS behavior.
>>
>> Also addressed all the comments Sashiko had on the test file and I'm
>> also now sending an EOMA in the test as without it between every test
>> there was a 1250ms wait until the session timed out...
> 
> Nice!
> 
>>   net/can/j1939/transport.c                     |  48 ++-
> 
> Please split fix and testing patches. Otherwise reverting it for testing
> will break testing (at least it will make things a bit more
> complicated).
Sure that's a good point! Also the test script test_cts_hold.sh is very 
much based on the  test_raw_filter.sh script. Now that I'm thinking 
about it, it would be probably cleaner to move out the common parts into 
a helper file?>
>>   tools/testing/selftests/net/can/.gitignore    |   1 +
>>   tools/testing/selftests/net/can/Makefile      |   8 +-
>>   tools/testing/selftests/net/can/config        |   1 +
>>   .../testing/selftests/net/can/test_cts_hold.c | 322 ++++++++++++++++++
>>   .../selftests/net/can/test_cts_hold.sh        |  45 +++
>>   6 files changed, 406 insertions(+), 19 deletions(-)
>>   create mode 100644 tools/testing/selftests/net/can/test_cts_hold.c
>>   create mode 100755 tools/testing/selftests/net/can/test_cts_hold.sh
> 
> 
>> +/* Time until session invalidation upon reception of a hold message.
>> + * Corresponds to T4 in the specification.
> 
> I would add here documentation references, it would help another me in
> two yars to understand things :)
> ISO 11783-3 2018 - 5.10.3.5 Connection closure
> SAE J1939-21 2001 - 5.10.2.4 Connection Closure
> 
> You can update version if you have more recent variants.
Oh yeah, the 2001 version is quite old, I'm using the 2022 revision.
Although I don't think there are any changes in the spec which would be
relevant for this commit in there.
Will add the comments.>
>> + */
>> +#define J1939_CTS_HOLD_TIMEOUT_MS 1050
>> +
>>   enum j1939_xtp_abort {
>>   	J1939_XTP_NO_ABORT = 0,
>>   	J1939_XTP_ABORT_BUSY = 1,
>> @@ -1428,6 +1433,11 @@ j1939_xtp_rx_eoma(struct j1939_priv *priv, struct sk_buff *skb,
>>   	j1939_session_put(session);
>>   }
>>   
> 
> Here comment:
> SAE J1939-21 2001 - 5.10.2.4 Connection Closure
> ISO 11783-3 2018 - 5.11.5.4 Extended Connection Mode Clear To Send (ETP.CM_CTS)
> 
> The number of packets to send can be set to 0 to hold the connection
> 
>> +static inline bool j1939_cts_is_hold(const struct sk_buff *skb)
>> +{
>> +	return (!skb->data[1]);
>> +}
>> +
>>   static void
>>   j1939_xtp_rx_cts_one(struct j1939_session *session, struct sk_buff *skb)
>>   {
>> @@ -1442,9 +1452,15 @@ j1939_xtp_rx_cts_one(struct j1939_session *session, struct sk_buff *skb)
>>   
>>   	netdev_dbg(session->priv->ndev, "%s: 0x%p\n", __func__, session);
>>   
>> -	if (session->last_cmd == dat[0]) {
>> -		err = J1939_XTP_ABORT_DUP_SEQ;
>> -		goto out_session_cancel;
> 
> We need to document old and new sanity check behavior in the commit
> message.
> 
> Before this change it protects only against a flood of CTS where the
> linux stack didn't managed to start sending the data - less probable
> scenario. And it prevents valid CTS hold support - most probable
> scenario.
> 
> After the patch it allows to sende multiple CTS including CTS(0), but
> prevents requesting the already transferred and acked packets. So, the
> kernel will abort immediately instead of going in to timeout.

Correct, and just to say it again sending a CTS while the transmission 
is ongoing is also not correct according to the standard. So a strict 
implementation would abort the session on reception of a CTS while 
transmitting. But that's a problem for a future patch.

>> +	session->last_cmd = dat[0];
>> +
>> +	if (j1939_cts_is_hold(skb)) {
>> +		if (session->transmission)
>> +			j1939_session_txtimer_cancel(session);
>> +
> 
> Here we need a comment:
> The originator should abort the session after T4 (=< 1050ms):
>   SAE J1939-21 2001 - 5.10.2.4 Connection Closure
>   a lack of a CTS for more than (T4) seconds after a CTS (0) message to
>   "hold the connection open" will all cause a connection closure to occur.
> 
> The receiver should send followup CTS not later then Th (=< 500ms):
>   SAE J1939-21 2001 - C.1 Connection Mode Data Transfer
>   The responder station then issues a TP.CM_CTS indicating that it wants
>   to hold the connection open but cannot receive any packets right now. A
>   maximum of 500 ms later it must send another TP.CM_CTS message to hold
>   the connection.
> 
> But we care only about T4 value on our side.
  Will add!>
>> +		j1939_tp_set_rxtimeout(session, J1939_CTS_HOLD_TIMEOUT_MS);
> 
> 
>> +		netdev_dbg(session->priv->ndev, "%s: 0x%p received CTS hold\n", __func__, session);
>> +		return;
>>   	}
>>   
>>   	if (session->skcb.addr.type == J1939_ETP)
>> @@ -1457,7 +1473,11 @@ j1939_xtp_rx_cts_one(struct j1939_session *session, struct sk_buff *skb)
>>   	else if (dat[1] > session->pkt.block /* 0xff for etp */)
>>   		goto out_session_cancel;
>>   
>> -	/* set packet counters only when not CTS(0) */
>> +	if (session->pkt.tx_acked >= pkt) {
>> +		err = J1939_XTP_ABORT_DUP_SEQ;
>> +		goto out_session_cancel;
>> +	}
>> +
>>   	session->pkt.tx_acked = pkt - 1;
>>   	j1939_session_skb_drop_old(session);
>>   	session->pkt.last = session->pkt.tx_acked + dat[1];
>> @@ -1467,19 +1487,13 @@ j1939_xtp_rx_cts_one(struct j1939_session *session, struct sk_buff *skb)
>>   	/* TODO: do not set tx here, do it in txtimer */
>>   	session->pkt.tx = session->pkt.tx_acked;
>>   
>> -	session->last_cmd = dat[0];
>> -	if (dat[1]) {
>> -		j1939_tp_set_rxtimeout(session, 1250);
>> -		if (session->transmission) {
>> -			if (session->pkt.tx_acked)
>> -				j1939_sk_errqueue(session,
>> -						  J1939_ERRQUEUE_TX_SCHED);
>> -			j1939_session_txtimer_cancel(session);
>> -			j1939_tp_schedule_txtimer(session, 0);
>> -		}
>> -	} else {
>> -		/* CTS(0) */
>> -		j1939_tp_set_rxtimeout(session, 550);
> 
> Now I understand where 550 comes from. It is Th + 50. But you are
> correct, we need to use T4 here.
Exactly, the Th is a receiver side performance requirement. But it is a
quite confusing standard. It's very easy to get confused.>
>> +	j1939_tp_set_rxtimeout(session, 1250);
>> +	if (session->transmission) {
>> +		if (session->pkt.tx_acked)
>> +			j1939_sk_errqueue(session,
>> +						J1939_ERRQUEUE_TX_SCHED);
>> +		j1939_session_txtimer_cancel(session);
>> +		j1939_tp_schedule_txtimer(session, 0);
>>   	}
>>   	return;
>>   
>> diff --git a/tools/testing/selftests/net/can/.gitignore b/tools/testing/selftests/net/can/.gitignore
>> +
>> +/* Segemented payload sent by the J1939 socket*/
>> +const uint8_t J1939_PAYLOAD[]	= {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09};
> 
> can you please remove tabs or extra spaces between [] and =
> const uint8_t J1939_PAYLOAD[] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09};
> 
Sure!

>> +
>> +/* Expected RTS payload */
>> +const uint8_t RTS_PAYLOAD[]					= {0x10, 0x0A, 0x00, 0x02, 0x02, 0x00, 0xAB, 0x00};
>> +/* Hold payload to be sent by raw socket */
>> +const uint8_t HOLD_PAYLOAD[]				= {0x11, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0xAB, 0x00};
>> +/* CTS to send to only allow for the transmission of one data frame */
>> +const uint8_t CTS_1_FRAME_PAYLOAD[]			= {0x11, 0x01, 0x01, 0xFF, 0xFF, 0x00, 0xAB, 0x00};
>> +/* Resume payload to resume from connection which has been held directly after RTS*/
>> +const uint8_t RESUME_IMMEDIATE_PAYLOAD[]	= {0x11, 0x02, 0x01, 0xFF, 0xFF, 0x00, 0xAB, 0x00};
>> +/* Resume payload to resume session which has been held after first data frame */
>> +const uint8_t RESUME_PAYLOAD[]				= {0x11, 0x01, 0x02, 0xFF, 0xFF, 0x00, 0xAB, 0x00};
>> +/* Data payloads */
>> +const uint8_t DATA_1_PAYLOAD[]				= {0x01, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
>> +const uint8_t DATA_2_PAYLOAD[]				= {0x02, 0x07, 0x08, 0x09, 0xFF, 0xFF, 0xFF, 0xFF};
>> +
>> +/* EOMA payload to cleanup session */
>> +const uint8_t EOMA_PAYLOAD[]				= {0x13, 0x0A, 0x00, 0x02, 0xFF, 0x00, 0xAB, 0x00};
>> +
>> +/* Timeout payload sent on connection timeout */
>> +const uint8_t ABORT_TIMEOUT_PAYLOAD[]		= {0xFF, 0x03, 0xFF, 0xFF, 0xFF, 0x00, 0xAB, 0x00};
>> +char CANIF[IFNAMSIZ];
>> +
>> +static int recv_payload(int sock, const uint8_t *payload, size_t len)
>> +{
>> +	struct can_frame rx_frame = {};
>> +
>> +	if (recv(sock, &rx_frame, sizeof(rx_frame), 0) < 0) {
> 
> recv will block until we receive something, it will be not good if test fails
> and nothing is send. Probably something like this:
> 
> static int recv_payload_timeout(int sock, const uint8_t *payload, size_t len, int timeout_ms)
> {
> 	struct can_frame rx_frame = {};
> 	struct pollfd pfd = {
> 		.fd = sock,
> 		.events = POLLIN,
> 	};
> 	int ret;
> 
> 	/* Wait for data to be ready to read, up to timeout_ms */
> 	ret = poll(&pfd, 1, timeout_ms);
> 	if (ret < 0) {
> 		perror("poll failed");
> 		return 1;
> 	}
> 	
> 	if (ret == 0) {
> 		fprintf(stderr, "timeout waiting for can raw frame\n");
> 		return 1;
> 	}
> 
> 	/* Socket is readable, recv will not block */
> 	if (recv(sock, &rx_frame, sizeof(rx_frame), 0) < 0) {
> 		perror("failed to recv can raw frame");
> 		return 1;
> 	}
> 
> 	if (rx_frame.len != len) {
> 		fprintf(stderr, "received data length does not match expected value\n");
> 		return 1;
> 	}
> 
> 	if (memcmp(rx_frame.data, payload, len)) {
> 		fprintf(stderr, "received data does not match expected value\n");
> 		return 1;
> 	}
> 
> 	return 0;
> }
> 
My original way to avoid this problem was setting SO_RCVTIMEO on the raw 
socket but I can use poll instead. That would also have the advantage 
that you could change the timeout more easily. Will do!

> ....
> 
>> +/* Test timeout after not resuming hold */
>> +TEST_F(can_env, test_hold_timeout)
>> +{
>> +	struct can_frame tx_frame = {
>> +		.can_id = RECEIVER_TP_CM_ID,
>> +		.len = 8,
>> +	};
>> +
>> +	memcpy(tx_frame.data, HOLD_PAYLOAD, sizeof(HOLD_PAYLOAD));
>> +	int res = send(self->j1939_sock, J1939_PAYLOAD, sizeof(J1939_PAYLOAD), 0);
>> +
>> +	ASSERT_GT(res, 0)
>> +		TH_LOG("failed to send j1939 payload: %d", errno);
>> +
>> +	res = recv_payload(self->raw_sock, RTS_PAYLOAD, sizeof(RTS_PAYLOAD));
>> +	ASSERT_EQ(res, 0)
>> +		TH_LOG("Failed to receive RTS as expeceted");
>> +
>> +	res = send(self->raw_sock, &tx_frame, sizeof(tx_frame), 0);
>> +	ASSERT_GT(res, 0)
>> +		TH_LOG("failed to send hold with raw sock: %d", errno);
>> +
>> +	/* Wait for 1100 ms and receive the abort due to CTS hold timeout.
>> +	 * The actual timeout is 1050ms but with this test setup there is no point
>> +	 * in trying to be this exact.
>> +	 */
>> +	usleep(1100 * 1000);
>> +
>> +	res = recv_payload(self->raw_sock, ABORT_TIMEOUT_PAYLOAD, sizeof(ABORT_TIMEOUT_PAYLOAD));
>> +	ASSERT_EQ(res, 0)
>> +		TH_LOG("Failed to receive abort as expeceted");
>> +}
>   
> This test may potentially have random fails due to CI system load.
> May be:
> 
> #define DEFAULT_RECV_TIMEOUT_MS 2000
> 
> static int recv_payload(int sock, const uint8_t *payload, size_t len)
> {
> 	return recv_payload_timeout(sock, payload, len, DEFAULT_RECV_TIMEOUT_MS);
> }
> 
> /* Test timeout after not resuming hold */
> TEST_F(can_env, test_hold_timeout)
> {
> 	struct can_frame tx_frame = {
> 		.can_id = RECEIVER_TP_CM_ID,
> 		.len = 8,
> 	};
> 	struct timespec start, end;
> 	long elapsed_ms;
> 	int res;
> 
> 	memcpy(tx_frame.data, HOLD_PAYLOAD, sizeof(HOLD_PAYLOAD));
> 	res = send(self->j1939_sock, J1939_PAYLOAD, sizeof(J1939_PAYLOAD), 0);
> 	ASSERT_GT(res, 0)
> 		TH_LOG("failed to send j1939 payload: %d", errno);
> 
> 	res = recv_payload(self->raw_sock, RTS_PAYLOAD, sizeof(RTS_PAYLOAD));
> 	ASSERT_EQ(res, 0)
> 		TH_LOG("Failed to receive RTS as expected");
> 
> 	res = send(self->raw_sock, &tx_frame, sizeof(tx_frame), 0);
> 	ASSERT_GT(res, 0)
> 		TH_LOG("failed to send hold with raw sock: %d", errno);
> 
> 	/* Record start time */
> 	clock_gettime(CLOCK_MONOTONIC, &start);
> 
> 	/*
> 	 * Receive with a timeout larger than the expected 1050ms J1939 timeout.
> 	 * 2000ms provides plenty of headroom for CI without hanging indefinitely.
> 	 */
> 	res = recv_payload_timeout(self->raw_sock, ABORT_TIMEOUT_PAYLOAD,
> 				   sizeof(ABORT_TIMEOUT_PAYLOAD), 2000);
> 	
> 	ASSERT_EQ(res, 0)
> 		TH_LOG("Failed to receive abort as expected");
> 
> 	/* Record end time and calculate elapsed milliseconds */
> 	clock_gettime(CLOCK_MONOTONIC, &end);
> 	elapsed_ms = (end.tv_sec - start.tv_sec) * 1000 +
> 		     (end.tv_nsec - start.tv_nsec) / 1000000;
> 
> 	/*
> 	 * The actual timeout is 1050ms. We define an acceptable window
> 	 * to account for CI scheduling variations.
> 	 */
> 	ASSERT_GE(elapsed_ms, 1000)
> 		TH_LOG("Abort received too early: %ld ms", elapsed_ms);
> 	ASSERT_LE(elapsed_ms, 1500)
> 		TH_LOG("Abort received too late: %ld ms", elapsed_ms);
> }
> 
> 
Perfect, thank you!



      reply	other threads:[~2026-05-17 20:58 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-05-16 14:35 Alexander Hölzl
2026-05-17  7:46 ` Oleksij Rempel
2026-05-17 20:57   ` Alexander Hölzl [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=21dbdcce-1355-411b-8be0-32a0dd773853@gmx.net \
    --to=alexander.hoelzl@gmx.net \
    --cc=kernel@pengutronix.de \
    --cc=linux-can@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=o.rempel@pengutronix.de \
    --cc=robin@protonic.nl \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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®