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
next prev parent 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