Skip to content

[LTS 8.6] CVE-2026-31669, CVE-2026-46135, CVE-2026-43198 - #1561

Open
pvts-mat wants to merge 3 commits into
ctrliq:ciqlts8_6from
pvts-mat:CVE-batch-44_ciqlts8_6
Open

[LTS 8.6] CVE-2026-31669, CVE-2026-46135, CVE-2026-43198#1561
pvts-mat wants to merge 3 commits into
ctrliq:ciqlts8_6from
pvts-mat:CVE-batch-44_ciqlts8_6

Conversation

@pvts-mat

Copy link
Copy Markdown
Contributor

[LTS 8.6]

CVE-2026-31669 VULN-182698
CVE-2026-46135 VULN-187106
CVE-2026-43198 VULN-184569

Commits

CVE-2026-31669

mptcp: fix slab-use-after-free in __inet_lookup_established

jira VULN-182698
cve CVE-2026-31669
commit-author Jiayuan Chen <jiayuan.chen@linux.dev>
commit 9b55b253907e7431210483519c5ad711a37dafa1
upstream-diff Conflicts due to multiple updates to the
  `mptcp_subflow_init' function. Preserved the split of
  `mptcp_subflow_init' along the `IS_ENABLED(CONFIG_MPTCP_IPV6)' line.

CVE-2026-46135

nvmet-tcp: fix race between ICReq handling and queue teardown

jira VULN-187106
cve CVE-2026-46135
commit-author Chaitanya Kulkarni <kch@nvidia.com>
commit 5293a8882c549fab4a878bc76b0b6c951f980a61
upstream-diff In the upstream the ICResp send failure path contained
  `queue->state = NVMET_TCP_Q_FAILED' assignment before return and it
  was preserved by the fix (it was introduced in
  675b453e024154dd547921c6e6d5b58747ba7e0e ("nvmet-tcp: enable TLS
  handshake upcall"), conflict-merged in
  90d624af2e5a9945eedd5cafd6ae6d88f32cc977, missing from LTS 8.6).
  Similarly the LTS 8.6 version did _not_ contain this assignment and
  that was preserved in the backport as well. Provided the
  CVE-2026-46135 bug condition didn't occur and
  `nvmet_tcp_handle_icreq()' didn't bail out early with `-ESHUTDOWN',
  not setting `queue->state' leaves it with the `NVMET_TCP_Q_CONNECTING'
  value which it had to contain for the `nvmet_tcp_handle_icreq()' to be
  called in the first place. Since it exits with a non-zero value the
  `nvmet_tcp_socket_error()' shuts down the socket, which triggers
  `nvmet_tcp_state_change()' callback carrying out the queue teardown in
  `nvmet_tcp_schedule_release_queue()'. The `queue->state !=
  NVMET_TCP_Q_DISCONNECTING' condition is passed and the queue is
  released properly.

The problem with the backport of the upstream fix is that, in the upstream, the modification in the send-failure code branch assumes that queue->state = NVMET_TCP_Q_FAILED is in place

queue->state = NVMET_TCP_Q_FAILED;

while the LTS 8.6 version does not contain such state setting. Moreover, this exact state isn't even defined in that version

enum nvmet_tcp_queue_state {
NVMET_TCP_Q_CONNECTING,
NVMET_TCP_Q_LIVE,
NVMET_TCP_Q_DISCONNECTING,
};

While the immediate intuition may be to leave this branch without queue->state modification even in the presence of introduced -ESHUTDOWN bail-out, the solutions in other versions with similar situation are contradictory. Stable linux-5.15.y backport 9c63cf80895a70eb4fcfcaa725bb1ac9ae76f02b, for example, doesn't set the queue->state, while rocky8_10 backport (embedded in 4ecf105) does, although to a different state from the upstream:

queue->state = NVMET_TCP_Q_DISCONNECTING;

Given those contradictory precedents and the subtle nature of the bug involving race condition between protocol state transitions and queue teardowns a deeper analysis was warranted.

