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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.