Netdev List
 help / color / mirror / Atom feed
From: netdev-bot+sashiko@kernel.org
To: jiayuan.chen@linux.dev
Cc: netdev@vger.kernel.org, andrew+netdev@lunn.ch,
	davem@davemloft.net, edumazet@google.com, kuba@kernel.org,
	pabeni@redhat.com, shuah@kernel.org,
	linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org
Subject: Re: [PATCH net-next v1 2/2] selftests: netdevsim: add a kTLS device offload test
Date: Fri, 04 Sep 2026 22:25:38 +0000	[thread overview]
Message-ID: <178856073881.219967.7291257459649538794@kernel.org> (raw)
In-Reply-To: <20260901154914.11637-3-jiayuan.chen@linux.dev>

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

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

- [Medium] The `run_pair nosample` run (offload features turned off) has
  no rendezvous between the two helpers: both are expected to abort…
- [Medium] The payload verification for splice fragments generates
  identical data for all fragments, which silently defeats the test's…
- [Low] tls.sh reuses the global, fixed network-namespace names
  `nssv`/`nscl` and exactly the same random netdevsim device-ID ranges…
- [Low] Two assertions inspect only one of the two ports/namespaces even
  though their messages imply both: (a) the `tls-hw-tx-offload`/`tls-hw-…
- [Low] The "per-port debugfs tls file exists" assertion is dead code: a
  few lines earlier the script already does `if [ !

Pre-existing issues:
- [High] The new test is the first in-tree user of setsockopt(SOL_TLS,
  TLS_TX_MAX_PAYLOAD_LEN) on a device-offloaded (TLS_HW) socket, and it…

--

selftests: netdevsim: add a kTLS device offload test

Adds tls.sh plus the tls_offload helper under
tools/testing/selftests/drivers/net/netdevsim/, running kTLS over a linked
netdevsim pair with one port per netns.  The helper asserts via
/proc/net/tls_stat that both directions landed on the device path, then
exercises bulk transfers, MSG_MORE writes, splice() with TLS_TX_ZEROCOPY_RO
and a minimal record limit, plus the ethtool off/on path.

A few questions about the new test below.

> diff --git a/tools/testing/selftests/drivers/net/netdevsim/tls.sh b/tools/testing/selftests/drivers/net/netdevsim/tls.sh
> new file mode 100755
> index 0000000000000..59b7d1c0e5148
> --- /dev/null
> +++ b/tools/testing/selftests/drivers/net/netdevsim/tls.sh
> @@ -0,0 +1,334 @@
> +#!/bin/bash
> +# SPDX-License-Identifier: GPL-2.0-only
> +#
> +# Exercise netdevsim's emulated kTLS device offload over a linked
> +# netdevsim pair, one port per network namespace.
> +#
> +# shellcheck disable=SC2154 # ksft_skip comes from lib.sh
> +
> +lib_dir=$(dirname "$0")
> +# shellcheck source=./../../../net/lib.sh
> +# shellcheck disable=SC1091
> +source "$lib_dir"/../../../net/lib.sh
> +
> +# Device IDs and the nssv/nscl namespaces are picked the same way as peer.sh.
> +NSIM_DEV_1_ID=$((256 + RANDOM % 256))
> +NSIM_DEV_1_SYS=/sys/bus/netdevsim/devices/netdevsim$NSIM_DEV_1_ID
> +NSIM_DEV_2_ID=$((512 + RANDOM % 256))
> +NSIM_DEV_2_SYS=/sys/bus/netdevsim/devices/netdevsim$NSIM_DEV_2_ID

[ ... ]

> +# Each resource is handed to defer only once it exists, so a run that loses a
> +# race for one of the global names does not tear down the winner's.
> +setup()
> +{
> +	set -e
> +
> +	echo "$NSIM_DEV_1_ID" > "$NSIM_DEV_SYS_NEW"
> +	defer nsim_dev_del "$NSIM_DEV_1_ID"
> +	echo "$NSIM_DEV_2_ID" > "$NSIM_DEV_SYS_NEW"
> +	defer nsim_dev_del "$NSIM_DEV_2_ID"

[ ... ]

> +	ip netns add nssv
> +	defer ip netns del nssv
> +	ip netns add nscl
> +	defer ip netns del nscl

[Severity: Low]
The fixed names nssv and nscl, and the device ID ranges $((256 + RANDOM %
256)) / $((512 + RANDOM % 256)), are exactly the ones peer.sh in the same
directory already uses.

What happens when a stale nssv or nscl namespace is left behind by an
aborted peer.sh or tls.sh run, or when peer.sh runs concurrently?  setup()
runs under set -e, so ip netns add nssv (or the new_device write) failing
aborts the script mid-setup with a raw shell error and no PASS, FAIL or
SKIP line at all.

Would setup_ns() from the already sourced tools/testing/selftests/net/lib.sh
be preferable here, since it generates per-run unique namespace names?

[ ... ]

> +run_pair()
> +{
> +	local sample="${1:-sample}"
> +	local srv_rc cli_rc ready=1

[ ... ]

> +	if [ "$sample" = "sample" ]; then
> +		# Comfortably longer than the helpers take to get to the
> +		# barrier.  Timing out here means a sample would be stale, so
> +		# fail the run rather than assert on it.
> +		slowwait "$READY_TIMEOUT_SEC" both_ready >/dev/null || ready=0
> +		if [ "$ready" -eq 1 ]; then
> +			CONNS_1=$(dbg_field "$NSIM_DEV_1_TLS" count)
> +			CONNS_2=$(dbg_field "$NSIM_DEV_2_TLS" count)
> +			: "${CONNS_1:=0}"
> +			: "${CONNS_2:=0}"
> +		fi
> +	fi
> +	touch "$SYNCDIR/go"

[ ... ]

> +# The offload has to be advertised, and on by default like the other
> +# netdevsim crypto offloads.
> +for f in tls-hw-tx-offload tls-hw-rx-offload; do
> +	[ "$(feature nssv "$NSIM_DEV_1_NAME" "$f")" = "on" ]
> +	check "$f advertised and on by default" $?
> +done

[Severity: Low]
This loop only queries feature nssv "$NSIM_DEV_1_NAME", but the message says
the feature is advertised and on by default without qualifying which port.
Should the client port in nscl be checked too, the way set_features() does?

The same one-sided pattern shows up later for the current-context counters:

	[ "$(tls_stat nssv TlsCurrTxDevice)" -eq 0 ] && \
		[ "$(tls_stat nssv TlsCurrRxDevice)" -eq 0 ]
	check "no device contexts left behind" $?

/proc/net/tls_stat is per netns, and enable_ktls() installs TX and RX
contexts on both ends, so would a context left behind on the client port in
nscl pass unnoticed here?

> +
> +[ -e "$NSIM_DEV_1_TLS" ]
> +check "per-port debugfs tls file exists" $?

[Severity: Low]
Is this assertion reachable in the failing state?  A few lines earlier the
script already does:

	if [ ! -e "$NSIM_DEV_1_TLS" ]; then
		echo "SKIP: netdevsim built without TLS device offload (CONFIG_TLS_DEVICE=n)"
		exit "$ksft_skip"
	fi

so by the time [ -e "$NSIM_DEV_1_TLS" ] runs it is always true, and the
check only adds to num_pass.

Meanwhile $NSIM_DEV_2_TLS is never checked for existence, although
all_ctx_released() and the tx_packets/rx_packets assertions parse it.  If
that file is missing or a field name changes, dbg_field() returns an empty
string and [ "" -eq 0 ] / [ "" -ge 1 ] emit "integer expression expected"
rather than a clean FAIL.  Should the existence check cover port 2 instead?

[ ... ]

> +[ "$(tls_stat nssv TlsCurrTxDevice)" -eq 0 ] && \
> +	[ "$(tls_stat nssv TlsCurrRxDevice)" -eq 0 ]
> +check "no device contexts left behind" $?

[ ... ]

> +# Turning the features off has to make the offload refuse the connection;
> +# the test binary insists on the device path, so it must now fail.
> +set_features off
> +check "both offload features turned off on both ports" $?
> +
> +run_pair nosample
> +rc=$?
> +[ "$rc" -ne 0 ]
> +check "offload declined once the feature is off" $?
> +
> +# Both directions on both ends have to have landed on the software path.
> +[ "$(tls_stat nssv TlsTxSw)" -ge 1 ] && [ "$(tls_stat nssv TlsRxSw)" -ge 1 ] && \
> +	[ "$(tls_stat nscl TlsTxSw)" -ge 1 ] && \
> +	[ "$(tls_stat nscl TlsRxSw)" -ge 1 ]
> +check "software path used when offload is off" $?

[Severity: Medium]
Can this assertion fail intermittently?  In nosample mode run_pair() skips
the slowwait/both_ready barrier entirely, and neither helper reaches
rendezvous() because enable_ktls() exits first:

tools/testing/selftests/drivers/net/netdevsim/tls_offload.c:enable_ktls() {
	if (read_tls_stat("TlsTxDevice") != tx_before + 1)
		fail("TX did not land on the device path (TlsTxDevice %lu -> %lu)",
	...
}

So whichever end aborts first closes its socket and sends a FIN.  If the
peer processes that FIN before it gets to setsockopt(TCP_ULP, "tls"):

net/tls/tls_main.c:tls_init() {
	...
	if (sk->sk_state != TCP_ESTABLISHED)
		return -ENOTCONN;
	...
}

the peer dies in die("setsockopt(TCP_ULP, tls)") and never installs software
kTLS, so TlsTxSw/TlsRxSw in that namespace stay at 0 and this check fails.
Note the first run already asserted TlsTxSw == 0 in both namespaces, so
earlier traffic cannot cover for it.

"offload declined once the feature is off" still passes in that case since
it only requires a non-zero exit status, so the failure reason is hidden.
Would a rendezvous (or a retry) before the helpers give up make this run
deterministic?

> diff --git a/tools/testing/selftests/drivers/net/netdevsim/tls_offload.c b/tools/testing/selftests/drivers/net/netdevsim/tls_offload.c
> new file mode 100644
> index 0000000000000..0d7c8950ca6ce
> --- /dev/null
> +++ b/tools/testing/selftests/drivers/net/netdevsim/tls_offload.c
> @@ -0,0 +1,505 @@

[ ... ]

> +/* A record can be up to 16K, so normally one segment carries a piece of a
> + * single record.  Shrinking the limit puts a dozen or so whole records in
> + * every segment instead, which is the multi-record path through the driver.
> + */
> +static void send_small_records(int fd, unsigned int seed)
> +{
> +	char buf[SMALL_LEN];
> +	uint16_t limit;
> +
> +	limit = REC_LIM_MIN;
> +	if (setsockopt(fd, SOL_TLS, TLS_TX_MAX_PAYLOAD_LEN, &limit,
> +		       sizeof(limit)))
> +		die("setsockopt(TLS_TX_MAX_PAYLOAD_LEN)");

[Severity: High]
This isn't a bug introduced by this patch, but it looks like the first
in-tree caller of TLS_TX_MAX_PAYLOAD_LEN on a socket that the test has just
asserted is device offloaded (tx_conf == TLS_HW), and the kernel handler
looks type confused:

net/tls/tls_main.c:do_tls_setsockopt_tx_payload_len() {
	struct tls_sw_context_tx *sw_ctx = tls_sw_ctx_tx(ctx);
	...
	if (sw_ctx && sw_ctx->open_rec)
		return -EBUSY;
	...
}

include/net/tls.h:tls_sw_ctx_tx() is an unchecked cast:

	return (struct tls_sw_context_tx *)tls_ctx->priv_ctx_tx;

and for an offloaded socket net/tls/tls_device.c:tls_set_device_offload()
stores a struct tls_offload_context_tx there:

	ctx->priv_ctx_tx = offload_ctx;

There is no tx_conf == TLS_SW test, so the ->open_rec read lands inside the
offload context's sg_tx_data[MAX_SKB_FRAGS], which tls_push_record() fills
for every offloaded record:

net/tls/tls_device.c:tls_push_record() {
	sg_unmark_end(&offload_ctx->sg_tx_data[i]);
	sg_set_page(&offload_ctx->sg_tx_data[i], skb_frag_page(frag),
		    skb_frag_size(frag), skb_frag_off(frag));
	...
}

Two things follow, and which one happens depends on the struct layout:

If the aliased bytes are non-zero (page_link/offset/length, e.g. once the
offsets shift with CONFIG_DEBUG_SPINLOCK or LOCKDEP changing the
scatterlist stride), setsockopt returns a spurious -EBUSY.  That is fatal
for this helper, since the call above goes straight to
die("setsockopt(TLS_TX_MAX_PAYLOAD_LEN)") after the bulk, MSG_MORE and
splice traffic has already populated sg_tx_data.

If they are zero (default x86_64 layout, where the read lands on
sg_tx_data[n].dma_address, which tls_device.c never writes), the intended
guard never fires, because tls_offload_context_tx->open_record is never
consulted.  The limit can then be shrunk while an offloaded record is open,
and the next send hits:

net/tls/tls_device.c:tls_push_data() {
	...
	copy = min_t(size_t, size, max_open_record_len - record->len);
	...
}

with max_open_record_len < record->len, i.e. a u32 underflow that makes copy
== size and lets the record grow past the requested limit and past the 2^14
payload ceiling.

Should the guard check ctx->tx_conf == TLS_SW (and consult
tls_offload_ctx_tx(ctx)->open_record for the TLS_HW case) before the test
starts relying on this option on offloaded sockets?

> +
> +	fill_pattern(buf, sizeof(buf), seed);
> +	write_all(fd, buf, sizeof(buf));

[ ... ]

> @@ -0,0 +1,505 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * kTLS device offload data path exercise, driven by tls.sh.
> + *
> + * One instance runs as the server and one as the client, each in its own
> + * network namespace, connected back to back by a linked netdevsim pair.
> + * Both ends enable kTLS and rely on netdevsim's emulated TLS offload, so
> + * every record travels through net/tls/tls_device.c rather than the
> + * software path.
> + *
> + * The two processes rendezvous through a shared directory so that neither
> + * side sends before the other has installed its RX offload.
> + */
> +
> +#define _GNU_SOURCE
> +
> +#include <arpa/inet.h>
> +#include <errno.h>
> +#include <fcntl.h>
> +#include <limits.h>
> +#include <netinet/in.h>
> +#include <netinet/tcp.h>
> +#include <stdarg.h>
> +#include <stdbool.h>
> +#include <stdint.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <sys/socket.h>
> +#include <sys/stat.h>
> +#include <sys/types.h>
> +#include <time.h>
> +#include <unistd.h>
> +
> +#include <linux/tls.h>
> +
> +#ifndef SOL_TLS
> +#define SOL_TLS			282
> +#endif
> +
> +#ifndef TCP_ULP
> +#define TCP_ULP			31
> +#endif
> +
> +#define BULK_LEN		(200 * 1024)
> +#define MORE_FRAGS		64
> +#define SPLICE_FRAG_LEN		4096
> +#define SPLICE_FRAGS		8
> +
> +/* TLS_MIN_RECORD_SIZE_LIM and TLS_MAX_PAYLOAD_SIZE, which are not uapi. */
> +#define REC_LIM_MIN		64
> +#define REC_LIM_MAX		16384
> +
> +#define SMALL_RECS		100
> +#define SMALL_LEN		(REC_LIM_MIN * SMALL_RECS)
> +
> +#define SYNC_TIMEOUT_MS		20000
> +#define CONNECT_TIMEOUT_MS	20000
> +
> +/* accept() and the transfers have no timeout of their own, and tls.sh waits
> + * on both helpers.  Bound the whole run so a stuck peer cannot hang the test.
> + */
> +#define RUN_TIMEOUT_SEC		120
> +
> +static const char *role;
> +
> +static void die(const char *what)
> +{
> +	fprintf(stderr, "%s: %s: %s\n", role, what, strerror(errno));
> +	exit(1);
> +}
> +
> +static void fail(const char *fmt, ...)
> +{
> +	va_list ap;
> +
> +	fprintf(stderr, "%s: ", role);
> +	va_start(ap, fmt);
> +	vfprintf(stderr, fmt, ap);
> +	va_end(ap);
> +	fprintf(stderr, "\n");
> +	exit(1);
> +}
> +
> +static void msleep(unsigned int ms)
> +{
> +	struct timespec ts = {
> +		.tv_sec = ms / 1000,
> +		.tv_nsec = (ms % 1000) * 1000000L,
> +	};
> +
> +	nanosleep(&ts, NULL);
> +}
> +
> +/* /proc/net/tls_stat is per netns, so both ends can check that their own
> + * connection really landed on the device path.
> + */
> +static unsigned long read_tls_stat(const char *name)
> +{
> +	char line[256];
> +	unsigned long val;
> +	FILE *f;
> +
> +	f = fopen("/proc/net/tls_stat", "r");
> +	if (!f)
> +		die("open /proc/net/tls_stat");
> +
> +	while (fgets(line, sizeof(line), f)) {
> +		char key[64];
> +
> +		if (sscanf(line, "%63s %lu", key, &val) != 2)
> +			continue;
> +		if (!strcmp(key, name)) {
> +			fclose(f);
> +			return val;
> +		}
> +	}
> +
> +	fclose(f);
> +	fail("%s not found in /proc/net/tls_stat", name);
> +	return 0;
> +}
> +
> +static void fill_pattern(char *buf, size_t len, unsigned int seed)
> +{
> +	size_t i;
> +
> +	for (i = 0; i < len; i++)
> +		buf[i] = (char)(seed + i * 31 + (i >> 8) * 7);
> +}
> +
> +static void check_pattern(const char *buf, size_t len, unsigned int seed,
> +			  const char *what)
> +{
> +	char *want = malloc(len);
> +	size_t i;
> +
> +	if (!want)
> +		die("malloc");
> +
> +	fill_pattern(want, len, seed);
> +	for (i = 0; i < len; i++) {
> +		if (buf[i] != want[i])
> +			fail("%s: payload mismatch at byte %zu: got 0x%02x want 0x%02x",
> +			     what, i, (unsigned char)buf[i],
> +			     (unsigned char)want[i]);
> +	}
> +
> +	free(want);
> +}
> +
> +static void write_all(int fd, const char *buf, size_t len)
> +{
> +	size_t done = 0;
> +
> +	while (done < len) {
> +		ssize_t n = send(fd, buf + done, len - done, 0);
> +
> +		if (n < 0) {
> +			if (errno == EINTR)
> +				continue;
> +			die("send");
> +		}
> +		done += n;
> +	}
> +}
> +
> +static void read_all(int fd, char *buf, size_t len)
> +{
> +	size_t done = 0;
> +
> +	while (done < len) {
> +		ssize_t n = recv(fd, buf + done, len - done, 0);
> +
> +		if (n < 0) {
> +			if (errno == EINTR)
> +				continue;
> +			die("recv");
> +		}
> +		if (n == 0)
> +			fail("peer closed after %zu of %zu bytes", done, len);
> +		done += n;
> +	}
> +}
> +
> +static void enable_ktls(int fd)
> +{
> +	struct tls12_crypto_info_aes_gcm_128 ci = {};
> +	unsigned long tx_before, rx_before;
> +
> +	tx_before = read_tls_stat("TlsTxDevice");
> +	rx_before = read_tls_stat("TlsRxDevice");
> +
> +	if (setsockopt(fd, IPPROTO_TCP, TCP_ULP, "tls", sizeof("tls")))
> +		die("setsockopt(TCP_ULP, tls)");
> +
> +	ci.info.version = TLS_1_2_VERSION;
> +	ci.info.cipher_type = TLS_CIPHER_AES_GCM_128;
> +	memset(ci.iv, 'i', sizeof(ci.iv));
> +	memset(ci.key, 'k', sizeof(ci.key));
> +	memset(ci.salt, 's', sizeof(ci.salt));
> +	memset(ci.rec_seq, 0, sizeof(ci.rec_seq));
> +
> +	if (setsockopt(fd, SOL_TLS, TLS_TX, &ci, sizeof(ci)))
> +		die("setsockopt(TLS_TX)");
> +	if (setsockopt(fd, SOL_TLS, TLS_RX, &ci, sizeof(ci)))
> +		die("setsockopt(TLS_RX)");
> +
> +	/* The whole point of the exercise: refuse to silently fall back to
> +	 * the software path, otherwise the test would pass without ever
> +	 * touching tls_device.c.
> +	 */
> +	if (read_tls_stat("TlsTxDevice") != tx_before + 1)
> +		fail("TX did not land on the device path (TlsTxDevice %lu -> %lu)",
> +		     tx_before, read_tls_stat("TlsTxDevice"));
> +	if (read_tls_stat("TlsRxDevice") != rx_before + 1)
> +		fail("RX did not land on the device path (TlsRxDevice %lu -> %lu)",
> +		     rx_before, read_tls_stat("TlsRxDevice"));
> +}
> +
> +static void sync_path(char *out, size_t len, const char *dir, const char *who)
> +{
> +	if ((size_t)snprintf(out, len, "%s/%s.ready", dir, who) >= len)
> +		fail("sync dir path too long");
> +}
> +
> +static void rendezvous(const char *dir, const char *me, const char *peer)
> +{
> +	char mine[PATH_MAX], theirs[PATH_MAX];
> +	unsigned int waited = 0;
> +	int fd;
> +
> +	sync_path(mine, sizeof(mine), dir, me);
> +	sync_path(theirs, sizeof(theirs), dir, peer);
> +
> +	fd = open(mine, O_CREAT | O_WRONLY, 0600);
> +	if (fd < 0)
> +		die("create sync file");
> +	close(fd);
> +
> +	while (access(theirs, F_OK)) {
> +		if (waited >= SYNC_TIMEOUT_MS)
> +			fail("timed out waiting for %s", peer);
> +		msleep(20);
> +		waited += 20;
> +	}
> +}
> +
> +/* Both ends stop here with their offload installed and no data sent yet,
> + * so that the driver state can be inspected from the outside.
> + */
> +static void wait_for_go(const char *dir)
> +{
> +	unsigned int waited = 0;
> +	char go[PATH_MAX];
> +
> +	if ((size_t)snprintf(go, sizeof(go), "%s/go", dir) >= sizeof(go))
> +		fail("sync dir path too long");
> +
> +	while (access(go, F_OK)) {
> +		if (waited >= SYNC_TIMEOUT_MS)
> +			fail("timed out waiting for go");
> +		msleep(20);
> +		waited += 20;
> +	}
> +}
> +
> +/* Small writes with MSG_MORE accumulate into one open record before it is
> + * pushed, which is the interesting part of tls_push_data().
> + */
> +static void send_msg_more(int fd, unsigned int seed)
> +{
> +	char buf[MORE_FRAGS + 1];
> +	int i;
> +
> +	fill_pattern(buf, sizeof(buf), seed);
> +
> +	for (i = 0; i < MORE_FRAGS; i++) {
> +		if (send(fd, buf + i, 1, MSG_MORE) != 1)
> +			die("send(MSG_MORE)");
> +	}
> +	if (send(fd, buf + MORE_FRAGS, 1, 0) != 1)
> +		die("send(last)");
> +}
> +
> +/* splice() reaches tls_push_data() with MSG_SPLICE_PAGES once
> + * TLS_TX_ZEROCOPY_RO is enabled, which is a distinct fragment path.
> + */
> +static void send_splice(int fd, unsigned int seed)
> +{
> +	char buf[SPLICE_FRAG_LEN];
> +	int val = 1;
> +	int i;
> +
> +	if (setsockopt(fd, SOL_TLS, TLS_TX_ZEROCOPY_RO, &val, sizeof(val)))
> +		die("setsockopt(TLS_TX_ZEROCOPY_RO)");
> +
> +	for (i = 0; i < SPLICE_FRAGS; i++) {
> +		int p[2];
> +
> +		fill_pattern(buf, sizeof(buf), seed + i * SPLICE_FRAG_LEN);
> +
> +		if (pipe(p))
> +			die("pipe");
> +		if (write(p[1], buf, sizeof(buf)) != sizeof(buf))
> +			die("write to pipe");
> +		if (splice(p[0], NULL, fd, NULL, sizeof(buf),
> +			   i == SPLICE_FRAGS - 1 ? 0 : SPLICE_F_MORE) !=
> +		    sizeof(buf))
> +			die("splice");
> +		close(p[0]);
> +		close(p[1]);
> +	}
> +
> +	val = 0;
> +	if (setsockopt(fd, SOL_TLS, TLS_TX_ZEROCOPY_RO, &val, sizeof(val)))
> +		die("setsockopt(TLS_TX_ZEROCOPY_RO off)");
> +}
> +
> +/* A record can be up to 16K, so normally one segment carries a piece of a
> + * single record.  Shrinking the limit puts a dozen or so whole records in
> + * every segment instead, which is the multi-record path through the driver.
> + */
> +static void send_small_records(int fd, unsigned int seed)
> +{
> +	char buf[SMALL_LEN];
> +	uint16_t limit;
> +
> +	limit = REC_LIM_MIN;
> +	if (setsockopt(fd, SOL_TLS, TLS_TX_MAX_PAYLOAD_LEN, &limit,
> +		       sizeof(limit)))
> +		die("setsockopt(TLS_TX_MAX_PAYLOAD_LEN)");
> +
> +	fill_pattern(buf, sizeof(buf), seed);
> +	write_all(fd, buf, sizeof(buf));
> +
> +	limit = REC_LIM_MAX;
> +	if (setsockopt(fd, SOL_TLS, TLS_TX_MAX_PAYLOAD_LEN, &limit,
> +		       sizeof(limit)))
> +		die("setsockopt(TLS_TX_MAX_PAYLOAD_LEN restore)");
> +}
> +
> +#define SEED_C2S_BULK	0x11
> +#define SEED_S2C_BULK	0x22
> +#define SEED_C2S_MORE	0x33
> +#define SEED_C2S_SPLICE	0x44
> +#define SEED_C2S_SMALL	0x55
> +
> +static void run_client(int fd)
> +{
> +	char *buf = malloc(BULK_LEN);
> +
> +	if (!buf)
> +		die("malloc");
> +
> +	fill_pattern(buf, BULK_LEN, SEED_C2S_BULK);
> +	write_all(fd, buf, BULK_LEN);
> +
> +	read_all(fd, buf, BULK_LEN);
> +	check_pattern(buf, BULK_LEN, SEED_S2C_BULK, "server -> client bulk");
> +
> +	send_msg_more(fd, SEED_C2S_MORE);
> +	send_splice(fd, SEED_C2S_SPLICE);
> +	send_small_records(fd, SEED_C2S_SMALL);
> +
> +	/* Wait for the server's verdict before tearing anything down. */
> +	read_all(fd, buf, 1);
> +	if (buf[0] != 'k')
> +		fail("server reported a failure");
> +
> +	free(buf);
> +}
> +
> +static void run_server(int fd)
> +{
> +	size_t splice_len = (size_t)SPLICE_FRAG_LEN * SPLICE_FRAGS;
> +	char *buf = malloc(BULK_LEN);
> +	char more[MORE_FRAGS + 1];
> +	char *sbuf;
> +	char ok = 'k';
> +	int i;
> +
> +	sbuf = malloc(splice_len);
> +	if (!buf || !sbuf)
> +		die("malloc");
> +
> +	read_all(fd, buf, BULK_LEN);
> +	check_pattern(buf, BULK_LEN, SEED_C2S_BULK, "client -> server bulk");
> +
> +	fill_pattern(buf, BULK_LEN, SEED_S2C_BULK);
> +	write_all(fd, buf, BULK_LEN);
> +
> +	read_all(fd, more, sizeof(more));
> +	check_pattern(more, sizeof(more), SEED_C2S_MORE, "client -> server MSG_MORE");
> +
> +	read_all(fd, sbuf, splice_len);
> +	for (i = 0; i < SPLICE_FRAGS; i++)
> +		check_pattern(sbuf + (size_t)i * SPLICE_FRAG_LEN,
> +			      SPLICE_FRAG_LEN, SEED_C2S_SPLICE +
> +			      i * SPLICE_FRAG_LEN, "client -> server splice");
> +
> +	read_all(fd, buf, SMALL_LEN);
> +	check_pattern(buf, SMALL_LEN, SEED_C2S_SMALL,
> +		      "client -> server small records");
> +
> +	write_all(fd, &ok, 1);
> +
> +	free(sbuf);
> +	free(buf);
> +}
> +
> +static int do_server(const char *ip, int port, const char *syncdir)
> +{
> +	struct sockaddr_in sa = {};
> +	int lfd, fd, one = 1;
> +
> +	lfd = socket(AF_INET, SOCK_STREAM, 0);
> +	if (lfd < 0)
> +		die("socket");
> +	if (setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)))
> +		die("SO_REUSEADDR");
> +
> +	sa.sin_family = AF_INET;
> +	sa.sin_port = htons(port);
> +	if (inet_pton(AF_INET, ip, &sa.sin_addr) != 1)
> +		fail("bad bind address %s", ip);
> +
> +	if (bind(lfd, (struct sockaddr *)&sa, sizeof(sa)))
> +		die("bind");
> +	if (listen(lfd, 1))
> +		die("listen");
> +
> +	fd = accept(lfd, NULL, NULL);
> +	if (fd < 0)
> +		die("accept");
> +	close(lfd);
> +
> +	enable_ktls(fd);
> +	rendezvous(syncdir, "server", "client");
> +	wait_for_go(syncdir);
> +
> +	run_server(fd);
> +
> +	close(fd);
> +	return 0;
> +}
> +
> +static int do_client(const char *ip, int port, const char *syncdir)
> +{
> +	struct sockaddr_in sa = {};
> +	unsigned int waited = 0;
> +	int fd;
> +
> +	sa.sin_family = AF_INET;
> +	sa.sin_port = htons(port);
> +	if (inet_pton(AF_INET, ip, &sa.sin_addr) != 1)
> +		fail("bad server address %s", ip);
> +
> +	for (;;) {
> +		fd = socket(AF_INET, SOCK_STREAM, 0);
> +		if (fd < 0)
> +			die("socket");
> +		if (!connect(fd, (struct sockaddr *)&sa, sizeof(sa)))
> +			break;
> +		close(fd);
> +		if (waited >= CONNECT_TIMEOUT_MS)
> +			die("connect");
> +		msleep(20);
> +		waited += 20;
> +	}
> +
> +	enable_ktls(fd);
> +	rendezvous(syncdir, "client", "server");
> +	wait_for_go(syncdir);
> +
> +	run_client(fd);
> +
> +	close(fd);
> +	return 0;
> +}
> +
> +int main(int argc, char **argv)
> +{
> +	int port;
> +
> +	if (argc != 5) {
> +		fprintf(stderr,
> +			"usage: %s server|client <ip> <port> <syncdir>\n",
> +			argv[0]);
> +		return 2;
> +	}
> +
> +	role = argv[1];
> +	port = atoi(argv[3]);
> +
> +	alarm(RUN_TIMEOUT_SEC);
> +
> +	if (!strcmp(role, "server"))
> +		return do_server(argv[2], port, argv[4]);
> +	if (!strcmp(role, "client"))
> +		return do_client(argv[2], port, argv[4]);
> +
> +	fprintf(stderr, "unknown role %s\n", role);
> +	return 2;
> +}

[Severity: Medium]
Does the per-fragment seed here actually produce distinct payloads?
fill_pattern() truncates every byte to 8 bits:

	buf[i] = (char)(seed + i * 31 + (i >> 8) * 7);

and SPLICE_FRAG_LEN is 4096, a multiple of 256, so seed + i *
SPLICE_FRAG_LEN only ever differs from seed in bits 8 and above.  All
SPLICE_FRAGS fragments therefore get byte-for-byte identical contents,
and the matching loop in run_server()

	check_pattern(sbuf + (size_t)i * SPLICE_FRAG_LEN,
		      SPLICE_FRAG_LEN, SEED_C2S_SPLICE +
		      i * SPLICE_FRAG_LEN, "client -> server splice");

collapses to the same expected buffer for each i.

That is not a correctness problem for the transfer itself, but it does
remove most of what the splice case is supposed to prove: with every
fragment identical, reordering, duplication or dropping-and-refilling a
fragment inside the TLS_TX_ZEROCOPY_RO / MSG_SPLICE_PAGES path still
compares equal and the test passes.  Given that the whole point of this
hunk is to exercise a distinct fragment path in tls_push_data(), it
seems worth having the payload distinguish the fragments.

Would a seed step that survives the truncation be preferable here, e.g.
seed + i rather than seed + i * SPLICE_FRAG_LEN (with the same change on
the server side), or should fill_pattern() fold the seed's high bits into
the byte value so that any seed spacing works?  Note the other cases are
unaffected, since SEED_C2S_BULK/SEED_S2C_BULK/SEED_C2S_MORE/SEED_C2S_SMALL
all differ in the low 8 bits.

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260901154914.11637-1-jiayuan.chen%40linux.dev

  reply	other threads:[~2026-09-04 22:25 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-01 15:48 [PATCH net-next v1 0/2] netdevsim: add TLS device offload emulation and test Jiayuan Chen
2026-09-01 15:48 ` [PATCH net-next v1 1/2] netdevsim: add TLS device offload emulation Jiayuan Chen
2026-09-01 23:38   ` Daniel Zahka
2026-09-04 22:25   ` netdev-bot+sashiko
2026-09-01 15:48 ` [PATCH net-next v1 2/2] selftests: netdevsim: add a kTLS device offload test Jiayuan Chen
2026-09-04 22:25   ` netdev-bot+sashiko [this message]
2026-09-03 22:59 ` [PATCH net-next v1 0/2] netdevsim: add TLS device offload emulation and test Jakub Kicinski
2026-09-04  2:09   ` Jiayuan Chen

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=178856073881.219967.7291257459649538794@kernel.org \
    --to=netdev-bot+sashiko@kernel.org \
    --cc=andrew+netdev@lunn.ch \
    --cc=davem@davemloft.net \
    --cc=edumazet@google.com \
    --cc=jiayuan.chen@linux.dev \
    --cc=kuba@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=shuah@kernel.org \
    /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