The upstream bug scenario

Two communication sides: host and target. The bug occurs on the target side.

Host initiates connection, a socket is opened, and sends ICReq to target. The ICReq reaches target, where it will be handled by nvmet_tcp_handle_icreq(), in a process thread. Immediately after sending ICReq the host closes the socket. This triggers a software interrupt on the target side and nvmet_tcp_state_change() is called

static void nvmet_tcp_state_change(struct sock *sk)
{
struct nvmet_tcp_queue *queue;
read_lock_bh(&sk->sk_callback_lock);
queue = sk->sk_user_data;
if (!queue)
goto done;
switch (sk->sk_state) {
case TCP_FIN_WAIT2:
case TCP_LAST_ACK:
break;
case TCP_FIN_WAIT1:
case TCP_CLOSE_WAIT:
case TCP_CLOSE:
/* FALLTHRU */
nvmet_tcp_schedule_release_queue(queue);
break;
default:
pr_warn("queue %d unhandled state %d\n",
queue->idx, sk->sk_state);
}
done:
read_unlock_bh(&sk->sk_callback_lock);
}

which falls to nvmet_tcp_schedule_release_queue():

static void nvmet_tcp_schedule_release_queue(struct nvmet_tcp_queue *queue)
{
spin_lock_bh(&queue->state_lock);
if (queue->state == NVMET_TCP_Q_TLS_HANDSHAKE) {
/* Socket closed during handshake */
tls_handshake_cancel(queue->sock->sk);
}
if (queue->state != NVMET_TCP_Q_DISCONNECTING) {
queue->state = NVMET_TCP_Q_DISCONNECTING;
kref_put(&queue->kref, nvmet_tcp_release_queue);
}
spin_unlock_bh(&queue->state_lock);
}

The code branches off to queue release (upon receiving ICReq the queue->state is NVMET_TCP_Q_CONNECTING):

queue->state = NVMET_TCP_Q_DISCONNECTING;
kref_put(&queue->kref, nvmet_tcp_release_queue);

The queue->state is set to NVMET_TCP_Q_DISCONNECTING and the queue is released with kref_put().

All of this may happen before the ICReq is dequeued and fully handled. This work is done in the process context, in the io_work

INIT_WORK(&queue->io_work, nvmet_tcp_io_work);

function nvmet_tcp_io_work

static void nvmet_tcp_io_work(struct work_struct *w)

The chain of calls leading to nvmet_tcp_handle_icreq() handling the ICReq is following:

ret = nvmet_tcp_try_recv(queue, NVMET_TCP_RECV_BUDGET, &ops);

ret = nvmet_tcp_try_recv_one(queue);

result = nvmet_tcp_try_recv_pdu(queue);

return nvmet_tcp_done_recv_pdu(queue);

return nvmet_tcp_handle_icreq(queue);

Upon calling nvmet_tcp_handle_icreq() the queue->state is NVMET_TCP_Q_CONNECTING

