* [PATCH net] net/smc: do not credit bytes that splice() did not take
@ 2026-08-20 8:55 Hidayath Khan
2026-08-21 8:56 ` sashiko-bot
2026-08-24 10:47 ` Simon Horman
0 siblings, 2 replies; 4+ messages in thread
From: Hidayath Khan @ 2026-08-20 8:55 UTC (permalink / raw)
To: alibuda, dust.li, sidraya, mjambigi, andrew+netdev
Cc: tonylu, guwen, davem, edumazet, kuba, pabeni, horms, pasic,
hidayath, linux-s390, netdev, linux-rdma
smc_rx_recvmsg() offers a chunk of the RMB to splice_to_pipe() and then
credits the whole chunk regardless of what was taken.
splice_to_pipe() takes only what the pipe has room for. A short count and
a zero count are both non-negative, so both fall through as success: the
call returns more than it delivered, and the consumer cursor and
bytes_to_rcv are advanced by copylen rather than by what the reader
received. The skipped bytes are never handed to anyone.
A pipe that is not empty is enough to hit this; SPLICE_F_NONBLOCK on a
full pipe produces the zero case.
Account what splice_to_pipe() actually took, and stop there rather than
continuing to the second chunk of a wrapped read. When it took nothing,
return -EAGAIN instead of reporting a length that was not delivered.
Fixes: 9014db202cb7 ("smc: add support for splice()")
Cc: stable@vger.kernel.org
Reviewed-by: Sidraya Jayagond <sidraya@linux.ibm.com>
Signed-off-by: Hidayath Khan <hidayath@linux.ibm.com>
---
net/smc/smc_rx.c | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/net/smc/smc_rx.c b/net/smc/smc_rx.c
index 5c9e4d8b57de..d951dbe8eec3 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;
}
+ /* 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;
+ }
+ copylen = chunk_len_sum - chunk_len + rc;
+ read_remaining -= rc;
+ read_done += rc;
+ break;
+ }
}
read_remaining -= chunk_len;
read_done += chunk_len;
base-commit: d3083202a78cd0040be6a88953f728b0d3db1990
--
2.52.0
^ permalink raw reply related [flat|nested] 4+ messages in thread* Re: [PATCH net] net/smc: do not credit bytes that splice() did not take 2026-08-20 8:55 [PATCH net] net/smc: do not credit bytes that splice() did not take Hidayath Khan @ 2026-08-21 8:56 ` sashiko-bot 2026-08-24 10:47 ` Simon Horman 1 sibling, 0 replies; 4+ messages in thread From: sashiko-bot @ 2026-08-21 8:56 UTC (permalink / raw) To: Hidayath Khan Cc: linux-s390, Heiko Carstens, Alexander Gordeev, Christian Borntraeger, Vasily Gorbik Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: Pre-existing issues: - [High] Error paths and early exits during the second chunk of a wrapped read skip trailing socket state updates, causing data duplication and leaking `bytes_to_rcv`. - [High] The patch fails to handle a full pipe correctly, introducing dead code because `splice_to_pipe()` returns `-EAGAIN`, which is intercepted by the preexisting `rc < 0` handler and mapped to `-EFAULT`. -- --- Patch [1]: [PATCH net] net/smc: do not credit bytes that splice() did not take --- commit 13a906bee6c672b72e5c22c2fd7111bc713c0c62 Author: Hidayath Khan <hidayath@linux.ibm.com> net/smc: do not credit bytes that splice() did not take This patch modifies smc_rx_recvmsg() to account for the actual number of bytes consumed by splice_to_pipe(). It prevents advancing the consumer cursor when splice_to_pipe() takes less data than offered, and stops processing if the pipe is full. > A pipe that is not empty is enough to hit this; SPLICE_F_NONBLOCK on a > full pipe produces the zero case. > > Account what splice_to_pipe() actually took, and stop there rather than > continuing to the second chunk of a wrapped read. When it took nothing, > return -EAGAIN instead of reporting a length that was not delivered. Is this description accurate? splice_to_pipe() natively returns -EAGAIN when the pipe is full and the call is non-blocking. Since -EAGAIN is negative, it gets intercepted by the pre-existing error handler which maps it to -EFAULT when read_done is 0. Does this mean the zero-case handling added in this patch is unreachable? > 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 does this error handler unintentionally map -EAGAIN to -EFAULT? If smc_rx_splice() returns -EAGAIN on a full pipe, this block catches the negative return value and changes it to -EFAULT when read_done is 0, completely bypassing the new logic below. > + /* 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: High] This is a pre-existing issue, but does using 'goto out' here skip trailing socket state updates? If a ring buffer wrap occurs requiring two chunks, and the first chunk is successfully processed (read_done > 0), but the second chunk encounters an error or returns 0, jumping to 'out' bypasses the batched cursor update: net/smc/smc_rx.c:smc_rx_recvmsg() { ... /* update cursors */ if (!(flags & MSG_PEEK)) { ... atomic_sub(copylen, &conn->bytes_to_rcv); ... if (msg && smc_rx_update_consumer(smc, cons, copylen)) goto out; } ... } Does this leave the socket state permanently corrupted by leaking bytes_to_rcv and duplicating data on the next read? > + copylen = chunk_len_sum - chunk_len + rc; > + read_remaining -= rc; > + read_done += rc; > + break; > + } -- Sashiko AI review · https://sashiko.dev/#/patchset/20260820085543.967490-1-hidayath@linux.ibm.com?part=1 ^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH net] net/smc: do not credit bytes that splice() did not take 2026-08-20 8:55 [PATCH net] net/smc: do not credit bytes that splice() did not take Hidayath Khan 2026-08-21 8:56 ` sashiko-bot @ 2026-08-24 10:47 ` Simon Horman 2026-09-07 5:39 ` Hidayath Khan 1 sibling, 1 reply; 4+ messages in thread From: Simon Horman @ 2026-08-24 10:47 UTC (permalink / raw) To: hidayath Cc: Simon Horman, alibuda, dust.li, sidraya, mjambigi, andrew+netdev, tonylu, guwen, davem, edumazet, kuba, pabeni, pasic, linux-s390, netdev, linux-rdma 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/20260820085543.967490-1-hidayath@linux.ibm.com?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? ^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH net] net/smc: do not credit bytes that splice() did not take 2026-08-24 10:47 ` Simon Horman @ 2026-09-07 5:39 ` Hidayath Khan 0 siblings, 0 replies; 4+ messages in thread From: Hidayath Khan @ 2026-09-07 5:39 UTC (permalink / raw) To: Simon Horman Cc: alibuda, dust.li, sidraya, mjambigi, andrew+netdev, tonylu, guwen, davem, edumazet, kuba, pabeni, pasic, linux-s390, netdev, linux-rdma Thanks for the review, Simon. All four points are correct. Because the 3rd point removes part of the patch, v2 will be sent as a series. On 24/08/26 4:17 pm, Simon Horman wrote: > 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/20260820085543.967490-1-hidayath@linux.ibm.com?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? Yes. For SMC-D and SMC-R with physically contiguous RMBs, nr_pages is 1, so the transfer is all-or-nothing. I will clarify in the commit message that a short count only happens on SMC-R with virtually contiguous RMBs (is_vm). > > 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? Yes. The "goto out" skips atomic_sub(), so chunk 0 bytes are never subtracted from bytes_to_rcv. But the pipe reader still updates the cursor, leaving bytes_to_rcv permanently incorrect. I will fix this in the series: when chunk 1 fails after chunk 0 was delivered, set copylen to chunk 0 length and break instead of "goto out". The memcpy_to_msg() path has the same problem and will be fixed as well. > > 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? Yes, it is unreachable. smc_rx_splice() always passes nr_pages >= 1, so splice_to_pipe() returns -EAGAIN instead of 0. The zero case arrives as -EAGAIN, which the existing code turns into -EFAULT. In v2, I will drop the dead arm and return rc directly instead of -EFAULT (which also fixes -ENOMEM reporting). I will also reword the comment. > > 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? Yes. I will move the subtraction inside lock_sock() and keep the wake-up outside. I will send this in a separate patch. v2 will follow as a series Thanks, Hidayath ^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-09-07 5:39 UTC | newest] Thread overview: 4+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-08-20 8:55 [PATCH net] net/smc: do not credit bytes that splice() did not take Hidayath Khan 2026-08-21 8:56 ` sashiko-bot 2026-08-24 10:47 ` Simon Horman 2026-09-07 5:39 ` Hidayath Khan
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.