* [PATCH mptcp-net v2 1/3] mptcp: fallback to TCP on MP_FAIL with a single subflow
2026-07-15 6:18 [PATCH mptcp-net v2 0/3] mptcp: fix TCP fallback on single-subflow MP_FAIL Chenguang Zhao
@ 2026-07-15 6:18 ` Chenguang Zhao
2026-07-15 6:18 ` [PATCH mptcp-net v2 2/3] mptcp: use READ/WRITE_ONCE for allow_subflows Chenguang Zhao
2026-07-15 6:18 ` [PATCH mptcp-net v2 3/3] mptcp: add MPFailFallback MIB for MP_FAIL TCP fallback Chenguang Zhao
2 siblings, 0 replies; 4+ messages in thread
From: Chenguang Zhao @ 2026-07-15 6:18 UTC (permalink / raw)
To: mptcp; +Cc: chenguang.zhao, Chenguang Zhao
From: Chenguang Zhao <zhaochenguang@kylinos.cn>
When a valid MP_FAIL is received and infinite fallback is still allowed
(single contiguous subflow), RFC8684 §3.7 requires leaving MPTCP mode.
The stack only cleared allow_subflows and deferred the real fallback to
the later infinite-map transmit path. Before any data is sent, a peer
could still complete the 4th ACK as MPTCP and keep using MPTCP options.
Fall back immediately after sending the MP_FAIL response, and teach
mptcp_is_fully_established() to reject joins after fallback or when
subflows are disallowed. If the out-of-order queue is non-empty, reset
the subflow instead of leaving a half-fallback state.
Fixes: 1e39e5a32ad7 ("mptcp: infinite mapping sending")
Signed-off-by: Chenguang Zhao <zhaochenguang@kylinos.cn>
---
net/mptcp/pm.c | 51 +++++++++++++++++++++++++++++++++++++++++++-
net/mptcp/protocol.c | 7 +++++-
net/mptcp/protocol.h | 18 ++++++++++------
3 files changed, 67 insertions(+), 9 deletions(-)
diff --git a/net/mptcp/pm.c b/net/mptcp/pm.c
index 6afd39aea110..c1f5c3ced4ee 100644
--- a/net/mptcp/pm.c
+++ b/net/mptcp/pm.c
@@ -870,7 +870,15 @@ void mptcp_pm_mp_fail_received(struct sock *sk, u64 fail_seq)
pr_debug("fail_seq=%llu\n", fail_seq);
- /* After accepting the fail, we can't create any other subflows */
+ /* MP_FAIL on a single contiguous subflow: fall back to TCP.
+ * allow_infinite_fallback is cleared once other subflows join or
+ * non-contiguous data is retransmitted; in that case ignore MP_FAIL
+ * here (the peer should reset the failing subflow instead).
+ *
+ * Send the MP_FAIL (+ DSS) response before setting FALLBACK_DONE,
+ * otherwise mptcp_established_options() would drop all MPTCP options
+ * on this ACK. InfiniteMapTx is accounted later when the map is sent.
+ */
spin_lock_bh(&msk->fallback_lock);
if (!msk->allow_infinite_fallback) {
spin_unlock_bh(&msk->fallback_lock);
@@ -882,9 +890,50 @@ void mptcp_pm_mp_fail_received(struct sock *sk, u64 fail_seq)
if (!subflow->fail_tout) {
pr_debug("send MP_FAIL response and infinite map\n");
+ /* Infinite mapping requires contiguous data. With OoO still
+ * queued, do not leave allow_subflows=false without
+ * FALLBACK_DONE; tear the subflow down instead (RFC8684 §3.7).
+ */
+ if (!RB_EMPTY_ROOT(&msk->out_of_order_queue)) {
+ MPTCP_INC_STATS(sock_net(sk), MPTCP_MIB_FALLBACKFAILED);
+ subflow->send_mp_fail = 1;
+ mptcp_subflow_reset(sk);
+ return;
+ }
+
subflow->send_mp_fail = 1;
subflow->send_infinite_map = 1;
tcp_send_ack(sk);
+
+ /* RFC8684 §3.7: after accepting MP_FAIL with a single
+ * subflow, leave MPTCP mode and never revert. No dedicated
+ * fallback MIB yet; InfiniteMapTx is counted when the map
+ * is transmitted. Handle pending DATA_FIN like
+ * mptcp_try_fallback().
+ */
+ spin_lock_bh(&msk->fallback_lock);
+ if (__mptcp_check_fallback(msk)) {
+ spin_unlock_bh(&msk->fallback_lock);
+ return;
+ }
+ if (!msk->allow_infinite_fallback) {
+ spin_unlock_bh(&msk->fallback_lock);
+ MPTCP_INC_STATS(sock_net(sk), MPTCP_MIB_FALLBACKFAILED);
+ mptcp_subflow_reset(sk);
+ return;
+ }
+ set_bit(MPTCP_FALLBACK_DONE, &msk->flags);
+ spin_unlock_bh(&msk->fallback_lock);
+
+ if (READ_ONCE(msk->snd_data_fin_enable) &&
+ !(sk->sk_shutdown & SEND_SHUTDOWN)) {
+ gfp_t saved_allocation = sk->sk_allocation;
+
+ sk->sk_allocation = GFP_ATOMIC;
+ sk->sk_shutdown |= SEND_SHUTDOWN;
+ tcp_shutdown(sk, SEND_SHUTDOWN);
+ sk->sk_allocation = saved_allocation;
+ }
} else {
pr_debug("MP_FAIL response received\n");
WRITE_ONCE(subflow->fail_tout, 0);
diff --git a/net/mptcp/protocol.c b/net/mptcp/protocol.c
index cb9515f505aa..5b9522caaf43 100644
--- a/net/mptcp/protocol.c
+++ b/net/mptcp/protocol.c
@@ -1299,7 +1299,12 @@ static void mptcp_update_infinite_map(struct mptcp_sock *msk,
mpext->infinite_map = 1;
mpext->data_len = 0;
- if (!mptcp_try_fallback(ssk, MPTCP_MIB_INFINITEMAPTX)) {
+ /* Fallback may already have been completed on MP_FAIL reception;
+ * still account for the infinite mapping being transmitted.
+ */
+ if (__mptcp_check_fallback(msk)) {
+ MPTCP_INC_STATS(sock_net(ssk), MPTCP_MIB_INFINITEMAPTX);
+ } else if (!mptcp_try_fallback(ssk, MPTCP_MIB_INFINITEMAPTX)) {
MPTCP_INC_STATS(sock_net(ssk), MPTCP_MIB_FALLBACKFAILED);
mptcp_subflow_reset(ssk);
return;
diff --git a/net/mptcp/protocol.h b/net/mptcp/protocol.h
index 4a2d40cd7b13..03f0b33694d7 100644
--- a/net/mptcp/protocol.h
+++ b/net/mptcp/protocol.h
@@ -369,7 +369,7 @@ struct mptcp_sock {
spinlock_t fallback_lock; /* protects fallback,
* allow_infinite_fallback and
- * allow_join
+ * allow_subflows
*/
struct list_head backlog_list; /* protected by the data lock */
@@ -947,12 +947,6 @@ static inline void mptcp_start_tout_timer(struct sock *sk)
mptcp_reset_tout_timer(mptcp_sk(sk), 0);
}
-static inline bool mptcp_is_fully_established(struct sock *sk)
-{
- return inet_sk_state_load(sk) == TCP_ESTABLISHED &&
- READ_ONCE(mptcp_sk(sk)->fully_established);
-}
-
static inline u64 mptcp_stamp(void)
{
return div_u64(tcp_clock_ns(), NSEC_PER_USEC);
@@ -1290,6 +1284,16 @@ static inline bool mptcp_check_fallback(const struct sock *sk)
return __mptcp_check_fallback(msk);
}
+static inline bool mptcp_is_fully_established(struct sock *sk)
+{
+ struct mptcp_sock *msk = mptcp_sk(sk);
+
+ return inet_sk_state_load(sk) == TCP_ESTABLISHED &&
+ READ_ONCE(msk->fully_established) &&
+ !__mptcp_check_fallback(msk) &&
+ msk->allow_subflows;
+}
+
static inline bool __mptcp_has_initial_subflow(const struct mptcp_sock *msk)
{
struct sock *ssk = READ_ONCE(msk->first);
--
2.25.1
^ permalink raw reply related [flat|nested] 4+ messages in thread* [PATCH mptcp-net v2 2/3] mptcp: use READ/WRITE_ONCE for allow_subflows
2026-07-15 6:18 [PATCH mptcp-net v2 0/3] mptcp: fix TCP fallback on single-subflow MP_FAIL Chenguang Zhao
2026-07-15 6:18 ` [PATCH mptcp-net v2 1/3] mptcp: fallback to TCP on MP_FAIL with a single subflow Chenguang Zhao
@ 2026-07-15 6:18 ` Chenguang Zhao
2026-07-15 6:18 ` [PATCH mptcp-net v2 3/3] mptcp: add MPFailFallback MIB for MP_FAIL TCP fallback Chenguang Zhao
2 siblings, 0 replies; 4+ messages in thread
From: Chenguang Zhao @ 2026-07-15 6:18 UTC (permalink / raw)
To: mptcp; +Cc: chenguang.zhao, Chenguang Zhao
From: Chenguang Zhao <zhaochenguang@kylinos.cn>
mptcp_is_fully_established() may read allow_subflows without holding
fallback_lock. Annotate all allow_subflows accesses with READ_ONCE and
WRITE_ONCE to avoid data-race warnings and make the concurrency model
explicit.
Signed-off-by: Chenguang Zhao <zhaochenguang@kylinos.cn>
---
net/mptcp/pm.c | 2 +-
net/mptcp/protocol.c | 12 ++++++------
net/mptcp/protocol.h | 2 +-
net/mptcp/subflow.c | 2 +-
4 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/net/mptcp/pm.c b/net/mptcp/pm.c
index c1f5c3ced4ee..869876a06493 100644
--- a/net/mptcp/pm.c
+++ b/net/mptcp/pm.c
@@ -884,7 +884,7 @@ void mptcp_pm_mp_fail_received(struct sock *sk, u64 fail_seq)
spin_unlock_bh(&msk->fallback_lock);
return;
}
- msk->allow_subflows = false;
+ WRITE_ONCE(msk->allow_subflows, false);
spin_unlock_bh(&msk->fallback_lock);
if (!subflow->fail_tout) {
diff --git a/net/mptcp/protocol.c b/net/mptcp/protocol.c
index 5b9522caaf43..13a8d16c657a 100644
--- a/net/mptcp/protocol.c
+++ b/net/mptcp/protocol.c
@@ -93,7 +93,7 @@ bool __mptcp_try_fallback(struct mptcp_sock *msk, int fb_mib)
return false;
}
- msk->allow_subflows = false;
+ WRITE_ONCE(msk->allow_subflows, false);
set_bit(MPTCP_FALLBACK_DONE, &msk->flags);
__MPTCP_INC_STATS(net, fb_mib);
spin_unlock_bh(&msk->fallback_lock);
@@ -958,7 +958,7 @@ static bool __mptcp_finish_join(struct mptcp_sock *msk, struct sock *ssk)
return false;
spin_lock_bh(&msk->fallback_lock);
- if (!msk->allow_subflows) {
+ if (!READ_ONCE(msk->allow_subflows)) {
spin_unlock_bh(&msk->fallback_lock);
return false;
}
@@ -2844,7 +2844,7 @@ static void __mptcp_retrans(struct sock *sk)
*/
spin_lock_bh(&msk->fallback_lock);
if (__mptcp_check_fallback(msk) ||
- !msk->allow_subflows) {
+ !READ_ONCE(msk->allow_subflows)) {
spin_unlock_bh(&msk->fallback_lock);
release_sock(ssk);
goto clear_scheduled;
@@ -3055,7 +3055,7 @@ static void __mptcp_init_sock(struct sock *sk)
inet_csk(sk)->icsk_sync_mss = mptcp_sync_mss;
WRITE_ONCE(msk->csum_enabled, mptcp_is_checksum_enabled(sock_net(sk)));
msk->allow_infinite_fallback = true;
- msk->allow_subflows = true;
+ WRITE_ONCE(msk->allow_subflows, true);
msk->recovery = false;
msk->subflow_id = 1;
msk->last_data_sent = tcp_jiffies32;
@@ -3473,7 +3473,7 @@ static int mptcp_disconnect(struct sock *sk, int flags)
* can't overlap with a fallback anymore
*/
spin_lock_bh(&msk->fallback_lock);
- msk->allow_subflows = true;
+ WRITE_ONCE(msk->allow_subflows, true);
msk->allow_infinite_fallback = true;
WRITE_ONCE(msk->flags, 0);
spin_unlock_bh(&msk->fallback_lock);
@@ -3873,7 +3873,7 @@ bool mptcp_finish_join(struct sock *ssk)
*/
if (!list_empty(&subflow->node)) {
spin_lock_bh(&msk->fallback_lock);
- if (!msk->allow_subflows) {
+ if (!READ_ONCE(msk->allow_subflows)) {
spin_unlock_bh(&msk->fallback_lock);
return false;
}
diff --git a/net/mptcp/protocol.h b/net/mptcp/protocol.h
index 03f0b33694d7..833c76c41768 100644
--- a/net/mptcp/protocol.h
+++ b/net/mptcp/protocol.h
@@ -1291,7 +1291,7 @@ static inline bool mptcp_is_fully_established(struct sock *sk)
return inet_sk_state_load(sk) == TCP_ESTABLISHED &&
READ_ONCE(msk->fully_established) &&
!__mptcp_check_fallback(msk) &&
- msk->allow_subflows;
+ READ_ONCE(msk->allow_subflows);
}
static inline bool __mptcp_has_initial_subflow(const struct mptcp_sock *msk)
diff --git a/net/mptcp/subflow.c b/net/mptcp/subflow.c
index 8e386899ceb9..b04d6edb9be3 100644
--- a/net/mptcp/subflow.c
+++ b/net/mptcp/subflow.c
@@ -1323,7 +1323,7 @@ static bool mptcp_subflow_fail(struct mptcp_sock *msk, struct sock *ssk)
spin_unlock_bh(&msk->fallback_lock);
return false;
}
- msk->allow_subflows = false;
+ WRITE_ONCE(msk->allow_subflows, false);
spin_unlock_bh(&msk->fallback_lock);
/* graceful failure can happen only on the MPC subflow */
--
2.25.1
^ permalink raw reply related [flat|nested] 4+ messages in thread* [PATCH mptcp-net v2 3/3] mptcp: add MPFailFallback MIB for MP_FAIL TCP fallback
2026-07-15 6:18 [PATCH mptcp-net v2 0/3] mptcp: fix TCP fallback on single-subflow MP_FAIL Chenguang Zhao
2026-07-15 6:18 ` [PATCH mptcp-net v2 1/3] mptcp: fallback to TCP on MP_FAIL with a single subflow Chenguang Zhao
2026-07-15 6:18 ` [PATCH mptcp-net v2 2/3] mptcp: use READ/WRITE_ONCE for allow_subflows Chenguang Zhao
@ 2026-07-15 6:18 ` Chenguang Zhao
2 siblings, 0 replies; 4+ messages in thread
From: Chenguang Zhao @ 2026-07-15 6:18 UTC (permalink / raw)
To: mptcp; +Cc: chenguang.zhao, Chenguang Zhao
From: Chenguang Zhao <zhaochenguang@kylinos.cn>
Track fallbacks triggered by receiving MP_FAIL with a dedicated MIB, and
switch mptcp_pm_mp_fail_received() to mptcp_try_fallback() so the counter
is updated and pending DATA_FIN is handled through the common path.
InfiniteMapTx remains counted when the infinite mapping is transmitted.
Signed-off-by: Chenguang Zhao <zhaochenguang@kylinos.cn>
---
net/mptcp/mib.c | 1 +
net/mptcp/mib.h | 1 +
net/mptcp/pm.c | 28 ++++------------------------
3 files changed, 6 insertions(+), 24 deletions(-)
diff --git a/net/mptcp/mib.c b/net/mptcp/mib.c
index f23fda0c55a7..4dc107ab1aec 100644
--- a/net/mptcp/mib.c
+++ b/net/mptcp/mib.c
@@ -83,6 +83,7 @@ static const struct snmp_mib mptcp_snmp_list[] = {
SNMP_MIB_ITEM("MD5SigFallback", MPTCP_MIB_MD5SIGFALLBACK),
SNMP_MIB_ITEM("DssFallback", MPTCP_MIB_DSSFALLBACK),
SNMP_MIB_ITEM("SimultConnectFallback", MPTCP_MIB_SIMULTCONNFALLBACK),
+ SNMP_MIB_ITEM("MPFailFallback", MPTCP_MIB_MPFAILFALLBACK),
SNMP_MIB_ITEM("FallbackFailed", MPTCP_MIB_FALLBACKFAILED),
SNMP_MIB_ITEM("WinProbe", MPTCP_MIB_WINPROBE),
};
diff --git a/net/mptcp/mib.h b/net/mptcp/mib.h
index 812218b5ed2b..9db8812d8e04 100644
--- a/net/mptcp/mib.h
+++ b/net/mptcp/mib.h
@@ -86,6 +86,7 @@ enum linux_mptcp_mib_field {
MPTCP_MIB_MD5SIGFALLBACK, /* Conflicting TCP option enabled */
MPTCP_MIB_DSSFALLBACK, /* Bad or missing DSS */
MPTCP_MIB_SIMULTCONNFALLBACK, /* Simultaneous connect */
+ MPTCP_MIB_MPFAILFALLBACK, /* Received MP_FAIL, fallback to TCP */
MPTCP_MIB_FALLBACKFAILED, /* Can't fallback due to msk status */
MPTCP_MIB_WINPROBE, /* MPTCP-level zero window probe */
__MPTCP_MIB_MAX
diff --git a/net/mptcp/pm.c b/net/mptcp/pm.c
index 869876a06493..82d3dbedd603 100644
--- a/net/mptcp/pm.c
+++ b/net/mptcp/pm.c
@@ -906,33 +906,13 @@ void mptcp_pm_mp_fail_received(struct sock *sk, u64 fail_seq)
tcp_send_ack(sk);
/* RFC8684 §3.7: after accepting MP_FAIL with a single
- * subflow, leave MPTCP mode and never revert. No dedicated
- * fallback MIB yet; InfiniteMapTx is counted when the map
- * is transmitted. Handle pending DATA_FIN like
- * mptcp_try_fallback().
+ * subflow, leave MPTCP mode and never revert. Use
+ * mptcp_try_fallback() so pending DATA_FIN is handled.
+ * InfiniteMapTx is counted when the map is transmitted.
*/
- spin_lock_bh(&msk->fallback_lock);
- if (__mptcp_check_fallback(msk)) {
- spin_unlock_bh(&msk->fallback_lock);
- return;
- }
- if (!msk->allow_infinite_fallback) {
- spin_unlock_bh(&msk->fallback_lock);
+ if (!mptcp_try_fallback(sk, MPTCP_MIB_MPFAILFALLBACK)) {
MPTCP_INC_STATS(sock_net(sk), MPTCP_MIB_FALLBACKFAILED);
mptcp_subflow_reset(sk);
- return;
- }
- set_bit(MPTCP_FALLBACK_DONE, &msk->flags);
- spin_unlock_bh(&msk->fallback_lock);
-
- if (READ_ONCE(msk->snd_data_fin_enable) &&
- !(sk->sk_shutdown & SEND_SHUTDOWN)) {
- gfp_t saved_allocation = sk->sk_allocation;
-
- sk->sk_allocation = GFP_ATOMIC;
- sk->sk_shutdown |= SEND_SHUTDOWN;
- tcp_shutdown(sk, SEND_SHUTDOWN);
- sk->sk_allocation = saved_allocation;
}
} else {
pr_debug("MP_FAIL response received\n");
--
2.25.1
^ permalink raw reply related [flat|nested] 4+ messages in thread