if (unlikely(queue->state == NVMET_TCP_Q_CONNECTING)) {

The ICResp is constructed

memset(icresp, 0, sizeof(*icresp));
icresp->hdr.type = nvme_tcp_icresp;
icresp->hdr.hlen = sizeof(*icresp);
icresp->hdr.pdo = 0;
icresp->hdr.plen = cpu_to_le32(icresp->hdr.hlen);
icresp->pfv = cpu_to_le16(NVME_TCP_PFV_1_0);
icresp->maxdata = cpu_to_le32(NVMET_TCP_MAXH2CDATA);
icresp->cpda = 0;
if (queue->hdr_digest)
icresp->digest |= NVME_TCP_HDR_DIGEST_ENABLE;
if (queue->data_digest)
icresp->digest |= NVME_TCP_DATA_DIGEST_ENABLE;
iov.iov_base = icresp;
iov.iov_len = sizeof(*icresp);

and sent back to the host

ret = kernel_sendmsg(queue->sock, &msg, &iov, 1, iov.iov_len);

Then, after nvmet_tcp_schedule_release_queue() done its work in softirq in reaction to the closed socket and set queue->state = NVMET_TCP_Q_DISCONNECTING, the nvmet_tcp_handle_icreq() function sets queue->state to NVMET_TCP_Q_LIVE, reopening the connection again,

queue->state = NVMET_TCP_Q_LIVE;

or to NVMET_TCP_Q_FAILED

queue->state = NVMET_TCP_Q_FAILED;

Now, a change of the socket state can trigger nvmet_tcp_schedule_release_queue() again, and because queue->state is no longer NVMET_TCP_Q_DISCONNECTING the queue teardown code path is hit again, on an already released queue

queue->state = NVMET_TCP_Q_DISCONNECTING;
kref_put(&queue->kref, nvmet_tcp_release_queue);

The upstream fix

Right before the queue state change in nvmet_tcp_handle_icreq(), whether it's to NVMET_TCP_Q_FAILED or NVMET_TCP_Q_LIVE, a condition described above is detected by checking if queue->state didn't become NVMET_TCP_Q_DISCONNECTING since the last check at

if (unlikely(queue->state == NVMET_TCP_Q_CONNECTING)) {

If that's the case the request handling is interrupted with -ESHUTDOWN. If not, the code proceeds to alter the queue->state and continues protocol processing. In either case the handling of queue->state is guarded by queue->state_lock spinlock to serialize it with the possible concurrent access in the softirq nvmet_tcp_schedule_release_queue() callback.

If the request handling is interrupted the -ESHUTDOWN return code is eventually passed to nvmet_tcp_socket_error() as status argument, at

nvmet_tcp_socket_error(queue, ret);

The setting queue->rcv_state = NVMET_TCP_RECV_ERR remains unchanged, including now also the newly introduced case status == -ESHUTDOWN. This prevents attempts of processing the ICReq again. From the commit's message:

Keep nvmet_tcp_socket_error() setting rcv_state to NVMET_TCP_RECV_ERR before honoring that sentinel so receive-side parsing stays quiesced until the existing release path completes.

The "staying quiescent" refers to this fragment

if (unlikely(queue->rcv_state == NVMET_TCP_RECV_ERR))
return 0;

Once queue->state_recv is set to NVMET_TCP_RECV_ERR the processing of any messages from the host is effectively suppressed.

Verdict for LTS 8.6

For the LTS 8.6 version it is correct for the ICResp send failure path in nvmet_tcp_handle_icreq() to remain without setting the queue->state.

Because of

if (unlikely(queue->state == NVMET_TCP_Q_CONNECTING)) {

not setting queue->state to anything in the ICResp send failure path equals to setting it to NVMET_TCP_Q_CONNECTING. From the fixing commit's message:

If io_work later processes that ICReq, nvmet_tcp_handle_icreq() can
still overwrite the state back to NVMET_TCP_Q_LIVE. That defeats the
DISCONNECTING-state guard in nvmet_tcp_schedule_release_queue() and
allows a later socket state change to re-enter teardown and issue a
second kref_put() on an already released queue.

The ICResp send failure path has the same problem. If teardown has
already moved the queue to DISCONNECTING, a send error can still
overwrite the state with NVMET_TCP_Q_FAILED, again reopening the
window for a second teardown path to drop the queue reference.

The "reopening of the window" refers to the previously mentioned "defeat of the DISCONNECTING-state guard":

if (queue->state != NVMET_TCP_Q_DISCONNECTING) {

In the upstream both NVMET_TCP_Q_LIVE and NVMET_TCP_Q_FAILED states would pass the test and proceed to the second teardown (if not for the patch). This situation doesn't differ from LTS 8.6's NVMET_TCP_Q_LIVE and NVMET_TCP_Q_CONNECTING, both also different from NVMET_TCP_Q_DISCONNECTING, upholding the patch's logic.

In particular the queue->state should not be set to NVMET_TCP_Q_DISCONNECTING in the ICResp send failure path, as it's done in rocky8_10, because it's not accompanied by the queue teardown, which happens only in the nvmet_tcp_schedule_release_queue() function, and when it's eventually called from the nvmet_tcp_state_change() callback as the result of closing the socket (kernel_sock_shutdown(...)) in the nvmet_tcp_socket_error() status-handling function, the teardown will be skipped, resulting in leaks.

CVE-2026-43198

tcp: fix potential race in tcp_v6_syn_recv_sock()

jira VULN-184569
cve CVE-2026-43198
commit-author Eric Dumazet <edumazet@google.com>
commit 858d2a4f67ff69e645a43487ef7ea7f28f06deae
upstream-diff |
  1. Omitted changes to the `smc_tcp_syn_recv_sock()' function as it's not
     present in LTS 8.6 codebase (introduced in
     8270d9c21041470f58348248b9d9dcf3bf79592e ("net/smc: Limit backlog
     connections")).
  2. The `tcp_v6_mapped_child_init()' function differs from the upstream
     to the extent that `tcp_v6_syn_recv_sock()' from which it was
     extracted differ, specifically its `skb->protocol == htons(ETH_P_IP)'
     branch.

In the upstream the tcp_v6_mapped_child_init() function was created from tcp_v6_syn_recv_sock() - the code was extracted to be executed at different point in the sequence. The LTS 8.6 backport maintains this logic. Since the upstream version of tcp_v6_syn_recv_sock() differs from what can be found in LTS 8.6 the tcp_v6_mapped_child_init() also differs in the same way.

What may be confusing is that the Rocky 8.10 backport of that same fix (theoretically 0010a14, technically embedded in the buildable 0010a14) contains tcp_v6_mapped_child_init() which is different from what is proposed in this PR, despite tcp_v6_syn_recv_sock() from before the fix being the same in both versions (at least up to the modified skb->protocol == htons(ETH_P_IP) branch).

This discrepancy suggested the following scenarios (at least):

  1. LTS 8.6 solution is wrong and Rocky 8.10 is correct.
  2. Rocky 8.10 solution is wrong and LTS 8.6 is correct.
  3. Both solutions are correct and the code is equivalent.
  4. Both solutions are correct, but Rocky 8.10 incorporates some other related changes.

To eliminate the possibility of (1) or (4) the solution differences were analyzed.

Extracted fragment, identical in both versions:

{
struct inet_request_sock *ireq;
struct ipv6_pinfo *newnp;
const struct ipv6_pinfo *np = inet6_sk(sk);
struct ipv6_txoptions *opt;
struct tcp6_sock *newtcp6sk;
struct inet_sock *newinet;
bool found_dup_sk = false;
struct tcp_sock *newtp;
struct sock *newsk;
#ifdef CONFIG_TCP_MD5SIG
struct tcp_md5sig_key *key;
#endif
struct flowi6 fl6;
if (skb->protocol == htons(ETH_P_IP)) {
/*
* v6 mapped
*/
newsk = tcp_v4_syn_recv_sock(sk, skb, req, dst,
req_unhash, own_req);
if (!newsk)
return NULL;
newtcp6sk = (struct tcp6_sock *)newsk;
inet_sk(newsk)->pinet6 = &newtcp6sk->inet6;
newinet = inet_sk(newsk);
newnp = inet6_sk(newsk);
newtp = tcp_sk(newsk);
memcpy(newnp, np, sizeof(struct ipv6_pinfo));
newnp->saddr = newsk->sk_v6_rcv_saddr;
inet_csk(newsk)->icsk_af_ops = &ipv6_mapped;
if (sk_is_mptcp(newsk))
mptcpv6_handle_mapped(newsk, true);
newsk->sk_backlog_rcv = tcp_v4_do_rcv;
#ifdef CONFIG_TCP_MD5SIG
newtp->af_specific = &tcp_sock_ipv6_mapped_specific;
#endif
newnp->ipv6_mc_list = NULL;
newnp->ipv6_ac_list = NULL;
newnp->ipv6_fl_list = NULL;
newnp->pktoptions = NULL;
newnp->opt = NULL;
newnp->mcast_oif = inet_iif(skb);
newnp->mcast_hops = ip_hdr(skb)->ttl;
newnp->rcv_flowinfo = 0;
if (np->repflow)
newnp->flow_label = 0;

Function tcp_v6_mapped_child_init() in Rocky 8.10 after fix:

static void tcp_v6_mapped_child_init(struct sock *newsk, const struct sock *sk)
{
struct inet_sock *newinet = inet_sk(newsk);
struct tcp6_sock *newtcp6sk;
struct ipv6_pinfo *newnp;
newtcp6sk = (struct tcp6_sock *)newsk;
newinet->pinet6 = newnp = &newtcp6sk->inet6;
memcpy(newnp, inet6_sk(sk), sizeof(struct ipv6_pinfo));
newnp->saddr = newsk->sk_v6_rcv_saddr;
inet_csk(newsk)->icsk_af_ops = &ipv6_mapped;
if (sk_is_mptcp(newsk))
mptcpv6_handle_mapped(newsk, true);
newsk->sk_backlog_rcv = tcp_v4_do_rcv;
#if defined(CONFIG_TCP_MD5SIG)
tcp_sk(newsk)->af_specific = &tcp_sock_ipv6_mapped_specific;
#endif
newnp->ipv6_mc_list = NULL;
newnp->ipv6_ac_list = NULL;
newnp->ipv6_fl_list = NULL;
newnp->pktoptions = NULL;
newnp->opt = NULL;
/* tcp_v4_syn_recv_sock() has initialized newinet->mc_{index,ttl} */
newnp->mcast_oif = newinet->mc_index;
newnp->mcast_hops = newinet->mc_ttl;
newnp->rcv_flowinfo = 0;
if (inet6_sk(sk)->repflow)
newnp->flow_label = 0;
}

Function tcp_v6_mapped_child_init() in the proposed solution for CVE-2026-43198 fix on LTS 8.6:

static void tcp_v6_mapped_child_init(struct sock *newsk, const struct sock *sk)
{
struct inet_sock *newinet = inet_sk(newsk);
struct tcp6_sock *newtcp6sk;
struct ipv6_pinfo *newnp = inet6_sk(newsk);
const struct ipv6_pinfo *np = inet6_sk(sk);
newtcp6sk = (struct tcp6_sock *)newsk;
inet_sk(newsk)->pinet6 = &newtcp6sk->inet6;
memcpy(newnp, np, sizeof(struct ipv6_pinfo));
newnp->saddr = newsk->sk_v6_rcv_saddr;
inet_csk(newsk)->icsk_af_ops = &ipv6_mapped;
if (sk_is_mptcp(newsk))
mptcpv6_handle_mapped(newsk, true);
newsk->sk_backlog_rcv = tcp_v4_do_rcv;
#if CONFIG_TCP_MD5SIG
tcp_sk(newsk)->af_specific = &tcp_sock_ipv6_mapped_specific;
#endif
newnp->ipv6_mc_list = NULL;
newnp->ipv6_ac_list = NULL;
newnp->ipv6_fl_list = NULL;
newnp->pktoptions = NULL;
newnp->opt = NULL;
/* tcp_v4_syn_recv_sock() has initialized newinet->mc_{index,ttl} */
newnp->mcast_oif = newinet->mc_index;
newnp->mcast_hops = newinet->mc_ttl;
newnp->rcv_flowinfo = 0;
if (np->repflow)
newnp->flow_label = 0;
}

After the eliminating the syntactically equivalent differences what remains is the value of newnp. Rocky 8.10:

newtcp6sk = (struct tcp6_sock *)newsk;
newnp = &newtcp6sk->inet6

LTS 8.6:

newnp = inet6_sk(newsk);

The LTS 8.6 version reflects directly what was in tcp_v6_syn_recv_sock() before the fix, the Rocky 8.10 version does not. These values are not strictly equivalent unless some additional conditions hold true.

The socket structs can be arranged in "type inheritance" hierarchy, where the "base class" is the first field:

  1. sock
  2. inet_sock
    • ipv6_pinfo pinet6
  3. inet_connection_sock
  4. tcp_sock
  5. tcp6_sock
    • ipv6_pinfo inet6

With newsk being of type tcp6_sock (at least), the "newtcp6sk" access method recovers the tcp6_sock::inet6 field. while inet6_sk(newsk) returns inet_sock::pinet6. See

static inline struct ipv6_pinfo *inet6_sk(const struct sock *__sk)
{
return sk_fullsock(__sk) ? inet_sk(__sk)->pinet6 : NULL;
}

These fields can nevertheless be, for all intents and purposes of tcp_v6_syn_recv_sock() function, the same. This is suggested by the commit 93a77c1, not backported to neither Rocky 8.10 nor LTS 8.6. It introduces function tcp_inet6_sk() abstracting away the "newtcp6sk" inet6 access method, but it also converts the inet6_sk(X) instances to tcp_inet6_sk(X) (see 93a77c1#diff-8b341e52e57c996bc4f294087ab526ac0b1c3c47e045557628cc24277cbfda0dL1088-L1098).

This suggests option (4): both solutions are correct, but Rocky 8.10 incorporates also the change from commit 93a77c1. For the LTS 8.6 solution a more straightforward implementation of tcp_v6_mapped_child_init() was chosen, reflecting the code extracted from tcp_v6_syn_recv_sock() directly.

kABI check: passed

[0/1] kabi_check_kernel	Check ABI of kernel [ciqlts8_6-CVE-batch-44]	_kabi_check_kernel__x86_64--test--ciqlts8_6-CVE-batch-44
ninja explain: output state/kernels/ciqlts8_6-CVE-batch-44/x86_64/kabi_checked doesn't exist
ninja explain: state/kernels/ciqlts8_6-CVE-batch-44/x86_64/kabi_checked is dirty
+ dist_git_version=el-8.6
+ local_version=ciqlts8_6-CVE-batch-44
+ arch=x86_64
+ user=pvts
+ buildmachine=x86_64--build--ciqlts8_6
+ virsh_timeout=600
+ ssh_daemon_wait=20
+ src_dir=/mnt/code/kernel-dist-git-el-8.6
+ build_dir=/mnt/build_files/kernel-src-tree-ciqlts8_6-CVE-batch-44
+ sudo chmod +x /data/src/ctrliq-github-haskell/kernel-dist-git-el-8.6/SOURCES/check-kabi
+ ninja-back/virssh.xsh --max 8 --shutdown-on-success --shutdown-on-failure --timeout 600 --ssh-daemon-wait 20 pvts x86_64--build--ciqlts8_6 ''\''/mnt/code/kernel-dist-git-el-8.6/SOURCES/check-kabi'\'' -k '\''/mnt/code/kernel-dist-git-el-8.6/SOURCES/Module.kabi_x86_64'\'' -s '\''/mnt/build_files/kernel-src-tree-ciqlts8_6-CVE-batch-44/Module.symvers'\'''
kABI check passed
+ touch state/kernels/ciqlts8_6-CVE-batch-44/x86_64/kabi_checked

Boot test: passed

boot-test.log

Kselftests: passed relative

Reference

kselftests–ciqlts8_6–run1.log
kselftests–ciqlts8_6–run2.log

Patch

kselftests–ciqlts8_6-CVE-batch-44–run1.log
kselftests–ciqlts8_6-CVE-batch-44–run2.log

Comparison

The tests results for the reference and the patch are the same.

$ ktests.xsh diff -d kselftests*.log

Column    File
--------  --------------------------------------------
Status0   kselftests--ciqlts8_6--run1.log
Status1   kselftests--ciqlts8_6--run2.log
Status2   kselftests--ciqlts8_6-CVE-batch-44--run1.log
Status3   kselftests--ciqlts8_6-CVE-batch-44--run2.log

full-test-results-comparison.log

jira VULN-184569
cve CVE-2026-43198
commit-author Eric Dumazet <edumazet@google.com>
commit 858d2a4
upstream-diff |
  1. Omitted changes to the `smc_tcp_syn_recv_sock()' function as it's not
     present in LTS 8.6 codebase (introduced in
     8270d9c ("net/smc: Limit backlog
     connections")).
  2. The `tcp_v6_mapped_child_init()' function differs from the upstream
     to the extent that `tcp_v6_syn_recv_sock()' from which it was
     extracted differ, specifically its `skb->protocol == htons(ETH_P_IP)'
     branch.

Code in tcp_v6_syn_recv_sock() after the call to tcp_v4_syn_recv_sock()
is done too late.

After tcp_v4_syn_recv_sock(), the child socket is already visible
from TCP ehash table and other cpus might use it.

Since newinet->pinet6 is still pointing to the listener ipv6_pinfo
bad things can happen as syzbot found.

Move the problematic code in tcp_v6_mapped_child_init()
and call this new helper from tcp_v4_syn_recv_sock() before
the ehash insertion.

This allows the removal of one tcp_sync_mss(), since
tcp_v4_syn_recv_sock() will call it with the correct
context.

Fixes: 1da177e ("Linux-2.6.12-rc2")
	Reported-by: syzbot+937b5bbb6a815b3e5d0b@syzkaller.appspotmail.com
Closes: https://lore.kernel.org/netdev/69949275.050a0220.2eeac1.0145.GAE@google.com/
	Signed-off-by: Eric Dumazet <edumazet@google.com>
	Reviewed-by: Kuniyuki Iwashima <kuniyu@google.com>
Link: https://patch.msgid.link/20260217161205.2079883-1-edumazet@google.com
	Signed-off-by: Jakub Kicinski <kuba@kernel.org>
(cherry picked from commit d707e4a97b50ae768d18426355045159f0f2394e)
	Signed-off-by: Marcin Wcisło <marcin.wcislo@conclusive.pl>
jira VULN-187106
cve CVE-2026-46135
commit-author Chaitanya Kulkarni <kch@nvidia.com>
commit 5293a88
upstream-diff In the upstream the ICResp send failure path contained
  `queue->state = NVMET_TCP_Q_FAILED' assignment before return and it
  was preserved by the fix (it was introduced in
  675b453 ("nvmet-tcp: enable TLS
  handshake upcall"), conflict-merged in
  90d624a, missing from LTS 8.6).
  Similarly the LTS 8.6 version did _not_ contain this assignment and
  that was preserved in the backport as well. Provided the
  CVE-2026-46135 bug condition didn't occur and
  `nvmet_tcp_handle_icreq()' didn't bail out early with `-ESHUTDOWN',
  not setting `queue->state' leaves it with the `NVMET_TCP_Q_CONNECTING'
  value which it had to contain for the `nvmet_tcp_handle_icreq()' to be
  called in the first place. Since it exits with a non-zero value the
  `nvmet_tcp_socket_error()' shuts down the socket, which triggers
  `nvmet_tcp_state_change()' callback carrying out the queue teardown in
  `nvmet_tcp_schedule_release_queue()'. The `queue->state !=
  NVMET_TCP_Q_DISCONNECTING' condition is passed and the queue is
  released properly.

nvmet_tcp_handle_icreq() updates queue->state after sending an
Initialization Connection Response (ICResp), but it does so without
serializing against target-side queue teardown.

If an NVMe/TCP host sends an Initialization Connection Request
(ICReq) and immediately closes the connection, target-side teardown
may start in softirq context before io_work drains the already
buffered ICReq. In that case, nvmet_tcp_schedule_release_queue()
sets queue->state to NVMET_TCP_Q_DISCONNECTING and drops the queue
reference under state_lock.

If io_work later processes that ICReq, nvmet_tcp_handle_icreq() can
still overwrite the state back to NVMET_TCP_Q_LIVE. That defeats the
DISCONNECTING-state guard in nvmet_tcp_schedule_release_queue() and
allows a later socket state change to re-enter teardown and issue a
second kref_put() on an already released queue.

The ICResp send failure path has the same problem. If teardown has
already moved the queue to DISCONNECTING, a send error can still
overwrite the state with NVMET_TCP_Q_FAILED, again reopening the
window for a second teardown path to drop the queue reference.

Fix this by serializing both post-send state transitions with
state_lock and bailing out if teardown has already started.

Use -ESHUTDOWN as an internal sentinel for that bail-out path rather
than propagating it as a transport error like -ECONNRESET. Keep
nvmet_tcp_socket_error() setting rcv_state to NVMET_TCP_RECV_ERR before
honoring that sentinel so receive-side parsing stays quiesced until the
existing release path completes.

Fixes: c46a6465bac2 ("nvmet-tcp: add NVMe over TCP target driver")
	Cc: stable@vger.kernel.org
	Reported-by: Shivam Kumar <skumar47@syr.edu>
	Tested-by: Shivam Kumar <kumar.shivam43666@gmail.com>
	Signed-off-by: Chaitanya Kulkarni <kch@nvidia.com>
	Signed-off-by: Keith Busch <kbusch@kernel.org>
(cherry picked from commit 7f39b4e844e63db6a6b4c4bba59347c21aa9f808)
	Signed-off-by: Marcin Wcisło <marcin.wcislo@conclusive.pl>
jira VULN-182698
cve CVE-2026-31669
commit-author Jiayuan Chen <jiayuan.chen@linux.dev>
commit 9b55b25
upstream-diff Conflicts due to multiple updates to the
  `mptcp_subflow_init' function. Preserved the split of
  `mptcp_subflow_init' along the `IS_ENABLED(CONFIG_MPTCP_IPV6)' line.

The ehash table lookups are lockless and rely on
SLAB_TYPESAFE_BY_RCU to guarantee socket memory stability
during RCU read-side critical sections. Both tcp_prot and
tcpv6_prot have their slab caches created with this flag
via proto_register().

However, MPTCP's mptcp_subflow_init() copies tcpv6_prot into
tcpv6_prot_override during inet_init() (fs_initcall, level 5),
before inet6_init() (module_init/device_initcall, level 6) has
called proto_register(&tcpv6_prot). At that point,
tcpv6_prot.slab is still NULL, so tcpv6_prot_override.slab
remains NULL permanently.

This causes MPTCP v6 subflow child sockets to be allocated via
kmalloc (falling into kmalloc-4k) instead of the TCPv6 slab
cache. The kmalloc-4k cache lacks SLAB_TYPESAFE_BY_RCU, so
when these sockets are freed without SOCK_RCU_FREE (which is
cleared for child sockets by design), the memory can be
immediately reused. Concurrent ehash lookups under
rcu_read_lock can then access freed memory, triggering a
slab-use-after-free in __inet_lookup_established.

Fix this by splitting the IPv6-specific initialization out of
mptcp_subflow_init() into a new mptcp_subflow_v6_init(), called
from mptcp_proto_v6_init() before protocol registration. This
ensures tcpv6_prot_override.slab correctly inherits the
SLAB_TYPESAFE_BY_RCU slab cache.

Fixes: b19bc29 ("mptcp: implement delegated actions")
	Cc: stable@vger.kernel.org
	Signed-off-by: Jiayuan Chen <jiayuan.chen@linux.dev>
	Reviewed-by: Matthieu Baerts (NGI0) <matttbe@kernel.org>
Link: https://patch.msgid.link/20260406031512.189159-1-jiayuan.chen@linux.dev
	Signed-off-by: Jakub Kicinski <kuba@kernel.org>
(cherry picked from commit 6b644e703c07d879025067d7e2a6fac8db3b2ac1)
	Signed-off-by: Marcin Wcisło <marcin.wcislo@conclusive.pl>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant