Re: [PATCH net] net/smc: do not credit bytes that splice() did not take

Simon Horman <[email protected]>
Newsgroups org.kernel.vger.linux-rdma,org.kernel.vger.linux-s390,org.kernel.vger.netdev
Message-ID <[email protected]>
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
Full review at: https://netdev-ai.bots.linux.dev/sashiko/#/patchset/[email protected]?part=1
---
net/smc: do not credit bytes that splice() did not take

The patch adds a short-count path to the chunk loop in smc_rx_recvmsg()
so that only the number of bytes splice_to_pipe() actually accepted is
credited to copylen, read_remaining and read_done.  It also adds a
zero-count path that returns -EAGAIN when nothing was taken.

>     A pipe that is not empty is enough to hit this; SPLICE_F_NONBLOCK on a
>     full pipe produces the zero case.

[Severity: Low]
Is "A pipe that is not empty is enough to hit this" accurate?  A non-zero
short return from splice_to_pipe() requires smc_rx_splice() to submit more
than one page:

net/smc/smc_rx.c:smc_rx_splice() {
	nr_pages = !lgr->is_smcd && smc->conn.rmb_desc->is_vm ?
		   PAGE_ALIGN(len + offset) / PAGE_SIZE : 1;

For SMC-D and for SMC-R with physically contiguous RMBs nr_pages is 1 and
the single pipe_buffer carries partial[0].len == len, so the transfer is
all-or-nothing.  Together with wait_for_space() in splice_file_to_pipe()
guaranteeing at least one free slot on entry, doesn't the short-count case
only occur for SMC-R with is_vm RMBs?

Would it help to name that configuration in the changelog so backporters
know what to test?

> diff --git a/net/smc/smc_rx.c b/net/smc/smc_rx.c
> index 5c9e4d8b57de4..d951dbe8eec3c 100644
> --- a/net/smc/smc_rx.c
> +++ b/net/smc/smc_rx.c
> @@ -488,6 +488,24 @@ int smc_rx_recvmsg(struct smc_sock *smc, struct msghdr *msg,
>  						read_done = -EFAULT;
>  					goto out;
>  				}

[Severity: High]
This isn't a bug introduced by this patch, but since the patch reworks
exactly this accounting: does this pre-existing exit leak bytes_to_rcv on
a wrapped read?

For a read that wraps the ring, chunk 0 can succeed and chunk 1 fail.
smc_rx_splice() has already committed chunk 0:

net/smc/smc_rx.c:smc_rx_splice() {
	bytes = splice_to_pipe(pipe, &spd);
	if (bytes > 0)
		atomic_add(bytes, &smc->conn.splice_pending);

and the queued buffers advance the consumer cursor when the reader drains
them:

net/smc/smc_rx.c:smc_rx_pipe_buf_release() {
	lock_sock(sk);
	smc_rx_update_cons(smc, priv->len);
	release_sock(sk);

Chunk 1 then finds the pipe full.  fs/splice.c:splice_to_pipe() converts
no progress to an error rather than 0:

	if (!ret)
		ret = -EAGAIN;

so control takes the quoted "if (rc < 0) ... goto out" with read_done ==
chunk 0 length, jumping past the only debit site in smc_rx_recvmsg():

	atomic_sub(copylen, &conn->bytes_to_rcv);

Doesn't that leave cons advanced by the delivered bytes while
bytes_to_rcv still counts them?  smc_rx_data_available() reads
atomic_read(&conn->bytes_to_rcv) - peeked, so would a subsequent
recvmsg()/splice() start at the advanced cursor and copy that many bytes
of stale RMB content from past the producer position?

Also, can the documented invariant "0 <= bytes_to_rcv <= rmb_desc->len"
be violated if this repeats?

> +				/* splice_to_pipe() takes only what the pipe
> +				 * has room for, which may be less than was
> +				 * offered, or nothing. Account what it took
> +				 * and stop: crediting the whole chunk would
> +				 * advance the consumer past data the reader
> +				 * never received.
> +				 */
> +				if (!msg && rc < chunk_len) {
> +					if (!rc) {
> +						if (!read_done)
> +							read_done = -EAGAIN;
> +						goto out;
> +					}

[Severity: Medium]
Can rc ever be 0 here?  splice_to_pipe() returns 0 only for
spd->nr_pages == 0, and otherwise ends with:

fs/splice.c:splice_to_pipe() {
	if (!ret)
		ret = -EAGAIN;

smc_rx_splice() always builds nr_pages >= 1 and returns the
splice_to_pipe() value unchanged, so the "took nothing" case arrives as
-EAGAIN and is consumed by the pre-existing block above, which reports
-EFAULT to userspace.  Does that make this "if (!rc)" arm unreachable,
and the changelog claim

     When it took nothing, return -EAGAIN instead of reporting a length
     that was not delivered.

not take effect?

On the same note, does "SPLICE_F_NONBLOCK on a full pipe produces the
zero case" hold?  splice_file_to_pipe() calls wait_for_space() before
do_splice_read():

fs/splice.c:wait_for_space() {
		if (!pipe_is_full(pipe))
			return 0;
		if (flags & SPLICE_F_NONBLOCK)
			return -EAGAIN;

so a completely full pipe fails before smc_splice_read() is entered.
Should the comment block above also be reworded, since it describes a
callee returning zero that splice_to_pipe() does not do?

> +					copylen = chunk_len_sum - chunk_len + rc;
> +					read_remaining -= rc;
> +					read_done += rc;
> +					break;
> +				}
>  			}
>  			read_remaining -= chunk_len;
>  			read_done += chunk_len;

[Severity: High]
This is a pre-existing issue outside the diff, but this patch depends on
the invariant it breaks, so it may be worth mentioning: in
smc_rx_pipe_buf_release() the cursor advance happens under the socket
lock while the splice_pending decrement happens after it is dropped:

net/smc/smc_rx.c:smc_rx_pipe_buf_release() {
	lock_sock(sk);
	smc_rx_update_cons(smc, priv->len);
	release_sock(sk);
	if (atomic_sub_and_test(priv->len, &conn->splice_pending))
		smc_rx_wake_up(sk);

smc_rx_recvmsg() assumes cons plus splice_pending marks the start of
unread data:

	splbytes = atomic_read(&conn->splice_pending);
	...
	smc_curs_copy(&cons, &conn->local_tx_ctrl.cons, conn);
	...
	if (splbytes)
		smc_curs_add(conn->rmb_desc->len, &cons, splbytes);

In the window between release_sock() and atomic_sub_and_test(), cons
already includes priv->len and splice_pending still counts the same
bytes.  Can a concurrent splice on the same socket then start priv->len
bytes too far, skipping unread data and handing stale ring content to the
pipe?

The recvmsg() path looks protected because it waits while splice_pending
is non-zero:

		if (!readable || (msg && splbytes)) {

but the splice path (msg == NULL) is not gated on splice_pending.  The
pipe mutex doesn't serialize this either, since splice_file_to_pipe()
holds only the destination pipe's lock, so draining pipe A can race with
splicing the same socket into pipe B.

Would moving the atomic_sub_and_test() before release_sock() make cons
and splice_pending change together with respect to the socket lock?
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.