From 2c8b7bbe29cb4526b190d3a33ef9efff1fe7d1eb Mon Sep 17 00:00:00 2001 From: Aurelien Aptel Date: Sat, 12 Sep 2026 03:16:43 -0300 Subject: [PATCH 01/16] BACKPORT: cifs: enable multichannel on the non-DFS mount path Open multichannel connections after a successful non-DFS mount. The vendor 5.15 tree uses a different mount path from current mainline, so adapt the upstream channel-opening behavior to that layout. Signed-off-by: Aurelien Aptel Change-Id: I320380d0e8efe25d55ec944a95d3a42a1e855928 (cherry picked from commit d70e9fa55884760b6d6c293dbf20d8c52ce11fb7) [juan: adapted the listed upstream change(s) to the CoreELEC 5.15.196 vendor layout; no original code authorship is claimed.] Signed-off-by: Juan Ruiz <6059278+juanruizm001@users.noreply.github.com> --- fs/cifs/connect.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fs/cifs/connect.c b/fs/cifs/connect.c index da386ae1c1416..ab80d7dec7764 100644 --- a/fs/cifs/connect.c +++ b/fs/cifs/connect.c @@ -3614,6 +3614,11 @@ int cifs_mount(struct cifs_sb_info *cifs_sb, struct smb3_fs_context *ctx) goto error; } + /* + * SMB3 multichannel is independent of DFS. Keep the non-DFS mount + * path equivalent to the DFS path. + */ + cifs_try_adding_channels(cifs_sb, mnt_ctx.ses); rc = mount_setup_tlink(cifs_sb, mnt_ctx.ses, mnt_ctx.tcon); if (rc) goto error; From 2c9d770e6a5f5f5e4323620ce095bc8edd71fb02 Mon Sep 17 00:00:00 2001 From: Shyam Prasad N Date: Sat, 12 Sep 2026 03:16:44 -0300 Subject: [PATCH 02/16] BACKPORT: cifs: keep secondary-channel hostnames empty Keep secondary-channel hostnames empty so reconnect does not resolve the primary hostname to the wrong address for another channel. Signed-off-by: Shyam Prasad N Change-Id: I6bd519a1bbd1af5021a7b6bd592b69245c966d64 (cherry picked from commit 4c14d7043fede258957d7b01da0cad2d9fe3a205) [juan: adapted the listed upstream change(s) to the CoreELEC 5.15.196 vendor layout; no original code authorship is claimed.] Signed-off-by: Juan Ruiz <6059278+juanruizm001@users.noreply.github.com> --- fs/cifs/connect.c | 4 ++++ fs/cifs/sess.c | 3 +++ 2 files changed, 7 insertions(+) diff --git a/fs/cifs/connect.c b/fs/cifs/connect.c index ab80d7dec7764..9be1c5244dc80 100644 --- a/fs/cifs/connect.c +++ b/fs/cifs/connect.c @@ -97,6 +97,10 @@ static int reconn_set_ipaddr_from_hostname(struct TCP_Server_Info *server) if (!server->hostname) return -EINVAL; + /* if server hostname isn't populated, there's nothing to do here */ + if (server->hostname[0] == '\0') + return 0; + len = strlen(server->hostname) + 3; unc = kmalloc(len, GFP_KERNEL); diff --git a/fs/cifs/sess.c b/fs/cifs/sess.c index 0fbd0f78f361b..ab1dac4834f52 100644 --- a/fs/cifs/sess.c +++ b/fs/cifs/sess.c @@ -223,6 +223,9 @@ cifs_ses_add_channel(struct cifs_sb_info *cifs_sb, struct cifs_ses *ses, /* Auth */ ctx.domainauto = ses->domainAuto; ctx.domainname = ses->domainName; + + /* no hostname for extra channels */ + ctx.server_hostname = ""; ctx.username = ses->user_name; ctx.password = ses->password; ctx.sectype = ses->sectype; From 082e360b3a5aeacdeea8c411e8bd0a73e30c48a2 Mon Sep 17 00:00:00 2001 From: Shyam Prasad N Date: Sat, 12 Sep 2026 03:16:46 -0300 Subject: [PATCH 03/16] BACKPORT: cifs: use the least loaded channel for requests Select the least-loaded eligible channel for regular requests while retaining the binding behavior required by this vendor 5.15 tree. Signed-off-by: Shyam Prasad N Change-Id: I7d1dfe2b4d2da152c69dcf7d09ee0eeb5c54411d (cherry picked from commit ea90708d3cf3d0d92c02afe445ad463fb3c6bf10) [juan: adapted the listed upstream change(s) to the CoreELEC 5.15.196 vendor layout; no original code authorship is claimed.] Signed-off-by: Juan Ruiz <6059278+juanruizm001@users.noreply.github.com> --- fs/cifs/transport.c | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/fs/cifs/transport.c b/fs/cifs/transport.c index c5c1e743359dd..7fcb309741e53 100644 --- a/fs/cifs/transport.c +++ b/fs/cifs/transport.c @@ -1039,16 +1039,42 @@ cifs_cancelled_callback(struct mid_q_entry *mid) struct TCP_Server_Info *cifs_pick_channel(struct cifs_ses *ses) { uint index = 0; + unsigned int min_in_flight = UINT_MAX, max_in_flight = 0; + struct TCP_Server_Info *server = NULL; + int i; if (!ses) return NULL; spin_lock(&ses->chan_lock); if (!ses->binding) { - /* round robin */ if (ses->chan_count > 1) { - index = (uint)atomic_inc_return(&ses->chan_seq); - index %= ses->chan_count; + for (i = 0; i < ses->chan_count; i++) { + server = ses->chans[i].server; + if (!server) + continue; + + /* + * strictly speaking, we should pick up req_lock to read + * server->in_flight. But it shouldn't matter much here if we + * race while reading this data. The worst that can happen is + * that we could use a channel that's not least loaded. Avoiding + * taking the lock could help reduce wait time, which is + * important for this function + */ + if (server->in_flight < min_in_flight) { + min_in_flight = server->in_flight; + index = i; + } + if (server->in_flight > max_in_flight) + max_in_flight = server->in_flight; + } + + /* if all channels are equally loaded, fall back to round-robin */ + if (min_in_flight == max_in_flight) { + index = (uint)atomic_inc_return(&ses->chan_seq); + index %= ses->chan_count; + } } spin_unlock(&ses->chan_lock); return ses->chans[index].server; From 19a567fa29b3208505f83cf06b2640b9b4865680 Mon Sep 17 00:00:00 2001 From: Shyam Prasad N Date: Sat, 12 Sep 2026 03:16:47 -0300 Subject: [PATCH 04/16] BACKPORT: cifs: reduce channel loading lag during selection Start channel scanning from the previously selected position. This reduces selection skew while preserving the 5.15 binding model. Signed-off-by: Shyam Prasad N Change-Id: Ia756124913521f42956ee3c49fcf86735a88b5f3 (cherry picked from commit 66d590b828b1fd9fa337047ae58fe1c4c6f43609) [juan: adapted the listed upstream change(s) to the CoreELEC 5.15.196 vendor layout; no original code authorship is claimed.] Signed-off-by: Juan Ruiz <6059278+juanruizm001@users.noreply.github.com> --- fs/cifs/transport.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/fs/cifs/transport.c b/fs/cifs/transport.c index 7fcb309741e53..212a3b7b8d2bb 100644 --- a/fs/cifs/transport.c +++ b/fs/cifs/transport.c @@ -1041,7 +1041,7 @@ struct TCP_Server_Info *cifs_pick_channel(struct cifs_ses *ses) uint index = 0; unsigned int min_in_flight = UINT_MAX, max_in_flight = 0; struct TCP_Server_Info *server = NULL; - int i; + int i, start, cur; if (!ses) return NULL; @@ -1049,8 +1049,10 @@ struct TCP_Server_Info *cifs_pick_channel(struct cifs_ses *ses) spin_lock(&ses->chan_lock); if (!ses->binding) { if (ses->chan_count > 1) { + start = atomic_inc_return(&ses->chan_seq); for (i = 0; i < ses->chan_count; i++) { - server = ses->chans[i].server; + cur = (start + i) % ses->chan_count; + server = ses->chans[cur].server; if (!server) continue; @@ -1064,17 +1066,15 @@ struct TCP_Server_Info *cifs_pick_channel(struct cifs_ses *ses) */ if (server->in_flight < min_in_flight) { min_in_flight = server->in_flight; - index = i; + index = cur; } if (server->in_flight > max_in_flight) max_in_flight = server->in_flight; } /* if all channels are equally loaded, fall back to round-robin */ - if (min_in_flight == max_in_flight) { - index = (uint)atomic_inc_return(&ses->chan_seq); - index %= ses->chan_count; - } + if (min_in_flight == max_in_flight) + index = (uint)start % ses->chan_count; } spin_unlock(&ses->chan_lock); return ses->chans[index].server; From 03686fb2366c2ff2faa682c2c5ce8b233d73311e Mon Sep 17 00:00:00 2001 From: Shyam Prasad N Date: Sat, 12 Sep 2026 03:16:48 -0300 Subject: [PATCH 05/16] BACKPORT: cifs: select only eligible multichannel transports Compose the upstream channel-eligibility fixes for the vendor 5.15 selector. Skip reconnecting channels, test the correct candidate slot, and keep the scan-selected channel when loads are equal. Signed-off-by: Shyam Prasad N Change-Id: I8674146879a3e664553bb08d31093af156d1bd77 (cherry picked from commit fc43a8ac396d302ced1e991e4913827cf72c8eb9) (cherry picked from commit 79280191c2fd7f24899bbd640003b5389d3c109c) (cherry picked from commit 663c28469d3274d6456f206a6671c91493d85ff1) [juan: adapted the listed upstream change(s) to the CoreELEC 5.15.196 vendor layout; no original code authorship is claimed.] Signed-off-by: Juan Ruiz <6059278+juanruizm001@users.noreply.github.com> --- fs/cifs/transport.c | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/fs/cifs/transport.c b/fs/cifs/transport.c index 212a3b7b8d2bb..4062a535980b1 100644 --- a/fs/cifs/transport.c +++ b/fs/cifs/transport.c @@ -1039,7 +1039,7 @@ cifs_cancelled_callback(struct mid_q_entry *mid) struct TCP_Server_Info *cifs_pick_channel(struct cifs_ses *ses) { uint index = 0; - unsigned int min_in_flight = UINT_MAX, max_in_flight = 0; + unsigned int min_in_flight = UINT_MAX; struct TCP_Server_Info *server = NULL; int i, start, cur; @@ -1056,6 +1056,9 @@ struct TCP_Server_Info *cifs_pick_channel(struct cifs_ses *ses) if (!server) continue; + if (server->tcpStatus == CifsNeedReconnect) + continue; + /* * strictly speaking, we should pick up req_lock to read * server->in_flight. But it shouldn't matter much here if we @@ -1068,13 +1071,7 @@ struct TCP_Server_Info *cifs_pick_channel(struct cifs_ses *ses) min_in_flight = server->in_flight; index = cur; } - if (server->in_flight > max_in_flight) - max_in_flight = server->in_flight; } - - /* if all channels are equally loaded, fall back to round-robin */ - if (min_in_flight == max_in_flight) - index = (uint)start % ses->chan_count; } spin_unlock(&ses->chan_lock); return ses->chans[index].server; From a946265a2ae03f1ab0618ae17d51d7a5f54071dd Mon Sep 17 00:00:00 2001 From: Shyam Prasad N Date: Sat, 12 Sep 2026 03:16:48 -0300 Subject: [PATCH 06/16] BACKPORT: cifs: add per-channel reconnect state Add the per-channel reconnect bitmap and helpers needed to keep an SMB session usable while an individual transport reconnects. Signed-off-by: Shyam Prasad N Change-Id: I75c5f2aec2c82ef678fde796c794f8efb4cd2f50 (cherry picked from commit d1a931ce2e3b7761d293ba8e0bde2b0180f456e9) (cherry picked from commit f486ef8e2003f6c308d0db81ea116c880a760d4f) [juan: adapted the listed upstream change(s) to the CoreELEC 5.15.196 vendor layout; no original code authorship is claimed.] Signed-off-by: Juan Ruiz <6059278+juanruizm001@users.noreply.github.com> --- fs/cifs/cifsglob.h | 26 ++++++++++++- fs/cifs/cifsproto.h | 12 ++++++ fs/cifs/connect.c | 1 + fs/cifs/sess.c | 93 ++++++++++++++++++++++++++++++++++++++++++--- fs/cifs/smb2pdu.c | 10 ++++- 5 files changed, 135 insertions(+), 7 deletions(-) diff --git a/fs/cifs/cifsglob.h b/fs/cifs/cifsglob.h index 68b9b382d44bf..70083cbd83eba 100644 --- a/fs/cifs/cifsglob.h +++ b/fs/cifs/cifsglob.h @@ -112,7 +112,13 @@ enum statusEnum { CifsGood, CifsExiting, CifsNeedReconnect, - CifsNeedNegotiate + CifsNeedNegotiate, + CifsInNegotiate, + CifsNeedSessSetup, + CifsInSessSetup, + CifsNeedTcon, + CifsInTcon, + CifsInFilesInvalidate }; enum securityEnum { @@ -959,11 +965,29 @@ struct cifs_ses { spinlock_t chan_lock; /* ========= begin: protected by chan_lock ======== */ #define CIFS_MAX_CHANNELS 16 +#define CIFS_ALL_CHANNELS_SET(ses) \ + ((1UL << (ses)->chan_count) - 1) +#define CIFS_ALL_CHANS_NEED_RECONNECT(ses) \ + ((ses)->chans_need_reconnect == CIFS_ALL_CHANNELS_SET(ses)) +#define CIFS_CHAN_NEEDS_RECONNECT(ses, index) \ + test_bit((index), &(ses)->chans_need_reconnect) +#define CIFS_SET_ALL_CHANS_NEED_RECONNECT(ses) \ + ((ses)->chans_need_reconnect = CIFS_ALL_CHANNELS_SET(ses)) + struct cifs_chan chans[CIFS_MAX_CHANNELS]; struct cifs_chan *binding_chan; size_t chan_count; size_t chan_max; atomic_t chan_seq; /* round robin state */ + + /* + * Bitmap of channels that require connection/session recovery. + * + * Track reconnect state per channel while another established transport + * can keep the SMB session usable. The bitmap is protected by chan_lock. + * Keep channel eligibility separate from the session-wide state. + */ + unsigned long chans_need_reconnect; /* ========= end: protected by chan_lock ======== */ }; diff --git a/fs/cifs/cifsproto.h b/fs/cifs/cifsproto.h index 7d00802f97223..1b6ba7907cc69 100644 --- a/fs/cifs/cifsproto.h +++ b/fs/cifs/cifsproto.h @@ -594,6 +594,18 @@ void rqst_page_get_length(const struct smb_rqst *rqst, unsigned int page, unsigned int *len, unsigned int *offset); struct cifs_chan * cifs_ses_find_chan(struct cifs_ses *ses, struct TCP_Server_Info *server); +unsigned int +cifs_ses_get_chan_index(struct cifs_ses *ses, + struct TCP_Server_Info *server); +void +cifs_chan_set_need_reconnect(struct cifs_ses *ses, + struct TCP_Server_Info *server); +void +cifs_chan_clear_need_reconnect(struct cifs_ses *ses, + struct TCP_Server_Info *server); +bool +cifs_chan_needs_reconnect(struct cifs_ses *ses, + struct TCP_Server_Info *server); int cifs_try_adding_channels(struct cifs_sb_info *cifs_sb, struct cifs_ses *ses); bool is_server_using_iface(struct TCP_Server_Info *server, struct cifs_server_iface *iface); diff --git a/fs/cifs/connect.c b/fs/cifs/connect.c index 9be1c5244dc80..a181cbe364892 100644 --- a/fs/cifs/connect.c +++ b/fs/cifs/connect.c @@ -2057,6 +2057,7 @@ cifs_get_smb_ses(struct TCP_Server_Info *server, struct smb3_fs_context *ctx) ses->chans[0].server = server; ses->chan_count = 1; ses->chan_max = ctx->multichannel ? ctx->max_channels:1; + ses->chans_need_reconnect = 1; spin_unlock(&ses->chan_lock); rc = cifs_negotiate_protocol(xid, ses); diff --git a/fs/cifs/sess.c b/fs/cifs/sess.c index ab1dac4834f52..00f977c194a1a 100644 --- a/fs/cifs/sess.c +++ b/fs/cifs/sess.c @@ -65,6 +65,64 @@ bool is_ses_using_iface(struct cifs_ses *ses, struct cifs_server_iface *iface) return false; } +/* + * The per-channel reconnect helpers below are called with ses->chan_lock held. + * Keep an unknown server fail-closed: it must never be treated as a healthy + * channel and must never silently alias channel zero. + */ +unsigned int +cifs_ses_get_chan_index(struct cifs_ses *ses, struct TCP_Server_Info *server) +{ + unsigned int i; + + for (i = 0; i < ses->chan_count; i++) { + if (ses->chans[i].server == server) + return i; + } + + return CIFS_MAX_CHANNELS; +} + +void +cifs_chan_set_need_reconnect(struct cifs_ses *ses, + struct TCP_Server_Info *server) +{ + unsigned int chan_index = cifs_ses_get_chan_index(ses, server); + + if (WARN_ON_ONCE(chan_index >= ses->chan_count)) + return; + + set_bit(chan_index, &ses->chans_need_reconnect); + cifs_dbg(FYI, "Set reconnect bit for chan %d; mask=0x%lx\n", + chan_index, ses->chans_need_reconnect); +} + +void +cifs_chan_clear_need_reconnect(struct cifs_ses *ses, + struct TCP_Server_Info *server) +{ + unsigned int chan_index = cifs_ses_get_chan_index(ses, server); + + if (WARN_ON_ONCE(chan_index >= ses->chan_count)) + return; + + clear_bit(chan_index, &ses->chans_need_reconnect); + cifs_dbg(FYI, "Cleared reconnect bit for chan %d; mask=0x%lx\n", + chan_index, ses->chans_need_reconnect); +} + +bool +cifs_chan_needs_reconnect(struct cifs_ses *ses, + struct TCP_Server_Info *server) +{ + unsigned int chan_index = cifs_ses_get_chan_index(ses, server); + + if (WARN_ON_ONCE(chan_index >= ses->chan_count)) + return true; + + return CIFS_CHAN_NEEDS_RECONNECT(ses, chan_index); +} + /* returns number of channels added */ int cifs_try_adding_channels(struct cifs_sb_info *cifs_sb, struct cifs_ses *ses) { @@ -193,6 +251,7 @@ cifs_ses_add_channel(struct cifs_sb_info *cifs_sb, struct cifs_ses *ses, char unc[sizeof(unc_fmt)+SERVER_NAME_LEN_WITH_NULL] = {0}; struct sockaddr_in *ipv4 = (struct sockaddr_in *)&iface->sockaddr; struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)&iface->sockaddr; + bool chan_registered = false; int rc; unsigned int xid = get_xid(); @@ -274,6 +333,15 @@ cifs_ses_add_channel(struct cifs_sb_info *cifs_sb, struct cifs_ses *ses, spin_unlock(&ses->chan_lock); goto out; } + + /* + * Register the channel before negotiate/session setup so its reconnect + * bit has a stable index throughout channel binding. + */ + ses->chan_count++; + atomic_set(&ses->chan_seq, 0); + cifs_chan_set_need_reconnect(ses, chan->server); + chan_registered = true; spin_unlock(&ses->chan_lock); spin_lock(&cifs_tcp_ses_lock); @@ -310,12 +378,18 @@ cifs_ses_add_channel(struct cifs_sb_info *cifs_sb, struct cifs_ses *ses, * ses to the new server. */ - spin_lock(&ses->chan_lock); - ses->chan_count++; - atomic_set(&ses->chan_seq, 0); - spin_unlock(&ses->chan_lock); - out: + if (rc && chan_registered) { + spin_lock(&ses->chan_lock); + cifs_chan_clear_need_reconnect(ses, chan->server); + WARN_ON_ONCE(!ses->chan_count || + &ses->chans[ses->chan_count - 1] != chan); + if (ses->chan_count && + &ses->chans[ses->chan_count - 1] == chan) + ses->chan_count--; + spin_unlock(&ses->chan_lock); + } + ses->binding = false; ses->binding_chan = NULL; mutex_unlock(&ses->session_mutex); @@ -933,6 +1007,15 @@ sess_establish_session(struct sess_data *sess_data) mutex_unlock(&ses->server->srv_mutex); cifs_dbg(FYI, "CIFS session established successfully\n"); + + spin_lock(&ses->chan_lock); + if (ses->binding) + cifs_chan_clear_need_reconnect(ses, ses->binding_chan->server); + else + cifs_chan_clear_need_reconnect(ses, ses->server); + spin_unlock(&ses->chan_lock); + + /* Keep the legacy session state until the reconnect engine is ported. */ spin_lock(&GlobalMid_Lock); ses->status = CifsGood; ses->need_reconnect = false; diff --git a/fs/cifs/smb2pdu.c b/fs/cifs/smb2pdu.c index 302c08dfb686d..2e3af3f784bf9 100644 --- a/fs/cifs/smb2pdu.c +++ b/fs/cifs/smb2pdu.c @@ -1378,7 +1378,15 @@ SMB2_sess_establish_session(struct SMB2_sess_data *sess_data) mutex_unlock(&server->srv_mutex); cifs_dbg(FYI, "SMB2/3 session established successfully\n"); - /* keep existing ses state if binding */ + + spin_lock(&ses->chan_lock); + if (ses->binding) + cifs_chan_clear_need_reconnect(ses, ses->binding_chan->server); + else + cifs_chan_clear_need_reconnect(ses, ses->server); + spin_unlock(&ses->chan_lock); + + /* Keep the legacy session state until the reconnect engine is ported. */ if (!ses->binding) { spin_lock(&GlobalMid_Lock); ses->status = CifsGood; From 7f7e12f605922c82d18871819dae85a0a3fecd08 Mon Sep 17 00:00:00 2001 From: Shyam Prasad N Date: Sat, 12 Sep 2026 03:16:49 -0300 Subject: [PATCH 07/16] BACKPORT: cifs: reconnect individual multichannel transports Adapt the reconnect engine so one failed TCP transport can recover while the SMB session remains usable through another established channel. Keep socket teardown owned by the channel's cifsd thread. Signed-off-by: Shyam Prasad N Change-Id: I28fd6813fc1a16f42f8f0ce8c55fad736ff115bb (cherry picked from commit 183eea2ee5ba968ca7c31f04a0f01fd3e5c1d014) (cherry picked from commit 3663c9045f51a7ad635a0785adef07c21b79b560) (cherry picked from commit a81da65fbae6436e1e2f415532b8aacc3274d840) (cherry picked from commit 489f710a738e24d887823a010b8b206b4124e26f) (cherry picked from commit 2a05137a0575b7d1006bdf4c1beeee9e391e22a0) (cherry picked from commit 52492ff5c583036306bc422a83e246c971af387a) (cherry picked from commit dca65818c80cf06e0f08ba2cf94060a5236e73c2) [juan: adapted the listed upstream change(s) to the CoreELEC 5.15.196 vendor layout; no original code authorship is claimed.] Signed-off-by: Juan Ruiz <6059278+juanruizm001@users.noreply.github.com> --- fs/cifs/cifsglob.h | 7 ++ fs/cifs/cifsproto.h | 2 + fs/cifs/connect.c | 301 +++++++++++++++++++++++++++++++++----------- fs/cifs/misc.c | 1 + fs/cifs/sess.c | 1 + fs/cifs/smb1ops.c | 7 +- fs/cifs/smb2pdu.c | 242 ++++++++++++++++++++--------------- fs/cifs/transport.c | 4 +- 8 files changed, 386 insertions(+), 179 deletions(-) diff --git a/fs/cifs/cifsglob.h b/fs/cifs/cifsglob.h index 70083cbd83eba..74ad7fb709e48 100644 --- a/fs/cifs/cifsglob.h +++ b/fs/cifs/cifsglob.h @@ -696,6 +696,12 @@ struct TCP_Server_Info { int nr_targets; bool noblockcnt; /* use non-blocking connect() */ bool is_channel; /* if a session channel */ + /* + * Primary transport owning the SMB session list. Primary connections + * point to themselves; secondary multichannel transports point to the + * session's original transport. + */ + struct TCP_Server_Info *primary_server; #ifdef CONFIG_CIFS_SWN_UPCALL bool use_swn_dstaddr; struct sockaddr_storage swn_dstaddr; @@ -912,6 +918,7 @@ struct cifs_chan { */ struct cifs_ses { struct list_head smb_ses_list; + struct list_head rlist; /* reconnect list */ struct list_head tcon_list; struct cifs_tcon *tcon_ipc; struct mutex session_mutex; diff --git a/fs/cifs/cifsproto.h b/fs/cifs/cifsproto.h index 1b6ba7907cc69..d9e70882d9f27 100644 --- a/fs/cifs/cifsproto.h +++ b/fs/cifs/cifsproto.h @@ -132,6 +132,8 @@ extern int SendReceiveBlockingLock(const unsigned int xid, struct smb_hdr *out_buf, int *bytes_returned); extern int cifs_reconnect(struct TCP_Server_Info *server); +extern void cifs_signal_cifsd_for_reconnect(struct TCP_Server_Info *server, + bool all_channels); extern int checkSMB(char *buf, unsigned int len, struct TCP_Server_Info *srvr); extern bool is_valid_oplock_break(char *, struct TCP_Server_Info *); extern bool backup_cred(struct cifs_sb_info *); diff --git a/fs/cifs/connect.c b/fs/cifs/connect.c index a181cbe364892..61482a26a5cb4 100644 --- a/fs/cifs/connect.c +++ b/fs/cifs/connect.c @@ -166,51 +166,136 @@ static void cifs_resolve_server(struct work_struct *work) mutex_unlock(&server->srv_mutex); } -/** - * Mark all sessions and tcons for reconnect. +/* + * Mark one transport, or every transport under the same SMB session family, + * so that the owning cifsd thread performs the reconnect. * - * @server needs to be previously set to CifsNeedReconnect. + * This 5.15 tree predates the upstream per-server srv_lock and explicit + * channel parent linkage. Use primary_server and cifs_tcp_ses_lock for the + * equivalent tcpStatus transitions. */ -static void cifs_mark_tcp_ses_conns_for_reconnect(struct TCP_Server_Info *server) +void +cifs_signal_cifsd_for_reconnect(struct TCP_Server_Info *server, + bool all_channels) { - struct list_head *tmp, *tmp2; + struct TCP_Server_Info *pserver; + struct cifs_ses *ses; + int i; + + pserver = server->is_channel ? server->primary_server : server; + if (WARN_ON_ONCE(!pserver)) + return; + + spin_lock(&cifs_tcp_ses_lock); + if (!all_channels) { + /* The affected channel itself must be marked, never its primary. */ + if (server->tcpStatus != CifsExiting) + server->tcpStatus = CifsNeedReconnect; + spin_unlock(&cifs_tcp_ses_lock); + return; + } + + list_for_each_entry(ses, &pserver->smb_ses_list, smb_ses_list) { + spin_lock(&ses->chan_lock); + for (i = 0; i < ses->chan_count; i++) { + if (ses->chans[i].server && + ses->chans[i].server->tcpStatus != CifsExiting) + ses->chans[i].server->tcpStatus = CifsNeedReconnect; + } + spin_unlock(&ses->chan_lock); + } + spin_unlock(&cifs_tcp_ses_lock); +} + +/* + * Mark the SMB state affected by a transport reconnect. + * + * A single failed channel does not invalidate a healthy SMB session/tcon. + * Only a full-session reconnect, or loss of every channel, promotes the + * session and its tcons to CifsNeedReconnect. + * + * Must be called by the cifsd reconnect path, after server->tcpStatus has + * been moved to CifsNeedReconnect. + */ +static void +cifs_mark_tcp_ses_conns_for_reconnect(struct TCP_Server_Info *server, + bool mark_smb_session) +{ + struct TCP_Server_Info *pserver; struct cifs_ses *ses; struct cifs_tcon *tcon; - struct mid_q_entry *mid_entry; - struct list_head retry_list; - server->maxBuf = 0; - server->max_read = 0; + pserver = server->is_channel ? server->primary_server : server; + if (WARN_ON_ONCE(!pserver)) + return; + + cifs_dbg(FYI, "%s: marking necessary sessions/tcons for reconnect\n", + __func__); - cifs_dbg(FYI, "Mark tcp session as need reconnect\n"); - trace_smb3_reconnect(server->CurrentMid, server->conn_id, server->hostname); - /* - * before reconnecting the tcp session, mark the smb session (uid) and the tid bad so they - * are not used until reconnected. - */ - cifs_dbg(FYI, "%s: marking sessions and tcons for reconnect\n", __func__); spin_lock(&cifs_tcp_ses_lock); - list_for_each(tmp, &server->smb_ses_list) { - ses = list_entry(tmp, struct cifs_ses, smb_ses_list); + list_for_each_entry(ses, &pserver->smb_ses_list, smb_ses_list) { + unsigned int chan_index; + + spin_lock(&ses->chan_lock); + chan_index = cifs_ses_get_chan_index(ses, server); + + /* + * Secondary transports are session-private in this vendor tree. + * Other sessions may share the same primary transport, so skip + * them without calling the fail-closed helper on an unknown server. + */ + if (!mark_smb_session && chan_index >= ses->chan_count) + goto next_session; + + if (!mark_smb_session && + CIFS_CHAN_NEEDS_RECONNECT(ses, chan_index)) + goto next_session; + + if (mark_smb_session) + CIFS_SET_ALL_CHANS_NEED_RECONNECT(ses); + else + cifs_chan_set_need_reconnect(ses, server); + + if (!mark_smb_session && !CIFS_ALL_CHANS_NEED_RECONNECT(ses)) + goto next_session; + + /* No usable channel remains, so the SMB session/tcons are stale. */ ses->need_reconnect = true; - list_for_each(tmp2, &ses->tcon_list) { - tcon = list_entry(tmp2, struct cifs_tcon, tcon_list); + ses->status = CifsNeedReconnect; + list_for_each_entry(tcon, &ses->tcon_list, tcon_list) { tcon->need_reconnect = true; + tcon->tidStatus = CifsNeedReconnect; } - if (ses->tcon_ipc) + if (ses->tcon_ipc) { ses->tcon_ipc->need_reconnect = true; + ses->tcon_ipc->tidStatus = CifsNeedReconnect; + } + +next_session: + spin_unlock(&ses->chan_lock); } spin_unlock(&cifs_tcp_ses_lock); +} + +/* Tear down only the failed transport and retry its outstanding MIDs. */ +static void +cifs_abort_connection(struct TCP_Server_Info *server) +{ + struct list_head *tmp, *tmp2; + struct mid_q_entry *mid_entry; + struct list_head retry_list; + + server->maxBuf = 0; + server->max_read = 0; - /* do not want to be sending data on a socket we are freeing */ cifs_dbg(FYI, "%s: tearing down socket\n", __func__); mutex_lock(&server->srv_mutex); if (server->ssocket) { - cifs_dbg(FYI, "State: 0x%x Flags: 0x%lx\n", server->ssocket->state, - server->ssocket->flags); + cifs_dbg(FYI, "State: 0x%x Flags: 0x%lx\n", + server->ssocket->state, server->ssocket->flags); kernel_sock_shutdown(server->ssocket, SHUT_WR); - cifs_dbg(FYI, "Post shutdown state: 0x%x Flags: 0x%lx\n", server->ssocket->state, - server->ssocket->flags); + cifs_dbg(FYI, "Post shutdown state: 0x%x Flags: 0x%lx\n", + server->ssocket->state, server->ssocket->flags); sock_release(server->ssocket); server->ssocket = NULL; } @@ -221,7 +306,6 @@ static void cifs_mark_tcp_ses_conns_for_reconnect(struct TCP_Server_Info *server server->session_key.len = 0; server->lstrp = jiffies; - /* mark submitted MIDs for retry and issue callback */ INIT_LIST_HEAD(&retry_list); cifs_dbg(FYI, "%s: moving mids to private list\n", __func__); spin_lock(&GlobalMid_Lock); @@ -251,46 +335,55 @@ static void cifs_mark_tcp_ses_conns_for_reconnect(struct TCP_Server_Info *server } } -static bool cifs_tcp_ses_needs_reconnect(struct TCP_Server_Info *server, int num_targets) +static bool +cifs_tcp_ses_needs_reconnect(struct TCP_Server_Info *server, int num_targets) { - spin_lock(&GlobalMid_Lock); + spin_lock(&cifs_tcp_ses_lock); server->nr_targets = num_targets; if (server->tcpStatus == CifsExiting) { - /* the demux thread will exit normally next time through the loop */ - spin_unlock(&GlobalMid_Lock); + spin_unlock(&cifs_tcp_ses_lock); wake_up(&server->response_q); return false; } + + cifs_dbg(FYI, "Mark tcp session as need reconnect\n"); + trace_smb3_reconnect(server->CurrentMid, server->conn_id, + server->hostname); server->tcpStatus = CifsNeedReconnect; - spin_unlock(&GlobalMid_Lock); + spin_unlock(&cifs_tcp_ses_lock); return true; } /* - * cifs tcp session reconnection - * - * mark tcp session as reconnecting so temporarily locked - * mark all smb sessions as reconnecting for tcp session - * reconnect tcp session - * wake up waiters on reconnection? - (not needed currently) + * Reconnect one TCP transport. mark_smb_session=true means that the SMB + * session itself is invalid, therefore every channel must also be reset. */ -static int __cifs_reconnect(struct TCP_Server_Info *server) +static int +__cifs_reconnect(struct TCP_Server_Info *server, bool mark_smb_session) { int rc = 0; if (!cifs_tcp_ses_needs_reconnect(server, 1)) return 0; - cifs_mark_tcp_ses_conns_for_reconnect(server); + /* + * Session-wide failure: make every sibling cifsd observe reconnect + * before marking the SMB session/tcons stale. + */ + if (mark_smb_session) + cifs_signal_cifsd_for_reconnect(server, true); + + cifs_mark_tcp_ses_conns_for_reconnect(server, mark_smb_session); + cifs_abort_connection(server); do { try_to_freeze(); mutex_lock(&server->srv_mutex); if (!cifs_swn_set_server_dstaddr(server)) { - /* resolve the hostname again to make sure that IP address is up-to-date */ rc = reconn_set_ipaddr_from_hostname(server); - cifs_dbg(FYI, "%s: reconn_set_ipaddr_from_hostname: rc=%d\n", __func__, rc); + cifs_dbg(FYI, "%s: reconn_set_ipaddr_from_hostname: rc=%d\n", + __func__, rc); } if (cifs_rdma_enabled(server)) @@ -304,17 +397,22 @@ static int __cifs_reconnect(struct TCP_Server_Info *server) } else { atomic_inc(&tcpSesReconnectCount); set_credits(server, 1); - spin_lock(&GlobalMid_Lock); + spin_lock(&cifs_tcp_ses_lock); if (server->tcpStatus != CifsExiting) server->tcpStatus = CifsNeedNegotiate; - spin_unlock(&GlobalMid_Lock); + spin_unlock(&cifs_tcp_ses_lock); cifs_swn_reset_server_dstaddr(server); mutex_unlock(&server->srv_mutex); + + /* Rebind this channel even when all tcons stayed healthy. */ + mod_delayed_work(cifsiod_wq, &server->reconnect, 0); } } while (server->tcpStatus == CifsNeedReconnect); + spin_lock(&cifs_tcp_ses_lock); if (server->tcpStatus == CifsNeedNegotiate) mod_delayed_work(cifsiod_wq, &server->echo, 0); + spin_unlock(&cifs_tcp_ses_lock); wake_up(&server->response_q); return rc; @@ -376,7 +474,8 @@ static int reconnect_target_unlocked(struct TCP_Server_Info *server, struct dfs_ return rc; } -static int reconnect_dfs_server(struct TCP_Server_Info *server) +static int reconnect_dfs_server(struct TCP_Server_Info *server, + bool mark_smb_session) { int rc = 0; const char *refpath = server->current_fullpath + 1; @@ -400,7 +499,10 @@ static int reconnect_dfs_server(struct TCP_Server_Info *server) if (!cifs_tcp_ses_needs_reconnect(server, num_targets)) return 0; - cifs_mark_tcp_ses_conns_for_reconnect(server); + if (mark_smb_session) + cifs_signal_cifsd_for_reconnect(server, true); + cifs_mark_tcp_ses_conns_for_reconnect(server, mark_smb_session); + cifs_abort_connection(server); do { try_to_freeze(); @@ -421,12 +523,13 @@ static int reconnect_dfs_server(struct TCP_Server_Info *server) */ atomic_inc(&tcpSesReconnectCount); set_credits(server, 1); - spin_lock(&GlobalMid_Lock); + spin_lock(&cifs_tcp_ses_lock); if (server->tcpStatus != CifsExiting) server->tcpStatus = CifsNeedNegotiate; - spin_unlock(&GlobalMid_Lock); + spin_unlock(&cifs_tcp_ses_lock); cifs_swn_reset_server_dstaddr(server); mutex_unlock(&server->srv_mutex); + mod_delayed_work(cifsiod_wq, &server->reconnect, 0); } while (server->tcpStatus == CifsNeedReconnect); if (target_hint) @@ -444,20 +547,39 @@ static int reconnect_dfs_server(struct TCP_Server_Info *server) int cifs_reconnect(struct TCP_Server_Info *server) { - /* If tcp session is not an dfs connection, then reconnect to last target server */ + /* + * Keep the one-argument ABI for callers outside this source + * file, but keep socket teardown owned by cifsd. Unknown legacy callers + * are conservative and invalidate the whole SMB session. + */ + if (current != server->tsk) { + cifs_signal_cifsd_for_reconnect(server, true); + return 0; + } + spin_lock(&cifs_tcp_ses_lock); if (!server->is_dfs_conn || !server->origin_fullpath || !server->leaf_fullpath) { spin_unlock(&cifs_tcp_ses_lock); - return __cifs_reconnect(server); + return __cifs_reconnect(server, true); } spin_unlock(&cifs_tcp_ses_lock); - return reconnect_dfs_server(server); + return reconnect_dfs_server(server, true); } #else int cifs_reconnect(struct TCP_Server_Info *server) { - return __cifs_reconnect(server); + /* + * Keep the one-argument ABI while ensuring that non-cifsd + * callers only signal. Their legacy semantics are conservatively treated + * as session-wide reconnect requests. + */ + if (current != server->tsk) { + cifs_signal_cifsd_for_reconnect(server, true); + return 0; + } + + return __cifs_reconnect(server, true); } #endif @@ -545,7 +667,7 @@ server_unresponsive(struct TCP_Server_Info *server) time_after(jiffies, server->lstrp + 3 * server->echo_interval)) { cifs_server_dbg(VFS, "has not responded in %lu seconds. Reconnecting...\n", (3 * server->echo_interval) / HZ); - cifs_reconnect(server); + __cifs_reconnect(server, false); return true; } @@ -578,7 +700,7 @@ cifs_readv_from_socket(struct TCP_Server_Info *server, struct msghdr *smb_msg) /* reconnect if no credits and no requests in flight */ if (zero_credits(server)) { - cifs_reconnect(server); + __cifs_reconnect(server, false); return -ECONNABORTED; } @@ -593,7 +715,7 @@ cifs_readv_from_socket(struct TCP_Server_Info *server, struct msghdr *smb_msg) return -ESHUTDOWN; if (server->tcpStatus == CifsNeedReconnect) { - cifs_reconnect(server); + __cifs_reconnect(server, false); return -ECONNABORTED; } @@ -612,7 +734,7 @@ cifs_readv_from_socket(struct TCP_Server_Info *server, struct msghdr *smb_msg) if (length <= 0) { cifs_dbg(FYI, "Received no data or error: %d\n", length); - cifs_reconnect(server); + __cifs_reconnect(server, false); return -ECONNABORTED; } } @@ -1126,7 +1248,7 @@ cifs_demultiplex_thread(void *p) /* do this reconnect at the very end after processing all MIDs */ if (pending_reconnect) - cifs_reconnect(server); + __cifs_reconnect(server, false); } /* end while !EXITING */ @@ -1432,6 +1554,7 @@ cifs_get_tcp_session(struct smb3_fs_context *ctx) rc = -ENOMEM; goto out_err; } + tcp_ses->primary_server = tcp_ses; tcp_ses->hostname = kstrdup(ctx->server_hostname, GFP_KERNEL); if (!tcp_ses->hostname) { @@ -3813,18 +3936,28 @@ cifs_negotiate_protocol(const unsigned int xid, struct cifs_ses *ses) if (!server->ops->need_neg || !server->ops->negotiate) return -ENOSYS; - /* only send once per connect */ - if (!server->ops->need_neg(server)) + spin_lock(&cifs_tcp_ses_lock); + if (!server->ops->need_neg(server) || + server->tcpStatus != CifsNeedNegotiate) { + spin_unlock(&cifs_tcp_ses_lock); return 0; + } + server->tcpStatus = CifsInNegotiate; + spin_unlock(&cifs_tcp_ses_lock); rc = server->ops->negotiate(xid, ses); - if (rc == 0) { - spin_lock(&GlobalMid_Lock); - if (server->tcpStatus == CifsNeedNegotiate) - server->tcpStatus = CifsGood; + if (!rc) { + spin_lock(&cifs_tcp_ses_lock); + if (server->tcpStatus == CifsInNegotiate) + server->tcpStatus = CifsNeedSessSetup; else rc = -EHOSTDOWN; - spin_unlock(&GlobalMid_Lock); + spin_unlock(&cifs_tcp_ses_lock); + } else { + spin_lock(&cifs_tcp_ses_lock); + if (server->tcpStatus == CifsInNegotiate) + server->tcpStatus = CifsNeedNegotiate; + spin_unlock(&cifs_tcp_ses_lock); } return rc; @@ -3837,6 +3970,19 @@ cifs_setup_session(const unsigned int xid, struct cifs_ses *ses, int rc = -ENOSYS; struct TCP_Server_Info *server = cifs_ses_server(ses); + /* + * On a healthy session, only the channel in CifsNeedSessSetup should + * perform another Session Setup/bind. + */ + spin_lock(&cifs_tcp_ses_lock); + if (server->tcpStatus != CifsNeedSessSetup && + ses->status == CifsGood) { + spin_unlock(&cifs_tcp_ses_lock); + return 0; + } + server->tcpStatus = CifsInSessSetup; + spin_unlock(&cifs_tcp_ses_lock); + if (!ses->binding) { ses->capabilities = server->capabilities; if (!linuxExtEnabled) @@ -3857,8 +4003,24 @@ cifs_setup_session(const unsigned int xid, struct cifs_ses *ses, if (server->ops->sess_setup) rc = server->ops->sess_setup(xid, ses, nls_info); - if (rc) + if (rc) { cifs_server_dbg(VFS, "Send error in SessSetup = %d\n", rc); + spin_lock(&cifs_tcp_ses_lock); + if (server->tcpStatus == CifsInSessSetup) + server->tcpStatus = CifsNeedSessSetup; + spin_unlock(&cifs_tcp_ses_lock); + } else { + spin_lock(&cifs_tcp_ses_lock); + if (server->tcpStatus == CifsInSessSetup) + server->tcpStatus = CifsGood; + /* One live channel keeps the SMB session usable. */ + ses->status = CifsGood; + spin_unlock(&cifs_tcp_ses_lock); + + spin_lock(&ses->chan_lock); + cifs_chan_clear_need_reconnect(ses, server); + spin_unlock(&ses->chan_lock); + } return rc; } @@ -4128,14 +4290,7 @@ cifs_prune_tlinks(struct work_struct *work) #ifdef CONFIG_CIFS_DFS_UPCALL static void mark_tcon_tcp_ses_for_reconnect(struct cifs_tcon *tcon) { - int i; - - for (i = 0; i < tcon->ses->chan_count; i++) { - spin_lock(&GlobalMid_Lock); - if (tcon->ses->chans[i].server->tcpStatus != CifsExiting) - tcon->ses->chans[i].server->tcpStatus = CifsNeedReconnect; - spin_unlock(&GlobalMid_Lock); - } + cifs_signal_cifsd_for_reconnect(tcon->ses->server, true); } /* Update dfs referral path of superblock */ diff --git a/fs/cifs/misc.c b/fs/cifs/misc.c index a3d37e7769e61..297733ea197ef 100644 --- a/fs/cifs/misc.c +++ b/fs/cifs/misc.c @@ -72,6 +72,7 @@ sesInfoAlloc(void) ret_buf->status = CifsNew; ++ret_buf->ses_count; INIT_LIST_HEAD(&ret_buf->smb_ses_list); + INIT_LIST_HEAD(&ret_buf->rlist); INIT_LIST_HEAD(&ret_buf->tcon_list); mutex_init(&ret_buf->session_mutex); spin_lock_init(&ret_buf->iface_lock); diff --git a/fs/cifs/sess.c b/fs/cifs/sess.c index 00f977c194a1a..dd1b8a82653fb 100644 --- a/fs/cifs/sess.c +++ b/fs/cifs/sess.c @@ -346,6 +346,7 @@ cifs_ses_add_channel(struct cifs_sb_info *cifs_sb, struct cifs_ses *ses, spin_lock(&cifs_tcp_ses_lock); chan->server->is_channel = true; + chan->server->primary_server = ses->server; spin_unlock(&cifs_tcp_ses_lock); /* diff --git a/fs/cifs/smb1ops.c b/fs/cifs/smb1ops.c index 41e468d06b5b0..65b0ff1af4050 100644 --- a/fs/cifs/smb1ops.c +++ b/fs/cifs/smb1ops.c @@ -164,6 +164,7 @@ cifs_get_next_mid(struct TCP_Server_Info *server) __u64 mid = 0; __u16 last_mid, cur_mid; bool collision; + bool reconnect = false; spin_lock(&GlobalMid_Lock); @@ -215,7 +216,7 @@ cifs_get_next_mid(struct TCP_Server_Info *server) * an eventual reconnect to clean out the pending_mid_q. */ if (num_mids > 32768) - server->tcpStatus = CifsNeedReconnect; + reconnect = true; if (!collision) { mid = (__u64)cur_mid; @@ -225,6 +226,10 @@ cifs_get_next_mid(struct TCP_Server_Info *server) cur_mid++; } spin_unlock(&GlobalMid_Lock); + + if (reconnect) + cifs_signal_cifsd_for_reconnect(server, false); + return mid; } diff --git a/fs/cifs/smb2pdu.c b/fs/cifs/smb2pdu.c index 2e3af3f784bf9..72f8b2fbed770 100644 --- a/fs/cifs/smb2pdu.c +++ b/fs/cifs/smb2pdu.c @@ -143,35 +143,22 @@ static int smb2_reconnect(__le16 smb2_command, struct cifs_tcon *tcon, struct TCP_Server_Info *server) { - int rc; + int rc = 0; struct nls_table *nls_codepage; struct cifs_ses *ses; int retries; + bool binding = false; - /* - * SMB2s NegProt, SessSetup, Logoff do not have tcon yet so - * check for tcp and smb session status done differently - * for those three - in the calling routine. - */ if (tcon == NULL) return 0; - /* - * Need to also skip SMB2_IOCTL because it is used for checking nested dfs links in - * cifs_tree_connect(). - */ if (smb2_command == SMB2_TREE_CONNECT || smb2_command == SMB2_IOCTL) return 0; if (tcon->tidStatus == CifsExiting) { - /* - * only tree disconnect, open, and write, - * (and ulogoff which does not have tcon) - * are allowed as we start force umount. - */ if ((smb2_command != SMB2_WRITE) && - (smb2_command != SMB2_CREATE) && - (smb2_command != SMB2_TREE_DISCONNECT)) { + (smb2_command != SMB2_CREATE) && + (smb2_command != SMB2_TREE_DISCONNECT)) { cifs_dbg(FYI, "can not send cmd %d while umounting\n", smb2_command); return -ENODEV; @@ -184,20 +171,8 @@ smb2_reconnect(__le16 smb2_command, struct cifs_tcon *tcon, ses = tcon->ses; retries = server->nr_targets; - /* - * Give demultiplex thread up to 10 seconds to each target available for - * reconnect -- should be greater than cifs socket timeout which is 7 - * seconds. - */ while (server->tcpStatus == CifsNeedReconnect) { - /* - * Return to caller for TREE_DISCONNECT and LOGOFF and CLOSE - * here since they are implicitly done when session drops. - */ switch (smb2_command) { - /* - * BB Should we keep oplock break and add flush to exceptions? - */ case SMB2_TREE_DISCONNECT: case SMB2_CANCEL: case SMB2_CLOSE: @@ -206,26 +181,22 @@ smb2_reconnect(__le16 smb2_command, struct cifs_tcon *tcon, } rc = wait_event_interruptible_timeout(server->response_q, - (server->tcpStatus != CifsNeedReconnect), + server->tcpStatus != + CifsNeedReconnect, 10 * HZ); if (rc < 0) { - cifs_dbg(FYI, "%s: aborting reconnect due to a received signal by the process\n", + cifs_dbg(FYI, + "%s: aborting reconnect due to process signal\n", __func__); return -ERESTARTSYS; } - /* are we still trying to reconnect? */ if (server->tcpStatus != CifsNeedReconnect) break; if (retries && --retries) continue; - /* - * on "soft" mounts we wait once. Hard mounts keep - * retrying until process is killed or server comes - * back on-line - */ if (!tcon->retry) { cifs_dbg(FYI, "gave up waiting on reconnect in smb_init\n"); return -EHOSTDOWN; @@ -233,58 +204,84 @@ smb2_reconnect(__le16 smb2_command, struct cifs_tcon *tcon, retries = server->nr_targets; } - if (!tcon->ses->need_reconnect && !tcon->need_reconnect) + spin_lock(&ses->chan_lock); + if (!cifs_chan_needs_reconnect(ses, server) && + !tcon->need_reconnect) { + spin_unlock(&ses->chan_lock); return 0; + } + spin_unlock(&ses->chan_lock); nls_codepage = load_nls_default(); - /* - * need to prevent multiple threads trying to simultaneously reconnect - * the same SMB session - */ - mutex_lock(&tcon->ses->session_mutex); + spin_lock(&cifs_tcp_ses_lock); + if (server->tcpStatus == CifsNeedReconnect) { + spin_unlock(&cifs_tcp_ses_lock); + rc = -EHOSTDOWN; + goto out; + } + spin_unlock(&cifs_tcp_ses_lock); /* - * Recheck after acquire mutex. If another thread is negotiating - * and the server never sends an answer the socket will be closed - * and tcpStatus set to reconnect. + * Serialize negotiate/session setup and re-check the channel bit after + * taking session_mutex because another reconnect worker may have won. */ - if (server->tcpStatus == CifsNeedReconnect) { - rc = -EHOSTDOWN; - mutex_unlock(&tcon->ses->session_mutex); + mutex_lock(&ses->session_mutex); + + spin_lock(&ses->chan_lock); + if (!cifs_chan_needs_reconnect(ses, server)) { + spin_unlock(&ses->chan_lock); + mutex_unlock(&ses->session_mutex); + + if (tcon->need_reconnect) + goto skip_sess_setup; goto out; } /* - * If we are reconnecting an extra channel, bind + * If another channel is still healthy, re-establish this transport as a + * binding to the existing SMB session. This applies to the primary too: + * after a primary-only failure, a surviving secondary keeps the session + * alive and the recovered primary must bind rather than create a new one. */ - if (server->is_channel) { + binding = !CIFS_ALL_CHANS_NEED_RECONNECT(ses); + spin_unlock(&ses->chan_lock); + + if (binding) { ses->binding = true; ses->binding_chan = cifs_ses_find_chan(ses, server); - } - - rc = cifs_negotiate_protocol(0, tcon->ses); - if (!rc && tcon->ses->need_reconnect) { - rc = cifs_setup_session(0, tcon->ses, nls_codepage); - if ((rc == -EACCES) && !tcon->retry) { - rc = -EHOSTDOWN; + if (!ses->binding_chan) { ses->binding = false; - ses->binding_chan = NULL; - mutex_unlock(&tcon->ses->session_mutex); - goto failed; - } else if (rc) { + rc = -EHOSTDOWN; mutex_unlock(&ses->session_mutex); goto out; } + } else { + ses->binding = false; + ses->binding_chan = NULL; } - /* - * End of channel binding - */ - ses->binding = false; - ses->binding_chan = NULL; - if (rc || !tcon->need_reconnect) { - mutex_unlock(&tcon->ses->session_mutex); + rc = cifs_negotiate_protocol(0, ses); + if (!rc) + rc = cifs_setup_session(0, ses, nls_codepage); + + if (binding) { + ses->binding = false; + ses->binding_chan = NULL; + } + + mutex_unlock(&ses->session_mutex); + + if (rc) { + if ((rc == -EACCES) && !tcon->retry) + rc = -EHOSTDOWN; + goto failed; + } + +skip_sess_setup: + mutex_lock(&ses->session_mutex); + if (!tcon->need_reconnect) { + mutex_unlock(&ses->session_mutex); goto out; } @@ -293,11 +290,10 @@ smb2_reconnect(__le16 smb2_command, struct cifs_tcon *tcon, tcon->need_reopen_files = true; rc = cifs_tree_connect(0, tcon, nls_codepage); - mutex_unlock(&tcon->ses->session_mutex); + mutex_unlock(&ses->session_mutex); cifs_dbg(FYI, "reconnect tcon rc = %d\n", rc); if (rc) { - /* If sess reconnected but tcon didn't, something strange ... */ pr_warn_once("reconnect tcon failed rc = %d\n", rc); goto out; } @@ -307,14 +303,6 @@ smb2_reconnect(__le16 smb2_command, struct cifs_tcon *tcon, atomic_inc(&tconInfoReconnectCount); out: - /* - * Check if handle based operation so we know whether we can continue - * or not without returning to caller to reset file handle. - */ - /* - * BB Is flush done by server on drop of tcp session? Should we special - * case it and skip above? - */ switch (smb2_command) { case SMB2_FLUSH: case SMB2_READ: @@ -3760,44 +3748,64 @@ void smb2_reconnect_server(struct work_struct *work) { struct TCP_Server_Info *server = container_of(work, struct TCP_Server_Info, reconnect.work); - struct cifs_ses *ses; + struct TCP_Server_Info *pserver; + struct cifs_ses *ses, *ses2; struct cifs_tcon *tcon, *tcon2; - struct list_head tmp_list; - int tcon_exist = false; + struct list_head tmp_list, tmp_ses_list; + bool tcon_exist = false, ses_exist = false; + bool tcon_selected; int rc; - int resched = false; + bool resched = false; + pserver = server->is_channel ? server->primary_server : server; + if (WARN_ON_ONCE(!pserver)) + return; - /* Prevent simultaneous reconnects that can corrupt tcon->rlist list */ - mutex_lock(&server->reconnect_mutex); + /* All channels belonging to one primary serialize reconnect collection. */ + mutex_lock(&pserver->reconnect_mutex); INIT_LIST_HEAD(&tmp_list); - cifs_dbg(FYI, "Need negotiate, reconnecting tcons\n"); + INIT_LIST_HEAD(&tmp_ses_list); + cifs_dbg(FYI, "Reconnecting tcons and channels\n"); spin_lock(&cifs_tcp_ses_lock); - list_for_each_entry(ses, &server->smb_ses_list, smb_ses_list) { + list_for_each_entry(ses, &pserver->smb_ses_list, smb_ses_list) { + tcon_selected = false; + list_for_each_entry(tcon, &ses->tcon_list, tcon_list) { if (tcon->need_reconnect || tcon->need_reopen_files) { tcon->tc_count++; list_add_tail(&tcon->rlist, &tmp_list); - tcon_exist = true; + tcon_selected = tcon_exist = true; } } - /* - * IPC has the same lifetime as its session and uses its - * refcount. - */ + if (ses->tcon_ipc && ses->tcon_ipc->need_reconnect) { list_add_tail(&ses->tcon_ipc->rlist, &tmp_list); - tcon_exist = true; + tcon_selected = tcon_exist = true; ses->ses_count++; } + + /* + * Critical multichannel case: this channel needs Session Setup + * binding although all tcons remain healthy on another channel. + */ + spin_lock(&ses->chan_lock); + if (!tcon_selected) { + unsigned int chan_index; + + chan_index = cifs_ses_get_chan_index(ses, server); + if (chan_index < ses->chan_count && + CIFS_CHAN_NEEDS_RECONNECT(ses, chan_index)) { + list_add_tail(&ses->rlist, &tmp_ses_list); + ses_exist = true; + ses->ses_count++; + } + } + spin_unlock(&ses->chan_lock); } - /* - * Get the reference to server struct to be sure that the last call of - * cifs_put_tcon() in the loop below won't release the server pointer. - */ - if (tcon_exist) + + if (tcon_exist || ses_exist) server->srv_count++; spin_unlock(&cifs_tcp_ses_lock); @@ -3815,13 +3823,43 @@ void smb2_reconnect_server(struct work_struct *work) cifs_put_tcon(tcon); } - cifs_dbg(FYI, "Reconnecting tcons finished\n"); + if (!ses_exist) + goto done; + + tcon = kzalloc(sizeof(struct cifs_tcon), GFP_KERNEL); + if (!tcon) { + resched = true; + goto drain_sessions; + } + + tcon->tidStatus = CifsGood; + tcon->retry = false; + tcon->need_reconnect = false; + + list_for_each_entry_safe(ses, ses2, &tmp_ses_list, rlist) { + tcon->ses = ses; + rc = smb2_reconnect(SMB2_INTERNAL_CMD, tcon, server); + if (rc) + resched = true; + list_del_init(&ses->rlist); + cifs_put_smb_ses(ses); + } + kfree(tcon); + goto done; + +drain_sessions: + list_for_each_entry_safe(ses, ses2, &tmp_ses_list, rlist) { + list_del_init(&ses->rlist); + cifs_put_smb_ses(ses); + } + +done: + cifs_dbg(FYI, "Reconnecting tcons and channels finished\n"); if (resched) queue_delayed_work(cifsiod_wq, &server->reconnect, 2 * HZ); - mutex_unlock(&server->reconnect_mutex); + mutex_unlock(&pserver->reconnect_mutex); - /* now we can safely release srv struct */ - if (tcon_exist) + if (tcon_exist || ses_exist) cifs_put_tcp_session(server, 1); } diff --git a/fs/cifs/transport.c b/fs/cifs/transport.c index 4062a535980b1..0078d8c535d44 100644 --- a/fs/cifs/transport.c +++ b/fs/cifs/transport.c @@ -426,9 +426,7 @@ __smb_send_rqst(struct TCP_Server_Info *server, int num_rqst, * be taken as the remainder of this one. We need to kill the * socket so the server throws away the partial SMB */ - spin_lock(&GlobalMid_Lock); - server->tcpStatus = CifsNeedReconnect; - spin_unlock(&GlobalMid_Lock); + cifs_signal_cifsd_for_reconnect(server, false); trace_smb3_partial_send_reconnect(server->CurrentMid, server->conn_id, server->hostname); } From a02076c6c3d953ab293a6b645f63f4c29aaf8e02 Mon Sep 17 00:00:00 2001 From: Shyam Prasad N Date: Sat, 12 Sep 2026 03:16:50 -0300 Subject: [PATCH 08/16] BACKPORT: cifs: harden multichannel reconnect concurrency Adapt upstream locking and reconnect serialization to the vendor 5.15 structures so parallel reconnect activity does not race channel state. Signed-off-by: Shyam Prasad N Change-Id: I8f69312cb1299cb07fee9e53087d02c83ddbbca2 (cherry picked from commit 88b024f556fcd5bf1288c6333016f576cfa5f539) (cherry picked from commit 5752bf645f9dd7db600651f726eb04a97c9f597f) (cherry picked from commit bc962159e8e326af634a506508034a375bf2b858) (cherry picked from commit a96c94481f5993eac2271f9fb4d009b7dc076c24) [juan: adapted the listed upstream change(s) to the CoreELEC 5.15.196 vendor layout; no original code authorship is claimed.] Signed-off-by: Juan Ruiz <6059278+juanruizm001@users.noreply.github.com> --- fs/cifs/cifs_debug.c | 8 +- fs/cifs/cifsglob.h | 38 ++++++-- fs/cifs/cifsproto.h | 12 +++ fs/cifs/connect.c | 204 ++++++++++++++++++++++++++-------------- fs/cifs/misc.c | 3 +- fs/cifs/sess.c | 65 +++++++++++-- fs/cifs/smb2pdu.c | 122 ++++++++++++++---------- fs/cifs/smb2transport.c | 107 ++++++++++++--------- fs/cifs/transport.c | 10 +- 9 files changed, 381 insertions(+), 188 deletions(-) diff --git a/fs/cifs/cifs_debug.c b/fs/cifs/cifs_debug.c index dd39097027b5f..3482ea410e4dd 100644 --- a/fs/cifs/cifs_debug.c +++ b/fs/cifs/cifs_debug.c @@ -142,6 +142,8 @@ cifs_dump_channel(struct seq_file *m, int i, struct cifs_chan *chan) in_flight(server), atomic_read(&server->in_send), atomic_read(&server->num_waiters)); + if (chan->in_reconnect) + seq_puts(m, "\n\t\t[RECONNECTING]"); } static void @@ -382,7 +384,7 @@ static int cifs_debug_data_proc_show(struct seq_file *m, void *v) list_for_each(tmp2, &server->smb_ses_list) { ses = list_entry(tmp2, struct cifs_ses, smb_ses_list); - if (ses->status == CifsExiting) + if (cifs_ses_exiting(ses)) continue; i++; if ((ses->serverDomain == NULL) || @@ -390,7 +392,7 @@ static int cifs_debug_data_proc_show(struct seq_file *m, void *v) (ses->serverNOS == NULL)) { seq_printf(m, "\n\t%d) Address: %s Uses: %d Capability: 0x%x\tSession Status: %d ", i, ses->ip_addr, ses->ses_count, - ses->capabilities, ses->status); + ses->capabilities, cifs_get_ses_status(ses)); if (ses->session_flags & SMB2_SESSION_FLAG_IS_GUEST) seq_printf(m, "Guest "); else if (ses->session_flags & SMB2_SESSION_FLAG_IS_NULL) @@ -402,7 +404,7 @@ static int cifs_debug_data_proc_show(struct seq_file *m, void *v) "\n\tSMB session status: %d ", i, ses->ip_addr, ses->serverDomain, ses->ses_count, ses->serverOS, ses->serverNOS, - ses->capabilities, ses->status); + ses->capabilities, cifs_get_ses_status(ses)); } seq_printf(m, "\n\tSecurity type: %s ", diff --git a/fs/cifs/cifsglob.h b/fs/cifs/cifsglob.h index 74ad7fb709e48..93fc58ef0ea13 100644 --- a/fs/cifs/cifsglob.h +++ b/fs/cifs/cifsglob.h @@ -106,7 +106,7 @@ * CIFS vfs client Status information (based on what we know.) */ -/* associated with each tcp and smb session */ +/* associated with each transport connection */ enum statusEnum { CifsNew = 0, CifsGood, @@ -114,13 +114,20 @@ enum statusEnum { CifsNeedReconnect, CifsNeedNegotiate, CifsInNegotiate, - CifsNeedSessSetup, - CifsInSessSetup, CifsNeedTcon, CifsInTcon, CifsInFilesInvalidate }; +/* associated with each SMB session */ +enum ses_status_enum { + SES_NEW = 0, + SES_GOOD, + SES_EXITING, + SES_NEED_RECON, + SES_IN_SETUP +}; + enum securityEnum { Unspecified = 0, /* not specified */ NTLMv2, /* Legacy NTLM auth with NTLMv2 hash */ @@ -909,6 +916,7 @@ struct cifs_server_iface { }; struct cifs_chan { + unsigned int in_reconnect : 1; /* Session Setup in progress on this channel */ struct TCP_Server_Info *server; __u8 signkey[SMB3_SIGN_KEY_SIZE]; }; @@ -922,9 +930,10 @@ struct cifs_ses { struct list_head tcon_list; struct cifs_tcon *tcon_ipc; struct mutex session_mutex; + spinlock_t ses_lock; /* protects ses_status */ struct TCP_Server_Info *server; /* pointer to server info */ int ses_count; /* reference counter */ - enum statusEnum status; /* updates protected by GlobalMid_Lock */ + enum ses_status_enum ses_status; unsigned overrideSecFlg; /* if non-zero override global sec flags */ char *serverOS; /* name of operating system underlying server */ char *serverNOS; /* name of network operating system of server */ @@ -974,12 +983,16 @@ struct cifs_ses { #define CIFS_MAX_CHANNELS 16 #define CIFS_ALL_CHANNELS_SET(ses) \ ((1UL << (ses)->chan_count) - 1) +#define CIFS_ALL_CHANS_GOOD(ses) \ + (!(ses)->chans_need_reconnect) #define CIFS_ALL_CHANS_NEED_RECONNECT(ses) \ ((ses)->chans_need_reconnect == CIFS_ALL_CHANNELS_SET(ses)) #define CIFS_CHAN_NEEDS_RECONNECT(ses, index) \ test_bit((index), &(ses)->chans_need_reconnect) #define CIFS_SET_ALL_CHANS_NEED_RECONNECT(ses) \ ((ses)->chans_need_reconnect = CIFS_ALL_CHANNELS_SET(ses)) +#define CIFS_CHAN_IN_RECONNECT(ses, index) \ + ((ses)->chans[(index)].in_reconnect) struct cifs_chan chans[CIFS_MAX_CHANNELS]; struct cifs_chan *binding_chan; @@ -1830,7 +1843,7 @@ require use of the stronger protocol */ * list operations on pending_mid_q and oplockQ * updates to XID counters, multiplex id and SMB sequence numbers * list operations on global DnotifyReqList - * updates to ses->status and TCP_Server_Info->tcpStatus + * TCP_Server_Info->tcpStatus (session state uses ses->ses_lock) * updates to server->CurrentMid * tcp_ses_lock protects: * list operations on tcp and SMB session lists @@ -2073,12 +2086,19 @@ static inline struct scatterlist *cifs_sg_set_buf(struct scatterlist *sg, return sg; } -static inline bool cifs_ses_exiting(struct cifs_ses *ses) +static inline enum ses_status_enum cifs_get_ses_status(struct cifs_ses *ses) { - bool ret; + enum ses_status_enum status; - ret = ses->status == CifsExiting; - return ret; + spin_lock(&ses->ses_lock); + status = ses->ses_status; + spin_unlock(&ses->ses_lock); + return status; +} + +static inline bool cifs_ses_exiting(struct cifs_ses *ses) +{ + return cifs_get_ses_status(ses) == SES_EXITING; } #endif /* _CIFS_GLOB_H */ diff --git a/fs/cifs/cifsproto.h b/fs/cifs/cifsproto.h index d9e70882d9f27..0c41aac220073 100644 --- a/fs/cifs/cifsproto.h +++ b/fs/cifs/cifsproto.h @@ -599,6 +599,18 @@ cifs_ses_find_chan(struct cifs_ses *ses, struct TCP_Server_Info *server); unsigned int cifs_ses_get_chan_index(struct cifs_ses *ses, struct TCP_Server_Info *server); +struct cifs_chan * +cifs_ses_find_chan_locked(struct cifs_ses *ses, + struct TCP_Server_Info *server); +void +cifs_chan_set_in_reconnect(struct cifs_ses *ses, + struct TCP_Server_Info *server); +void +cifs_chan_clear_in_reconnect(struct cifs_ses *ses, + struct TCP_Server_Info *server); +bool +cifs_chan_in_reconnect(struct cifs_ses *ses, + struct TCP_Server_Info *server); void cifs_chan_set_need_reconnect(struct cifs_ses *ses, struct TCP_Server_Info *server); diff --git a/fs/cifs/connect.c b/fs/cifs/connect.c index 61482a26a5cb4..8b2569cfb10e5 100644 --- a/fs/cifs/connect.c +++ b/fs/cifs/connect.c @@ -196,6 +196,8 @@ cifs_signal_cifsd_for_reconnect(struct TCP_Server_Info *server, } list_for_each_entry(ses, &pserver->smb_ses_list, smb_ses_list) { + if (cifs_ses_exiting(ses)) + continue; spin_lock(&ses->chan_lock); for (i = 0; i < ses->chan_count; i++) { if (ses->chans[i].server && @@ -235,33 +237,41 @@ cifs_mark_tcp_ses_conns_for_reconnect(struct TCP_Server_Info *server, spin_lock(&cifs_tcp_ses_lock); list_for_each_entry(ses, &pserver->smb_ses_list, smb_ses_list) { unsigned int chan_index; + bool all_channels_down; + + if (cifs_ses_exiting(ses)) + continue; spin_lock(&ses->chan_lock); chan_index = cifs_ses_get_chan_index(ses, server); - - /* - * Secondary transports are session-private in this vendor tree. - * Other sessions may share the same primary transport, so skip - * them without calling the fail-closed helper on an unknown server. - */ - if (!mark_smb_session && chan_index >= ses->chan_count) - goto next_session; - + if (!mark_smb_session && chan_index >= ses->chan_count) { + spin_unlock(&ses->chan_lock); + continue; + } if (!mark_smb_session && - CIFS_CHAN_NEEDS_RECONNECT(ses, chan_index)) - goto next_session; + CIFS_CHAN_NEEDS_RECONNECT(ses, chan_index)) { + spin_unlock(&ses->chan_lock); + continue; + } if (mark_smb_session) CIFS_SET_ALL_CHANS_NEED_RECONNECT(ses); else cifs_chan_set_need_reconnect(ses, server); - if (!mark_smb_session && !CIFS_ALL_CHANS_NEED_RECONNECT(ses)) - goto next_session; + all_channels_down = CIFS_ALL_CHANS_NEED_RECONNECT(ses); + spin_unlock(&ses->chan_lock); + + if (!mark_smb_session && !all_channels_down) + continue; + + spin_lock(&ses->ses_lock); + if (ses->ses_status != SES_EXITING) { + ses->need_reconnect = true; + ses->ses_status = SES_NEED_RECON; + } + spin_unlock(&ses->ses_lock); - /* No usable channel remains, so the SMB session/tcons are stale. */ - ses->need_reconnect = true; - ses->status = CifsNeedReconnect; list_for_each_entry(tcon, &ses->tcon_list, tcon_list) { tcon->need_reconnect = true; tcon->tidStatus = CifsNeedReconnect; @@ -270,9 +280,6 @@ cifs_mark_tcp_ses_conns_for_reconnect(struct TCP_Server_Info *server, ses->tcon_ipc->need_reconnect = true; ses->tcon_ipc->tidStatus = CifsNeedReconnect; } - -next_session: - spin_unlock(&ses->chan_lock); } spin_unlock(&cifs_tcp_ses_lock); } @@ -558,12 +565,23 @@ int cifs_reconnect(struct TCP_Server_Info *server) } spin_lock(&cifs_tcp_ses_lock); - if (!server->is_dfs_conn || !server->origin_fullpath || !server->leaf_fullpath) { + if (!server->is_dfs_conn) { spin_unlock(&cifs_tcp_ses_lock); return __cifs_reconnect(server, true); } spin_unlock(&cifs_tcp_ses_lock); + /* + * origin_fullpath/leaf_fullpath belong to refpath_lock, not the + * transport/session-list spinlock. + */ + mutex_lock(&server->refpath_lock); + if (!server->origin_fullpath || !server->leaf_fullpath) { + mutex_unlock(&server->refpath_lock); + return __cifs_reconnect(server, true); + } + mutex_unlock(&server->refpath_lock); + return reconnect_dfs_server(server, true); } #else @@ -1849,7 +1867,7 @@ cifs_find_smb_ses(struct TCP_Server_Info *server, struct smb3_fs_context *ctx) spin_lock(&cifs_tcp_ses_lock); list_for_each_entry(ses, &server->smb_ses_list, smb_ses_list) { - if (ses->status == CifsExiting) + if (cifs_ses_exiting(ses)) continue; if (!match_session(ses, ctx)) continue; @@ -1866,40 +1884,41 @@ void cifs_put_smb_ses(struct cifs_ses *ses) unsigned int rc, xid; unsigned int chan_count; struct TCP_Server_Info *server = ses->server; + cifs_dbg(FYI, "%s: ses_count=%d\n", __func__, ses->ses_count); spin_lock(&cifs_tcp_ses_lock); - if (ses->status == CifsExiting) { + if (cifs_ses_exiting(ses)) { spin_unlock(&cifs_tcp_ses_lock); return; } - cifs_dbg(FYI, "%s: ses_count=%d\n", __func__, ses->ses_count); - cifs_dbg(FYI, "%s: ses ipc: %s\n", __func__, ses->tcon_ipc ? ses->tcon_ipc->treeName : "NONE"); - if (--ses->ses_count > 0) { spin_unlock(&cifs_tcp_ses_lock); return; } + + /* Close the lookup/refcount UAF window before dropping the list lock. */ + spin_lock(&ses->ses_lock); + if (ses->ses_status != SES_EXITING) + ses->ses_status = SES_EXITING; + spin_unlock(&ses->ses_lock); spin_unlock(&cifs_tcp_ses_lock); - /* ses_count can never go negative */ WARN_ON(ses->ses_count < 0); - - spin_lock(&GlobalMid_Lock); - if (ses->status == CifsGood) - ses->status = CifsExiting; - spin_unlock(&GlobalMid_Lock); - cifs_free_ipc(ses); - if (ses->status == CifsExiting && server->ops->logoff) { + spin_lock(&ses->ses_lock); + if (ses->ses_status == SES_EXITING && server->ops->logoff) { + spin_unlock(&ses->ses_lock); xid = get_xid(); rc = server->ops->logoff(xid, ses); if (rc) cifs_server_dbg(VFS, "%s: Session Logoff failure rc=%d\n", - __func__, rc); + __func__, rc); _free_xid(xid); + } else { + spin_unlock(&ses->ses_lock); } spin_lock(&cifs_tcp_ses_lock); @@ -1910,19 +1929,19 @@ void cifs_put_smb_ses(struct cifs_ses *ses) chan_count = ses->chan_count; spin_unlock(&ses->chan_lock); - /* close any extra channels */ if (chan_count > 1) { int i; for (i = 1; i < chan_count; i++) { - /* - * note: for now, we're okay accessing ses->chans - * without chan_lock. But when chans can go away, we'll - * need to introduce ref counting to make sure that chan - * is not freed from under us. - */ - cifs_put_tcp_session(ses->chans[i].server, 0); + struct TCP_Server_Info *chan_server; + + spin_lock(&ses->chan_lock); + chan_server = ses->chans[i].server; ses->chans[i].server = NULL; + spin_unlock(&ses->chan_lock); + + if (chan_server) + cifs_put_tcp_session(chan_server, 0); } } @@ -2104,7 +2123,7 @@ cifs_get_smb_ses(struct TCP_Server_Info *server, struct smb3_fs_context *ctx) ses = cifs_find_smb_ses(server, ctx); if (ses) { cifs_dbg(FYI, "Existing smb sess found (status=%d)\n", - ses->status); + cifs_get_ses_status(ses)); mutex_lock(&ses->session_mutex); rc = cifs_negotiate_protocol(xid, ses); @@ -2115,7 +2134,9 @@ cifs_get_smb_ses(struct TCP_Server_Info *server, struct smb3_fs_context *ctx) free_xid(xid); return ERR_PTR(rc); } + spin_lock(&ses->ses_lock); if (ses->need_reconnect) { + spin_unlock(&ses->ses_lock); cifs_dbg(FYI, "Session needs reconnect\n"); rc = cifs_setup_session(xid, ses, ctx->local_nls); @@ -2126,6 +2147,8 @@ cifs_get_smb_ses(struct TCP_Server_Info *server, struct smb3_fs_context *ctx) free_xid(xid); return ERR_PTR(rc); } + } else { + spin_unlock(&ses->ses_lock); } mutex_unlock(&ses->session_mutex); @@ -2188,8 +2211,10 @@ cifs_get_smb_ses(struct TCP_Server_Info *server, struct smb3_fs_context *ctx) rc = cifs_setup_session(xid, ses, ctx->local_nls); /* each channel uses a different signing key */ + spin_lock(&ses->chan_lock); memcpy(ses->chans[0].signkey, ses->smb3signingkey, sizeof(ses->smb3signingkey)); + spin_unlock(&ses->chan_lock); mutex_unlock(&ses->session_mutex); if (rc) @@ -3937,11 +3962,19 @@ cifs_negotiate_protocol(const unsigned int xid, struct cifs_ses *ses) return -ENOSYS; spin_lock(&cifs_tcp_ses_lock); - if (!server->ops->need_neg(server) || + if (server->tcpStatus != CifsGood && + server->tcpStatus != CifsNew && server->tcpStatus != CifsNeedNegotiate) { + spin_unlock(&cifs_tcp_ses_lock); + return -EHOSTDOWN; + } + + if (!server->ops->need_neg(server) && + server->tcpStatus == CifsGood) { spin_unlock(&cifs_tcp_ses_lock); return 0; } + server->tcpStatus = CifsInNegotiate; spin_unlock(&cifs_tcp_ses_lock); @@ -3949,7 +3982,7 @@ cifs_negotiate_protocol(const unsigned int xid, struct cifs_ses *ses) if (!rc) { spin_lock(&cifs_tcp_ses_lock); if (server->tcpStatus == CifsInNegotiate) - server->tcpStatus = CifsNeedSessSetup; + server->tcpStatus = CifsGood; else rc = -EHOSTDOWN; spin_unlock(&cifs_tcp_ses_lock); @@ -3968,22 +4001,52 @@ cifs_setup_session(const unsigned int xid, struct cifs_ses *ses, struct nls_table *nls_info) { int rc = -ENOSYS; + bool is_binding; struct TCP_Server_Info *server = cifs_ses_server(ses); /* - * On a healthy session, only the channel in CifsNeedSessSetup should - * perform another Session Setup/bind. + * Session setup is serialized by session_mutex at all call sites. Keep + * the session-state and per-channel state transition atomic with respect + * to teardown and parallel reconnect workers. */ - spin_lock(&cifs_tcp_ses_lock); - if (server->tcpStatus != CifsNeedSessSetup && - ses->status == CifsGood) { - spin_unlock(&cifs_tcp_ses_lock); + spin_lock(&ses->ses_lock); + if (ses->ses_status != SES_GOOD && + ses->ses_status != SES_NEW && + ses->ses_status != SES_NEED_RECON) { + spin_unlock(&ses->ses_lock); + return -EHOSTDOWN; + } + + spin_lock(&ses->chan_lock); + if (CIFS_ALL_CHANS_GOOD(ses)) { + if (ses->ses_status == SES_NEED_RECON) + ses->ses_status = SES_GOOD; + spin_unlock(&ses->chan_lock); + spin_unlock(&ses->ses_lock); return 0; } - server->tcpStatus = CifsInSessSetup; - spin_unlock(&cifs_tcp_ses_lock); - if (!ses->binding) { + cifs_chan_set_in_reconnect(ses, server); + is_binding = !CIFS_ALL_CHANS_NEED_RECONNECT(ses); + if (is_binding) { + ses->binding = true; + ses->binding_chan = cifs_ses_find_chan_locked(ses, server); + if (!ses->binding_chan) { + cifs_chan_clear_in_reconnect(ses, server); + ses->binding = false; + spin_unlock(&ses->chan_lock); + spin_unlock(&ses->ses_lock); + return -EHOSTDOWN; + } + } else { + ses->binding = false; + ses->binding_chan = NULL; + ses->ses_status = SES_IN_SETUP; + } + spin_unlock(&ses->chan_lock); + spin_unlock(&ses->ses_lock); + + if (!is_binding) { ses->capabilities = server->capabilities; if (!linuxExtEnabled) ses->capabilities &= (~server->vals->cap_unix); @@ -4003,25 +4066,30 @@ cifs_setup_session(const unsigned int xid, struct cifs_ses *ses, if (server->ops->sess_setup) rc = server->ops->sess_setup(xid, ses, nls_info); + spin_lock(&ses->ses_lock); + spin_lock(&ses->chan_lock); + cifs_chan_clear_in_reconnect(ses, server); + if (rc) { - cifs_server_dbg(VFS, "Send error in SessSetup = %d\n", rc); - spin_lock(&cifs_tcp_ses_lock); - if (server->tcpStatus == CifsInSessSetup) - server->tcpStatus = CifsNeedSessSetup; - spin_unlock(&cifs_tcp_ses_lock); + if (!is_binding && ses->ses_status == SES_IN_SETUP) + ses->ses_status = SES_NEED_RECON; } else { - spin_lock(&cifs_tcp_ses_lock); - if (server->tcpStatus == CifsInSessSetup) - server->tcpStatus = CifsGood; - /* One live channel keeps the SMB session usable. */ - ses->status = CifsGood; - spin_unlock(&cifs_tcp_ses_lock); - - spin_lock(&ses->chan_lock); cifs_chan_clear_need_reconnect(ses, server); - spin_unlock(&ses->chan_lock); + if (ses->ses_status != SES_EXITING) { + ses->ses_status = SES_GOOD; + ses->need_reconnect = false; + } } + if (is_binding) { + ses->binding = false; + ses->binding_chan = NULL; + } + spin_unlock(&ses->chan_lock); + spin_unlock(&ses->ses_lock); + + if (rc) + cifs_server_dbg(VFS, "Send error in SessSetup = %d\n", rc); return rc; } diff --git a/fs/cifs/misc.c b/fs/cifs/misc.c index 297733ea197ef..1eccd668b6aec 100644 --- a/fs/cifs/misc.c +++ b/fs/cifs/misc.c @@ -69,12 +69,13 @@ sesInfoAlloc(void) ret_buf = kzalloc(sizeof(struct cifs_ses), GFP_KERNEL); if (ret_buf) { atomic_inc(&sesInfoAllocCount); - ret_buf->status = CifsNew; + ret_buf->ses_status = SES_NEW; ++ret_buf->ses_count; INIT_LIST_HEAD(&ret_buf->smb_ses_list); INIT_LIST_HEAD(&ret_buf->rlist); INIT_LIST_HEAD(&ret_buf->tcon_list); mutex_init(&ret_buf->session_mutex); + spin_lock_init(&ret_buf->ses_lock); spin_lock_init(&ret_buf->iface_lock); spin_lock_init(&ret_buf->chan_lock); } diff --git a/fs/cifs/sess.c b/fs/cifs/sess.c index dd1b8a82653fb..1c00f496e4402 100644 --- a/fs/cifs/sess.c +++ b/fs/cifs/sess.c @@ -83,6 +83,49 @@ cifs_ses_get_chan_index(struct cifs_ses *ses, struct TCP_Server_Info *server) return CIFS_MAX_CHANNELS; } +struct cifs_chan * +cifs_ses_find_chan_locked(struct cifs_ses *ses, struct TCP_Server_Info *server) +{ + unsigned int chan_index = cifs_ses_get_chan_index(ses, server); + + if (chan_index >= ses->chan_count) + return NULL; + return &ses->chans[chan_index]; +} + +void +cifs_chan_set_in_reconnect(struct cifs_ses *ses, + struct TCP_Server_Info *server) +{ + unsigned int chan_index = cifs_ses_get_chan_index(ses, server); + + if (WARN_ON_ONCE(chan_index >= ses->chan_count)) + return; + ses->chans[chan_index].in_reconnect = true; +} + +void +cifs_chan_clear_in_reconnect(struct cifs_ses *ses, + struct TCP_Server_Info *server) +{ + unsigned int chan_index = cifs_ses_get_chan_index(ses, server); + + if (WARN_ON_ONCE(chan_index >= ses->chan_count)) + return; + ses->chans[chan_index].in_reconnect = false; +} + +bool +cifs_chan_in_reconnect(struct cifs_ses *ses, + struct TCP_Server_Info *server) +{ + unsigned int chan_index = cifs_ses_get_chan_index(ses, server); + + if (WARN_ON_ONCE(chan_index >= ses->chan_count)) + return true; + return CIFS_CHAN_IN_RECONNECT(ses, chan_index); +} + void cifs_chan_set_need_reconnect(struct cifs_ses *ses, struct TCP_Server_Info *server) @@ -360,7 +403,9 @@ cifs_ses_add_channel(struct cifs_sb_info *cifs_sb, struct cifs_ses *ses, goto out; } + spin_lock(&ses->chan_lock); ses->binding = true; + spin_unlock(&ses->chan_lock); rc = cifs_negotiate_protocol(xid, ses); if (rc) goto out; @@ -391,8 +436,10 @@ cifs_ses_add_channel(struct cifs_sb_info *cifs_sb, struct cifs_ses *ses, spin_unlock(&ses->chan_lock); } + spin_lock(&ses->chan_lock); ses->binding = false; ses->binding_chan = NULL; + spin_unlock(&ses->chan_lock); mutex_unlock(&ses->session_mutex); if (rc && chan->server) @@ -1010,17 +1057,21 @@ sess_establish_session(struct sess_data *sess_data) cifs_dbg(FYI, "CIFS session established successfully\n"); spin_lock(&ses->chan_lock); - if (ses->binding) + if (ses->binding) { + cifs_chan_clear_in_reconnect(ses, ses->binding_chan->server); cifs_chan_clear_need_reconnect(ses, ses->binding_chan->server); - else + } else { + cifs_chan_clear_in_reconnect(ses, ses->server); cifs_chan_clear_need_reconnect(ses, ses->server); + } spin_unlock(&ses->chan_lock); - /* Keep the legacy session state until the reconnect engine is ported. */ - spin_lock(&GlobalMid_Lock); - ses->status = CifsGood; - ses->need_reconnect = false; - spin_unlock(&GlobalMid_Lock); + spin_lock(&ses->ses_lock); + if (ses->ses_status != SES_EXITING) { + ses->ses_status = SES_GOOD; + ses->need_reconnect = false; + } + spin_unlock(&ses->ses_lock); return 0; } diff --git a/fs/cifs/smb2pdu.c b/fs/cifs/smb2pdu.c index 72f8b2fbed770..ef8bd03979f54 100644 --- a/fs/cifs/smb2pdu.c +++ b/fs/cifs/smb2pdu.c @@ -164,13 +164,15 @@ smb2_reconnect(__le16 smb2_command, struct cifs_tcon *tcon, return -ENODEV; } } - if ((!tcon->ses) || (tcon->ses->status == CifsExiting) || - (!tcon->ses->server) || !server) + if (!tcon->ses || !server) return -EIO; ses = tcon->ses; - retries = server->nr_targets; + if (cifs_ses_exiting(ses) || !ses->server) + return -EIO; +again: + retries = server->nr_targets; while (server->tcpStatus == CifsNeedReconnect) { switch (smb2_command) { case SMB2_TREE_DISCONNECT: @@ -213,73 +215,72 @@ smb2_reconnect(__le16 smb2_command, struct cifs_tcon *tcon, spin_unlock(&ses->chan_lock); nls_codepage = load_nls_default(); + mutex_lock(&ses->session_mutex); + /* + * Another reconnect can win while this thread waits for session_mutex. + * Recheck the transport after acquiring the serialization point. Hard + * mounts retry; soft/internal reconnects return and let the worker retry. + */ spin_lock(&cifs_tcp_ses_lock); if (server->tcpStatus == CifsNeedReconnect) { spin_unlock(&cifs_tcp_ses_lock); - rc = -EHOSTDOWN; - goto out; + mutex_unlock(&ses->session_mutex); + unload_nls(nls_codepage); + if (tcon->retry) + goto again; + return -EHOSTDOWN; } spin_unlock(&cifs_tcp_ses_lock); - /* - * Serialize negotiate/session setup and re-check the channel bit after - * taking session_mutex because another reconnect worker may have won. - */ - mutex_lock(&ses->session_mutex); - + spin_lock(&ses->ses_lock); spin_lock(&ses->chan_lock); - if (!cifs_chan_needs_reconnect(ses, server)) { + if (!cifs_chan_needs_reconnect(ses, server) && + ses->ses_status == SES_GOOD) { spin_unlock(&ses->chan_lock); - mutex_unlock(&ses->session_mutex); - + spin_unlock(&ses->ses_lock); if (tcon->need_reconnect) goto skip_sess_setup; + mutex_unlock(&ses->session_mutex); goto out; } - /* - * If another channel is still healthy, re-establish this transport as a - * binding to the existing SMB session. This applies to the primary too: - * after a primary-only failure, a surviving secondary keeps the session - * alive and the recovered primary must bind rather than create a new one. - */ binding = !CIFS_ALL_CHANS_NEED_RECONNECT(ses); - spin_unlock(&ses->chan_lock); - if (binding) { ses->binding = true; - ses->binding_chan = cifs_ses_find_chan(ses, server); + ses->binding_chan = cifs_ses_find_chan_locked(ses, server); if (!ses->binding_chan) { ses->binding = false; - rc = -EHOSTDOWN; + spin_unlock(&ses->chan_lock); + spin_unlock(&ses->ses_lock); mutex_unlock(&ses->session_mutex); + rc = -EHOSTDOWN; goto out; } } else { ses->binding = false; ses->binding_chan = NULL; } + spin_unlock(&ses->chan_lock); + spin_unlock(&ses->ses_lock); rc = cifs_negotiate_protocol(0, ses); if (!rc) rc = cifs_setup_session(0, ses, nls_codepage); - if (binding) { + /* Negotiate can fail before cifs_setup_session clears binding state. */ + if (rc) { + spin_lock(&ses->chan_lock); ses->binding = false; ses->binding_chan = NULL; - } - - mutex_unlock(&ses->session_mutex); - - if (rc) { + spin_unlock(&ses->chan_lock); + mutex_unlock(&ses->session_mutex); if ((rc == -EACCES) && !tcon->retry) rc = -EHOSTDOWN; goto failed; } skip_sess_setup: - mutex_lock(&ses->session_mutex); if (!tcon->need_reconnect) { mutex_unlock(&ses->session_mutex); goto out; @@ -294,7 +295,7 @@ smb2_reconnect(__le16 smb2_command, struct cifs_tcon *tcon, cifs_dbg(FYI, "reconnect tcon rc = %d\n", rc); if (rc) { - pr_warn_once("reconnect tcon failed rc = %d\n", rc); + cifs_dbg(VFS, "reconnect tcon failed rc = %d\n", rc); goto out; } @@ -1252,6 +1253,7 @@ SMB2_sess_alloc_buffer(struct SMB2_sess_data *sess_data) struct smb2_sess_setup_req *req; struct TCP_Server_Info *server = cifs_ses_server(ses); unsigned int total_len; + bool is_binding; rc = smb2_plain_req_init(SMB2_SESSION_SETUP, NULL, server, (void **) &req, @@ -1259,8 +1261,9 @@ SMB2_sess_alloc_buffer(struct SMB2_sess_data *sess_data) if (rc) return rc; - if (sess_data->ses->binding) { - req->sync_hdr.SessionId = sess_data->ses->Suid; + is_binding = cifs_get_ses_status(ses) == SES_GOOD; + if (is_binding) { + req->sync_hdr.SessionId = ses->Suid; req->sync_hdr.Flags |= SMB2_FLAGS_SIGNED; req->PreviousSessionId = 0; req->Flags = SMB2_SESSION_REQ_FLAG_BINDING; @@ -1367,19 +1370,28 @@ SMB2_sess_establish_session(struct SMB2_sess_data *sess_data) cifs_dbg(FYI, "SMB2/3 session established successfully\n"); - spin_lock(&ses->chan_lock); - if (ses->binding) - cifs_chan_clear_need_reconnect(ses, ses->binding_chan->server); - else - cifs_chan_clear_need_reconnect(ses, ses->server); - spin_unlock(&ses->chan_lock); + { + bool is_binding; - /* Keep the legacy session state until the reconnect engine is ported. */ - if (!ses->binding) { - spin_lock(&GlobalMid_Lock); - ses->status = CifsGood; - ses->need_reconnect = false; - spin_unlock(&GlobalMid_Lock); + spin_lock(&ses->chan_lock); + is_binding = ses->binding; + if (is_binding) { + cifs_chan_clear_in_reconnect(ses, ses->binding_chan->server); + cifs_chan_clear_need_reconnect(ses, ses->binding_chan->server); + } else { + cifs_chan_clear_in_reconnect(ses, ses->server); + cifs_chan_clear_need_reconnect(ses, ses->server); + } + spin_unlock(&ses->chan_lock); + + if (!is_binding) { + spin_lock(&ses->ses_lock); + if (ses->ses_status != SES_EXITING) { + ses->ses_status = SES_GOOD; + ses->need_reconnect = false; + } + spin_unlock(&ses->ses_lock); + } } return rc; @@ -1394,10 +1406,12 @@ SMB2_auth_kerberos(struct SMB2_sess_data *sess_data) struct cifs_spnego_msg *msg; struct key *spnego_key = NULL; struct smb2_sess_setup_rsp *rsp = NULL; + bool is_binding; rc = SMB2_sess_alloc_buffer(sess_data); if (rc) goto out; + is_binding = cifs_get_ses_status(ses) == SES_GOOD; spnego_key = cifs_get_spnego_key(ses); if (IS_ERR(spnego_key)) { @@ -1421,7 +1435,7 @@ SMB2_auth_kerberos(struct SMB2_sess_data *sess_data) } /* keep session key if binding */ - if (!ses->binding) { + if (!is_binding) { ses->auth_key.response = kmemdup(msg->data, msg->sesskey_len, GFP_KERNEL); if (!ses->auth_key.response) { @@ -1442,7 +1456,7 @@ SMB2_auth_kerberos(struct SMB2_sess_data *sess_data) rsp = (struct smb2_sess_setup_rsp *)sess_data->iov[0].iov_base; /* keep session id and flags if binding */ - if (!ses->binding) { + if (!is_binding) { ses->Suid = rsp->sync_hdr.SessionId; ses->session_flags = le16_to_cpu(rsp->SessionFlags); } @@ -1478,6 +1492,7 @@ SMB2_sess_auth_rawntlmssp_negotiate(struct SMB2_sess_data *sess_data) char *ntlmssp_blob = NULL; bool use_spnego = false; /* else use raw ntlmssp */ u16 blob_length = 0; + bool is_binding; /* * If memory allocation is successful, caller of this function @@ -1493,6 +1508,7 @@ SMB2_sess_auth_rawntlmssp_negotiate(struct SMB2_sess_data *sess_data) rc = SMB2_sess_alloc_buffer(sess_data); if (rc) goto out_err; + is_binding = cifs_get_ses_status(ses) == SES_GOOD; ntlmssp_blob = kmalloc(sizeof(struct _NEGOTIATE_MESSAGE), GFP_KERNEL); @@ -1540,7 +1556,7 @@ SMB2_sess_auth_rawntlmssp_negotiate(struct SMB2_sess_data *sess_data) cifs_dbg(FYI, "rawntlmssp session setup challenge phase\n"); /* keep existing ses id and flags if binding */ - if (!ses->binding) { + if (!is_binding) { ses->Suid = rsp->sync_hdr.SessionId; ses->session_flags = le16_to_cpu(rsp->SessionFlags); } @@ -1570,10 +1586,12 @@ SMB2_sess_auth_rawntlmssp_authenticate(struct SMB2_sess_data *sess_data) unsigned char *ntlmssp_blob = NULL; bool use_spnego = false; /* else use raw ntlmssp */ u16 blob_length = 0; + bool is_binding; rc = SMB2_sess_alloc_buffer(sess_data); if (rc) goto out; + is_binding = cifs_get_ses_status(ses) == SES_GOOD; req = (struct smb2_sess_setup_req *) sess_data->iov[0].iov_base; req->sync_hdr.SessionId = ses->Suid; @@ -1601,7 +1619,7 @@ SMB2_sess_auth_rawntlmssp_authenticate(struct SMB2_sess_data *sess_data) rsp = (struct smb2_sess_setup_rsp *)sess_data->iov[0].iov_base; /* keep existing ses id and flags if binding */ - if (!ses->binding) { + if (!is_binding) { ses->Suid = rsp->sync_hdr.SessionId; ses->session_flags = le16_to_cpu(rsp->SessionFlags); } @@ -3770,6 +3788,8 @@ void smb2_reconnect_server(struct work_struct *work) spin_lock(&cifs_tcp_ses_lock); list_for_each_entry(ses, &pserver->smb_ses_list, smb_ses_list) { + if (cifs_ses_exiting(ses)) + continue; tcon_selected = false; list_for_each_entry(tcon, &ses->tcon_list, tcon_list) { @@ -3875,7 +3895,7 @@ SMB2_echo(struct TCP_Server_Info *server) cifs_dbg(FYI, "In echo request\n"); - if (server->tcpStatus == CifsNeedNegotiate) { + if (server->ops->need_neg && server->ops->need_neg(server)) { /* No need to send echo on newly established connections */ mod_delayed_work(cifsiod_wq, &server->reconnect, 0); return rc; diff --git a/fs/cifs/smb2transport.c b/fs/cifs/smb2transport.c index ffae3a7f46ce4..d79d8909d57d8 100644 --- a/fs/cifs/smb2transport.c +++ b/fs/cifs/smb2transport.c @@ -83,17 +83,23 @@ int smb2_get_sign_key(__u64 ses_id, struct TCP_Server_Info *server, u8 *key) { struct cifs_chan *chan; struct cifs_ses *ses = NULL; - struct TCP_Server_Info *it = NULL; + struct TCP_Server_Info *pserver; int i; int rc = 0; + bool is_binding; spin_lock(&cifs_tcp_ses_lock); - list_for_each_entry(it, &cifs_tcp_ses_list, tcp_ses_list) { - list_for_each_entry(ses, &it->smb_ses_list, smb_ses_list) { - if (ses->Suid == ses_id) - goto found; - } + pserver = server->is_channel ? server->primary_server : server; + if (!pserver) { + rc = -ENOENT; + goto out; + } + + list_for_each_entry(ses, &pserver->smb_ses_list, smb_ses_list) { + if (ses->Suid != ses_id || cifs_ses_exiting(ses)) + continue; + goto found; } cifs_server_dbg(VFS, "%s: Could not find session 0x%llx\n", __func__, ses_id); @@ -101,27 +107,28 @@ int smb2_get_sign_key(__u64 ses_id, struct TCP_Server_Info *server, u8 *key) goto out; found: - if (ses->binding) { - /* - * If we are in the process of binding a new channel - * to an existing session, use the master connection - * session key - */ + spin_lock(&ses->ses_lock); + spin_lock(&ses->chan_lock); + is_binding = (cifs_chan_needs_reconnect(ses, server) && + ses->ses_status == SES_GOOD); + if (is_binding) { memcpy(key, ses->smb3signingkey, SMB3_SIGN_KEY_SIZE); + spin_unlock(&ses->chan_lock); + spin_unlock(&ses->ses_lock); goto out; } - /* - * Otherwise, use the channel key. - */ - for (i = 0; i < ses->chan_count; i++) { chan = ses->chans + i; if (chan->server == server) { memcpy(key, chan->signkey, SMB3_SIGN_KEY_SIZE); + spin_unlock(&ses->chan_lock); + spin_unlock(&ses->ses_lock); goto out; } } + spin_unlock(&ses->chan_lock); + spin_unlock(&ses->ses_lock); cifs_dbg(VFS, "%s: Could not find channel signing key for session 0x%llx\n", @@ -136,10 +143,15 @@ int smb2_get_sign_key(__u64 ses_id, struct TCP_Server_Info *server, u8 *key) static struct cifs_ses * smb2_find_smb_ses_unlocked(struct TCP_Server_Info *server, __u64 ses_id) { + struct TCP_Server_Info *pserver; struct cifs_ses *ses; - list_for_each_entry(ses, &server->smb_ses_list, smb_ses_list) { - if (ses->Suid != ses_id) + pserver = server->is_channel ? server->primary_server : server; + if (!pserver) + return NULL; + + list_for_each_entry(ses, &pserver->smb_ses_list, smb_ses_list) { + if (ses->Suid != ses_id || cifs_ses_exiting(ses)) continue; ++ses->ses_count; return ses; @@ -393,25 +405,32 @@ static int generate_smb3signingkey(struct cifs_ses *ses, const struct derivation_triplet *ptriplet) { + struct TCP_Server_Info *server = cifs_ses_server(ses); + unsigned int chan_index; + bool is_binding; int rc; -#ifdef CONFIG_CIFS_DEBUG_DUMP_KEYS - struct TCP_Server_Info *server = ses->server; -#endif + + spin_lock(&ses->ses_lock); + spin_lock(&ses->chan_lock); + chan_index = cifs_ses_get_chan_index(ses, server); + if (WARN_ON_ONCE(chan_index >= ses->chan_count)) { + spin_unlock(&ses->chan_lock); + spin_unlock(&ses->ses_lock); + return -EHOSTDOWN; + } + is_binding = (CIFS_CHAN_NEEDS_RECONNECT(ses, chan_index) && + ses->ses_status == SES_GOOD); + spin_unlock(&ses->chan_lock); + spin_unlock(&ses->ses_lock); /* - * All channels use the same encryption/decryption keys but - * they have their own signing key. - * - * When we generate the keys, check if it is for a new channel - * (binding) in which case we only need to generate a signing - * key and store it in the channel as to not overwrite the - * master connection signing key stored in the session + * Each channel has its own signing key. A binding must not overwrite + * the established SMB session encryption/decryption keys. */ - - if (ses->binding) { + if (is_binding) { rc = generate_key(ses, ptriplet->signing.label, ptriplet->signing.context, - cifs_ses_binding_channel(ses)->signkey, + ses->chans[chan_index].signkey, SMB3_SIGN_KEY_SIZE); if (rc) return rc; @@ -423,8 +442,10 @@ generate_smb3signingkey(struct cifs_ses *ses, if (rc) return rc; - memcpy(ses->chans[0].signkey, ses->smb3signingkey, + spin_lock(&ses->chan_lock); + memcpy(ses->chans[chan_index].signkey, ses->smb3signingkey, SMB3_SIGN_KEY_SIZE); + spin_unlock(&ses->chan_lock); rc = generate_key(ses, ptriplet->encryption.label, ptriplet->encryption.context, @@ -442,28 +463,24 @@ generate_smb3signingkey(struct cifs_ses *ses, #ifdef CONFIG_CIFS_DEBUG_DUMP_KEYS cifs_dbg(VFS, "%s: dumping generated AES session keys\n", __func__); - /* - * The session id is opaque in terms of endianness, so we can't - * print it as a long long. we dump it as we got it on the wire - */ cifs_dbg(VFS, "Session Id %*ph\n", (int)sizeof(ses->Suid), - &ses->Suid); + &ses->Suid); cifs_dbg(VFS, "Cipher type %d\n", server->cipher_type); cifs_dbg(VFS, "Session Key %*ph\n", SMB2_NTLMV2_SESSKEY_SIZE, ses->auth_key.response); cifs_dbg(VFS, "Signing Key %*ph\n", SMB3_SIGN_KEY_SIZE, ses->smb3signingkey); if ((server->cipher_type == SMB2_ENCRYPTION_AES256_CCM) || - (server->cipher_type == SMB2_ENCRYPTION_AES256_GCM)) { + (server->cipher_type == SMB2_ENCRYPTION_AES256_GCM)) { cifs_dbg(VFS, "ServerIn Key %*ph\n", - SMB3_GCM256_CRYPTKEY_SIZE, ses->smb3encryptionkey); + SMB3_GCM256_CRYPTKEY_SIZE, ses->smb3encryptionkey); cifs_dbg(VFS, "ServerOut Key %*ph\n", - SMB3_GCM256_CRYPTKEY_SIZE, ses->smb3decryptionkey); + SMB3_GCM256_CRYPTKEY_SIZE, ses->smb3decryptionkey); } else { cifs_dbg(VFS, "ServerIn Key %*ph\n", - SMB3_GCM128_CRYPTKEY_SIZE, ses->smb3encryptionkey); + SMB3_GCM128_CRYPTKEY_SIZE, ses->smb3encryptionkey); cifs_dbg(VFS, "ServerOut Key %*ph\n", - SMB3_GCM128_CRYPTKEY_SIZE, ses->smb3decryptionkey); + SMB3_GCM128_CRYPTKEY_SIZE, ses->smb3decryptionkey); } #endif return rc; @@ -626,7 +643,7 @@ smb2_sign_rqst(struct smb_rqst *rqst, struct TCP_Server_Info *server) if (!is_signed) return 0; - if (server->tcpStatus == CifsNeedNegotiate) + if (server->ops->need_neg && server->ops->need_neg(server)) return 0; if (!is_binding && !server->session_estab) { strncpy(shdr->Signature, "BSRSPYL", 8); @@ -754,14 +771,14 @@ smb2_get_mid_entry(struct cifs_ses *ses, struct TCP_Server_Info *server, shdr->Command != SMB2_NEGOTIATE) return -EAGAIN; - if (ses->status == CifsNew) { + if (cifs_get_ses_status(ses) == SES_NEW) { if ((shdr->Command != SMB2_SESSION_SETUP) && (shdr->Command != SMB2_NEGOTIATE)) return -EAGAIN; /* else ok - we are setting up session */ } - if (ses->status == CifsExiting) { + if (cifs_get_ses_status(ses) == SES_EXITING) { if (shdr->Command != SMB2_LOGOFF) return -EAGAIN; /* else ok - we are shutting down the session */ diff --git a/fs/cifs/transport.c b/fs/cifs/transport.c index 0078d8c535d44..298417106c7cd 100644 --- a/fs/cifs/transport.c +++ b/fs/cifs/transport.c @@ -727,14 +727,14 @@ static int allocate_mid(struct cifs_ses *ses, struct smb_hdr *in_buf, return -EAGAIN; } - if (ses->status == CifsNew) { + if (cifs_get_ses_status(ses) == SES_NEW) { if ((in_buf->Command != SMB_COM_SESSION_SETUP_ANDX) && (in_buf->Command != SMB_COM_NEGOTIATE)) return -EAGAIN; /* else ok - we are setting up session */ } - if (ses->status == CifsExiting) { + if (cifs_get_ses_status(ses) == SES_EXITING) { /* check if SMB session is bad because we are setting it up */ if (in_buf->Command != SMB_COM_LOGOFF_ANDX) return -EAGAIN; @@ -1206,7 +1206,8 @@ compound_send_recv(const unsigned int xid, struct cifs_ses *ses, /* * Compounding is never used during session establish. */ - if ((ses->status == CifsNew) || (optype & CIFS_NEG_OP) || (optype & CIFS_SESS_OP)) { + if ((cifs_get_ses_status(ses) == SES_NEW) || + (optype & CIFS_NEG_OP) || (optype & CIFS_SESS_OP)) { mutex_lock(&server->srv_mutex); smb311_update_preauth_hash(ses, rqst[0].rq_iov, rqst[0].rq_nvec); @@ -1275,7 +1276,8 @@ compound_send_recv(const unsigned int xid, struct cifs_ses *ses, /* * Compounding is never used during session establish. */ - if ((ses->status == CifsNew) || (optype & CIFS_NEG_OP) || (optype & CIFS_SESS_OP)) { + if ((cifs_get_ses_status(ses) == SES_NEW) || + (optype & CIFS_NEG_OP) || (optype & CIFS_SESS_OP)) { struct kvec iov = { .iov_base = resp_iov[0].iov_base, .iov_len = resp_iov[0].iov_len From 6e26c0ae16ce488657710f4041ad85a52acdd539 Mon Sep 17 00:00:00 2001 From: Shyam Prasad N Date: Sat, 12 Sep 2026 03:16:50 -0300 Subject: [PATCH 09/16] BACKPORT: cifs: integrate reconnect-aware channel selection Wire negotiate, session and tree-connect state transitions into the vendor layout. Keep signing state and request selection aligned with the transport that is reconnecting. Signed-off-by: Shyam Prasad N Change-Id: Ia6e3e22100535bbdc4dfd40080356c0da72bca88 (cherry picked from commit 73f9bfbe3d818bb52266d5c9f3ba57d97842ffe7) (cherry picked from commit 47de760655f329ce4b3d3e6276557220956d8c38) (cherry picked from commit a05885ce13bd5ec9602551e32dfb1a4f26bfa542) (cherry picked from commit ba978e83255a759a4a07257a46ca6396a8b81787) (cherry picked from commit 05ce0448c3f36febd8db0ee0e9e16557f3ab5ee8) [juan: adapted the listed upstream change(s) to the CoreELEC 5.15.196 vendor layout; no original code authorship is claimed.] Signed-off-by: Juan Ruiz <6059278+juanruizm001@users.noreply.github.com> --- fs/cifs/cifsglob.h | 5 +++ fs/cifs/connect.c | 1 + fs/cifs/smb2ops.c | 15 +++++++++ fs/cifs/smb2pdu.c | 35 +++++++++++++++++---- fs/cifs/transport.c | 76 +++++++++++++++++++++++++-------------------- 5 files changed, 92 insertions(+), 40 deletions(-) diff --git a/fs/cifs/cifsglob.h b/fs/cifs/cifsglob.h index 93fc58ef0ea13..59ada8f5aa5b7 100644 --- a/fs/cifs/cifsglob.h +++ b/fs/cifs/cifsglob.h @@ -709,6 +709,11 @@ struct TCP_Server_Info { * session's original transport. */ struct TCP_Server_Info *primary_server; + /* + * SMB3 ChannelSequence is primary/session scoped. Increment it whenever + * any channel reconnects and use the primary value on all channels. + */ + __u16 channel_sequence_num; #ifdef CONFIG_CIFS_SWN_UPCALL bool use_swn_dstaddr; struct sockaddr_storage swn_dstaddr; diff --git a/fs/cifs/connect.c b/fs/cifs/connect.c index 8b2569cfb10e5..b8f243e00865e 100644 --- a/fs/cifs/connect.c +++ b/fs/cifs/connect.c @@ -1606,6 +1606,7 @@ cifs_get_tcp_session(struct smb3_fs_context *ctx) ctx->target_rfc1001_name, RFC1001_NAME_LEN_WITH_NULL); tcp_ses->session_estab = false; tcp_ses->sequence_number = 0; + tcp_ses->channel_sequence_num = 0; tcp_ses->reconnect_instance = 1; tcp_ses->lstrp = jiffies; tcp_ses->compress_algorithm = cpu_to_le16(ctx->compression); diff --git a/fs/cifs/smb2ops.c b/fs/cifs/smb2ops.c index 619905fc694e4..94246935b0c4d 100644 --- a/fs/cifs/smb2ops.c +++ b/fs/cifs/smb2ops.c @@ -152,6 +152,7 @@ smb2_add_credits(struct TCP_Server_Info *server, static void smb2_set_credits(struct TCP_Server_Info *server, const int val) { + struct TCP_Server_Info *pserver; int scredits, in_flight; spin_lock(&server->req_lock); @@ -162,6 +163,20 @@ smb2_set_credits(struct TCP_Server_Info *server, const int val) in_flight = server->in_flight; spin_unlock(&server->req_lock); + /* + * ChannelSequence is shared by all channels of the SMB session. + * Serialize the update on the primary transport's req_lock so parallel + * channel reconnects cannot lose an increment. + */ + if (val == 1) { + pserver = server->is_channel ? server->primary_server : server; + if (pserver) { + spin_lock(&pserver->req_lock); + pserver->channel_sequence_num++; + spin_unlock(&pserver->req_lock); + } + } + trace_smb3_set_credits(server->CurrentMid, server->conn_id, server->hostname, scredits, val, in_flight); cifs_dbg(FYI, "%s: set %u credits\n", __func__, val); diff --git a/fs/cifs/smb2pdu.c b/fs/cifs/smb2pdu.c index ef8bd03979f54..0e6208e1c833c 100644 --- a/fs/cifs/smb2pdu.c +++ b/fs/cifs/smb2pdu.c @@ -88,10 +88,31 @@ smb2_hdr_assemble(struct smb2_sync_hdr *shdr, __le16 smb2_cmd, const struct cifs_tcon *tcon, struct TCP_Server_Info *server) { + struct TCP_Server_Info *pserver; + __u16 channel_sequence = 0; + shdr->ProtocolId = SMB2_PROTO_NUMBER; shdr->StructureSize = cpu_to_le16(64); shdr->Command = smb2_cmd; + + /* + * In responses bytes 8..11 are Status. For SMB3 requests the same four + * bytes are ChannelSequence + Reserved. Keep Reserved zero and store the + * primary-scoped sequence in the low 16 bits. Check server before + * dereferencing dialect (upstream 05d0f8f55ad6 safety ordering). + */ + shdr->Status = 0; if (server) { + if (server->dialect >= SMB30_PROT_ID) { + pserver = server->is_channel ? server->primary_server : server; + if (pserver) { + spin_lock(&pserver->req_lock); + channel_sequence = pserver->channel_sequence_num; + spin_unlock(&pserver->req_lock); + shdr->Status = cpu_to_le32((__u32)channel_sequence); + } + } + spin_lock(&server->req_lock); /* Request up to 10 credits but don't go over the limit. */ if (server->credits >= server->max_credits) @@ -1809,10 +1830,7 @@ SMB2_tcon(const unsigned int xid, struct cifs_ses *ses, const char *tree, __le16 *unc_path = NULL; int flags = 0; unsigned int total_len; - struct TCP_Server_Info *server; - - /* always use master channel */ - server = ses->server; + struct TCP_Server_Info *server = cifs_pick_channel(ses); cifs_dbg(FYI, "TCON\n"); @@ -1940,6 +1958,7 @@ SMB2_tdis(const unsigned int xid, struct cifs_tcon *tcon) struct smb2_tree_disconnect_req *req; /* response is trivial */ int rc = 0; struct cifs_ses *ses = tcon->ses; + struct TCP_Server_Info *server; int flags = 0; unsigned int total_len; struct kvec iov[1]; @@ -1954,9 +1973,13 @@ SMB2_tdis(const unsigned int xid, struct cifs_tcon *tcon) if ((tcon->need_reconnect) || (tcon->ses->need_reconnect)) return 0; + server = cifs_pick_channel(ses); + if (!server) + return -EIO; + close_cached_dir_lease(&tcon->crfid); - rc = smb2_plain_req_init(SMB2_TREE_DISCONNECT, tcon, ses->server, + rc = smb2_plain_req_init(SMB2_TREE_DISCONNECT, tcon, server, (void **) &req, &total_len); if (rc) @@ -1974,7 +1997,7 @@ SMB2_tdis(const unsigned int xid, struct cifs_tcon *tcon) rqst.rq_iov = iov; rqst.rq_nvec = 1; - rc = cifs_send_recv(xid, ses, ses->server, + rc = cifs_send_recv(xid, ses, server, &rqst, &resp_buf_type, flags, &rsp_iov); cifs_small_buf_release(req); if (rc) diff --git a/fs/cifs/transport.c b/fs/cifs/transport.c index 298417106c7cd..43b90a46198ef 100644 --- a/fs/cifs/transport.c +++ b/fs/cifs/transport.c @@ -1028,11 +1028,12 @@ cifs_cancelled_callback(struct mid_q_entry *mid) } /* - * Return a channel (master if none) of @ses that can be used to send - * regular requests. + * Pick an eligible channel for regular network operations. * - * If we are currently binding a new channel (negprot/sess.setup), - * return the new incomplete channel. + * Prefer the least-loaded established channel. Channels still marked for + * reconnect or Session Setup are skipped. If no eligible channel exists, + * fall back to the primary channel so the normal reconnect path can wait + * for recovery. */ struct TCP_Server_Info *cifs_pick_channel(struct cifs_ses *ses) { @@ -1044,39 +1045,46 @@ struct TCP_Server_Info *cifs_pick_channel(struct cifs_ses *ses) if (!ses) return NULL; + /* + * Negotiate/Session Setup use cifs_ses_server() explicitly. Regular I/O + * must stay on an established channel while another channel is binding. + */ spin_lock(&ses->chan_lock); - if (!ses->binding) { - if (ses->chan_count > 1) { - start = atomic_inc_return(&ses->chan_seq); - for (i = 0; i < ses->chan_count; i++) { - cur = (start + i) % ses->chan_count; - server = ses->chans[cur].server; - if (!server) - continue; - - if (server->tcpStatus == CifsNeedReconnect) - continue; - - /* - * strictly speaking, we should pick up req_lock to read - * server->in_flight. But it shouldn't matter much here if we - * race while reading this data. The worst that can happen is - * that we could use a channel that's not least loaded. Avoiding - * taking the lock could help reduce wait time, which is - * important for this function - */ - if (server->in_flight < min_in_flight) { - min_in_flight = server->in_flight; - index = cur; - } - } + start = atomic_inc_return(&ses->chan_seq); + for (i = 0; i < ses->chan_count; i++) { + cur = (start + i) % ses->chan_count; + server = ses->chans[cur].server; + if (!server) + continue; + + /* + * The reconnect bit stays set from transport failure until Session + * Setup/binding succeeds. Do not re-admit a channel merely because + * its socket has already left CifsNeedReconnect. + */ + if (CIFS_CHAN_NEEDS_RECONNECT(ses, cur) || + CIFS_CHAN_IN_RECONNECT(ses, cur)) + continue; + + /* Keep the transport-state fence as a second guard. */ + if (server->tcpStatus == CifsNeedReconnect) + continue; + + /* + * Keep the rotated least-loaded policy. If no + * eligible channel exists, index remains zero and we deliberately + * fall back to the primary transport so normal reconnect handling + * can wait/recover instead of failing channel selection outright. + */ + if (server->in_flight < min_in_flight) { + min_in_flight = server->in_flight; + index = cur; } - spin_unlock(&ses->chan_lock); - return ses->chans[index].server; - } else { - spin_unlock(&ses->chan_lock); - return cifs_ses_server(ses); } + server = ses->chans[index].server; + spin_unlock(&ses->chan_lock); + + return server; } int From 8373b4b11cb07b06256462497d5b9b4fec202a26 Mon Sep 17 00:00:00 2001 From: Shyam Prasad N Date: Sat, 12 Sep 2026 03:16:51 -0300 Subject: [PATCH 10/16] BACKPORT: cifs: use session reconnect state in full-key ioctl Use the SMB session reconnect state when the full-key ioctl checks whether session state allows the operation. Signed-off-by: Shyam Prasad N Change-Id: I1a4128a7e7842384a104146e25af1cada93802b1 (cherry picked from commit dd3cd8709ed5f4ae8998e0cd44c05bd26bc879e8) [juan: adapted the listed upstream change(s) to the CoreELEC 5.15.196 vendor layout; no original code authorship is claimed.] Signed-off-by: Juan Ruiz <6059278+juanruizm001@users.noreply.github.com> --- fs/cifs/ioctl.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/cifs/ioctl.c b/fs/cifs/ioctl.c index e846c18b71d2d..253d4941eff7a 100644 --- a/fs/cifs/ioctl.c +++ b/fs/cifs/ioctl.c @@ -232,7 +232,7 @@ static int cifs_dump_full_key(struct cifs_tcon *tcon, struct smb3_full_key_debug spin_lock(&cifs_tcp_ses_lock); list_for_each_entry(server_it, &cifs_tcp_ses_list, tcp_ses_list) { list_for_each_entry(ses_it, &server_it->smb_ses_list, smb_ses_list) { - if (ses_it->status != CifsExiting && + if (!cifs_ses_exiting(ses_it) && ses_it->Suid == out.session_id) { ses = ses_it; /* From eb3a610a1939edbb158d99e0f0b7004386ec57a5 Mon Sep 17 00:00:00 2001 From: Aurelien Aptel Date: Sat, 12 Sep 2026 03:16:51 -0300 Subject: [PATCH 11/16] BACKPORT: cifs: repick a channel when retrying direct reads Move retry-time channel selection to the direct-read attempt so a replayed read can use a currently eligible transport. Signed-off-by: Aurelien Aptel Change-Id: Iab0fed2d7569acde490e3e2e83a4bad66472229b (cherry picked from commit 352d96f3acc6e02099f58a24d5cabce7f8ee061f) (cherry picked from commit f1bf10d7e909fe898a112f5cae1e97ce34d6484d) [juan: adapted the listed upstream change(s) to the CoreELEC 5.15.196 vendor layout; no original code authorship is claimed.] Signed-off-by: Juan Ruiz <6059278+juanruizm001@users.noreply.github.com> --- fs/cifs/file.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/fs/cifs/file.c b/fs/cifs/file.c index aabd80088f334..237b2dbc1af34 100644 --- a/fs/cifs/file.c +++ b/fs/cifs/file.c @@ -3640,9 +3640,7 @@ static int cifs_resend_rdata(struct cifs_readdata *rdata, struct cifs_credits credits; int rc; struct TCP_Server_Info *server; - - /* XXX: should we pick a new channel here? */ - server = rdata->server; + struct cifs_tcon *tcon = tlink_tcon(rdata->cfile->tlink); do { if (rdata->cfile->invalidHandle) { @@ -3653,6 +3651,14 @@ static int cifs_resend_rdata(struct cifs_readdata *rdata, break; } + /* Retry on a currently eligible channel, not the failed transport. */ + server = cifs_pick_channel(tcon->ses); + if (!server) { + rc = -EIO; + goto fail; + } + rdata->server = server; + /* * Wait for credits to resend this rdata. * Note: we are attempting to resend the whole rdata not in From f4733520ebdb03effc13baf17ca6f7c1599199a9 Mon Sep 17 00:00:00 2001 From: Shyam Prasad N Date: Sat, 12 Sep 2026 03:16:52 -0300 Subject: [PATCH 12/16] BACKPORT: cifs: track replay state for write retries Track whether a retried write request is a replay so the SMB2 replay flag is applied only when an earlier request may have reached the server. Signed-off-by: Shyam Prasad N Change-Id: I1f93b5183178a1b4042d1e0a06e511a7b9cf91ee (cherry picked from commit 4cdad80261862c8cdcbb5fd232aa713d0bdefe24) (cherry picked from commit 4f1fffa2376922f3d1d506e49c0fd445b023a28e) [juan: adapted the listed upstream change(s) to the CoreELEC 5.15.196 vendor layout; no original code authorship is claimed.] Signed-off-by: Juan Ruiz <6059278+juanruizm001@users.noreply.github.com> --- fs/cifs/cifsglob.h | 1 + 1 file changed, 1 insertion(+) diff --git a/fs/cifs/cifsglob.h b/fs/cifs/cifsglob.h index 59ada8f5aa5b7..94ec6ca6031fd 100644 --- a/fs/cifs/cifsglob.h +++ b/fs/cifs/cifsglob.h @@ -1406,6 +1406,7 @@ struct cifs_writedata { pid_t pid; unsigned int bytes; int result; + bool replay; /* retry may have reached server */ struct TCP_Server_Info *server; #ifdef CONFIG_CIFS_SMB_DIRECT struct smbd_mr *mr; From 49cc984a5c23352d3b316835d987c5ff596b67b7 Mon Sep 17 00:00:00 2001 From: Shyam Prasad N Date: Sat, 12 Sep 2026 03:16:52 -0300 Subject: [PATCH 13/16] BACKPORT: cifs: repick and replay retried direct writes Repick an eligible channel for each direct-write retry and preserve the replay state needed by SMB2 after a transport failure. Signed-off-by: Shyam Prasad N Change-Id: I23924bab8a9f7cf042da9e97a65e1583710d2c3a (cherry picked from commit 4cdad80261862c8cdcbb5fd232aa713d0bdefe24) (cherry picked from commit 4f1fffa2376922f3d1d506e49c0fd445b023a28e) [juan: adapted the listed upstream change(s) to the CoreELEC 5.15.196 vendor layout; no original code authorship is claimed.] Signed-off-by: Juan Ruiz <6059278+juanruizm001@users.noreply.github.com> --- fs/cifs/file.c | 15 ++++++++++++++- fs/cifs/smb2pdu.c | 2 ++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/fs/cifs/file.c b/fs/cifs/file.c index 237b2dbc1af34..262a89c6c29d3 100644 --- a/fs/cifs/file.c +++ b/fs/cifs/file.c @@ -2904,7 +2904,8 @@ cifs_resend_wdata(struct cifs_writedata *wdata, struct list_head *wdata_list, unsigned int wsize; struct cifs_credits credits; int rc; - struct TCP_Server_Info *server = wdata->server; + struct TCP_Server_Info *server; + struct cifs_tcon *tcon = tlink_tcon(wdata->cfile->tlink); do { if (wdata->cfile->invalidHandle) { @@ -2916,6 +2917,18 @@ cifs_resend_wdata(struct cifs_writedata *wdata, struct list_head *wdata_list, } + /* + * A retry may have been applied before the response was lost. + * Repick a healthy channel, then mark the SMB3 WRITE as replay. + */ + server = cifs_pick_channel(tcon->ses); + if (!server) { + rc = -EIO; + goto fail; + } + wdata->server = server; + wdata->replay = true; + /* * Wait for credits to resend this wdata. * Note: we are attempting to resend the whole wdata not in diff --git a/fs/cifs/smb2pdu.c b/fs/cifs/smb2pdu.c index 0e6208e1c833c..44ce2bb85cd77 100644 --- a/fs/cifs/smb2pdu.c +++ b/fs/cifs/smb2pdu.c @@ -4475,6 +4475,8 @@ smb2_async_writev(struct cifs_writedata *wdata, flags |= CIFS_TRANSFORM_REQ; shdr = (struct smb2_sync_hdr *)req; + if (wdata->replay && server && server->dialect >= SMB30_PROT_ID) + shdr->Flags |= SMB2_FLAGS_REPLAY_OPERATION; shdr->ProcessId = cpu_to_le32(wdata->cfile->pid); req->PersistentFileId = wdata->cfile->fid.persistent_fid; From 38f398cbf3af4ec220a7c89b44cdfe50678dcee1 Mon Sep 17 00:00:00 2001 From: Shyam Prasad N Date: Sat, 12 Sep 2026 03:16:52 -0300 Subject: [PATCH 14/16] BACKPORT: cifs: normalize replayable transport send errors Normalize transport send failures to a replayable error and use one helper to decide whether an operation can be retried. Signed-off-by: Shyam Prasad N Change-Id: If727a76848114ac8f2b5761ed0439a395c401d88 (cherry picked from commit a68106a6928e0a6680f12bcc7338c0dddcfe4d11) (cherry picked from commit 64cc377b7628b81ffdbdb1c6bacfba895dcac3f8) [juan: adapted the listed upstream change(s) to the CoreELEC 5.15.196 vendor layout; no original code authorship is claimed.] Signed-off-by: Juan Ruiz <6059278+juanruizm001@users.noreply.github.com> --- fs/cifs/cifsglob.h | 6 ++++++ fs/cifs/transport.c | 12 ++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/fs/cifs/cifsglob.h b/fs/cifs/cifsglob.h index 94ec6ca6031fd..c9d5ec9ca7c46 100644 --- a/fs/cifs/cifsglob.h +++ b/fs/cifs/cifsglob.h @@ -1757,6 +1757,12 @@ static inline bool is_retryable_error(int error) } +static inline bool is_replayable_error(int error) +{ + return error == -EAGAIN || error == -ECONNABORTED; +} + + /* cifs_get_writable_file() flags */ #define FIND_WR_ANY 0 #define FIND_WR_FSUID_ONLY 1 diff --git a/fs/cifs/transport.c b/fs/cifs/transport.c index 43b90a46198ef..e345d3877e03f 100644 --- a/fs/cifs/transport.c +++ b/fs/cifs/transport.c @@ -431,10 +431,18 @@ __smb_send_rqst(struct TCP_Server_Info *server, int num_rqst, server->conn_id, server->hostname); } smbd_done: - if (rc < 0 && rc != -EINTR) + /* + * Upper CIFS layers need one replayable transport error, not a family + * of socket errno values. Preserve signal/interruption returns from this + * 5.15 tree, and ask the owning cifsd to recover this channel. + */ + if (rc < 0 && rc != -EINTR && rc != -EAGAIN && + rc != -ERESTARTSYS) { cifs_server_dbg(VFS, "Error %d sending data on socket to server\n", rc); - else if (rc > 0) + rc = -ECONNABORTED; + cifs_signal_cifsd_for_reconnect(server, false); + } else if (rc > 0) rc = 0; out: cifs_in_send_dec(server); From 9cee56f7ab26648ceac6e55c329b32bc68a3572a Mon Sep 17 00:00:00 2001 From: Shyam Prasad N Date: Sat, 12 Sep 2026 03:16:53 -0300 Subject: [PATCH 15/16] BACKPORT: cifs: make SMB2 flush replay safe across channels Reinitialize per-attempt FLUSH response state, repick an eligible channel for each replay, set the SMB2 replay flag, and keep retries bounded for soft mounts. Signed-off-by: Shyam Prasad N Change-Id: I49b2c4eb78a7412f79074d432059ccecea332508 (cherry picked from commit 4f1fffa2376922f3d1d506e49c0fd445b023a28e) (cherry picked from commit 4be31c943a3a27a5a0251dbb8f5cb89059ec3d5a) [juan: adapted the listed upstream change(s) to the CoreELEC 5.15.196 vendor layout; no original code authorship is claimed.] Signed-off-by: Juan Ruiz <6059278+juanruizm001@users.noreply.github.com> --- fs/cifs/smb2pdu.c | 43 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/fs/cifs/smb2pdu.c b/fs/cifs/smb2pdu.c index 44ce2bb85cd77..8fa28f4b3e68d 100644 --- a/fs/cifs/smb2pdu.c +++ b/fs/cifs/smb2pdu.c @@ -3982,29 +3982,52 @@ SMB2_flush(const unsigned int xid, struct cifs_tcon *tcon, u64 persistent_fid, struct smb_rqst rqst; struct kvec iov[1]; struct kvec rsp_iov = {NULL, 0}; - struct TCP_Server_Info *server = cifs_pick_channel(ses); + struct TCP_Server_Info *server; int resp_buftype = CIFS_NO_BUFFER; int flags = 0; int rc = 0; + unsigned int retries = 0; + const unsigned int max_replays = 3; cifs_dbg(FYI, "flush\n"); if (!ses || !(ses->server)) return -EIO; - if (smb3_encryption_required(tcon)) - flags |= CIFS_TRANSFORM_REQ; - +replay_again: + /* Reinitialize all per-attempt response/request state. */ + resp_buftype = CIFS_NO_BUFFER; + memset(&rsp_iov, 0, sizeof(rsp_iov)); + flags = 0; memset(&rqst, 0, sizeof(struct smb_rqst)); memset(&iov, 0, sizeof(iov)); rqst.rq_iov = iov; rqst.rq_nvec = 1; + /* + * A replay must be rebuilt on the currently eligible channel so the + * current ChannelSequence header uses the current session state. + */ + server = cifs_pick_channel(ses); + if (!server) + return -EIO; + + if (smb3_encryption_required(tcon)) + flags |= CIFS_TRANSFORM_REQ; + rc = SMB2_flush_init(xid, &rqst, tcon, server, persistent_fid, volatile_fid); if (rc) goto flush_exit; trace_smb3_flush_enter(xid, persistent_fid, tcon->tid, ses->Suid); + + if (retries && server->dialect >= SMB30_PROT_ID) { + struct smb2_sync_hdr *shdr = rqst.rq_iov[0].iov_base; + + if (shdr) + shdr->Flags |= SMB2_FLAGS_REPLAY_OPERATION; + } + rc = cifs_send_recv(xid, ses, server, &rqst, &resp_buftype, flags, &rsp_iov); @@ -4016,9 +4039,19 @@ SMB2_flush(const unsigned int xid, struct cifs_tcon *tcon, u64 persistent_fid, trace_smb3_flush_done(xid, persistent_fid, tcon->tid, ses->Suid); - flush_exit: +flush_exit: SMB2_flush_free(&rqst); free_rsp_buf(resp_buftype, rsp_iov.iov_base); + + /* + * Keep soft-mount failover bounded to three replay attempts. + * Re-evaluate the channel before each replay. + */ + if (is_replayable_error(rc) && retries < max_replays) { + retries++; + goto replay_again; + } + return rc; } From 3294f2c166d2ce27d523f84243763b57e5641352 Mon Sep 17 00:00:00 2001 From: Shyam Prasad N Date: Sat, 12 Sep 2026 03:16:53 -0300 Subject: [PATCH 16/16] BACKPORT: cifs: close multichannel write retry paths Bind each write attempt to one eligible transport before credits are taken and keep retry/replay state consistent across writeback and direct-write paths. Signed-off-by: Shyam Prasad N Change-Id: Ia0f106c345be401b01ae77f2e5e648ddd1aabc8d (cherry picked from commit 4cdad80261862c8cdcbb5fd232aa713d0bdefe24) (cherry picked from commit 64cc377b7628b81ffdbdb1c6bacfba895dcac3f8) (cherry picked from commit a68106a6928e0a6680f12bcc7338c0dddcfe4d11) (cherry picked from commit 4f1fffa2376922f3d1d506e49c0fd445b023a28e) [juan: adapted the listed upstream change(s) to the CoreELEC 5.15.196 vendor layout; no original code authorship is claimed.] Signed-off-by: Juan Ruiz <6059278+juanruizm001@users.noreply.github.com> --- fs/cifs/cifssmb.c | 30 ++++++++++++++++++++++-------- fs/cifs/file.c | 32 +++++++++++++++++++++++++++----- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/fs/cifs/cifssmb.c b/fs/cifs/cifssmb.c index e6541bd5c63df..2a5353966d54b 100644 --- a/fs/cifs/cifssmb.c +++ b/fs/cifs/cifssmb.c @@ -1885,13 +1885,23 @@ cifs_writev_requeue(struct cifs_writedata *wdata) struct TCP_Server_Info *server; unsigned int rest_len; - server = tlink_tcon(wdata->cfile->tlink)->ses->server; i = 0; rest_len = wdata->bytes; do { struct cifs_writedata *wdata2; + struct cifs_tcon *tcon = tlink_tcon(wdata->cfile->tlink); unsigned int j, nr_pages, wsize, tailsz, cur_len; + /* + * Retry writeback on a healthy channel. Bind it before sizing + * and sending so one transport owns the complete retry attempt. + */ + server = cifs_pick_channel(tcon->ses); + if (!server) { + rc = -EIO; + break; + } + wsize = server->ops->wp_retry_size(inode); if (wsize < rest_len) { nr_pages = wsize / PAGE_SIZE; @@ -1920,6 +1930,8 @@ cifs_writev_requeue(struct cifs_writedata *wdata) } wdata2->sync_mode = wdata->sync_mode; + wdata2->server = server; + wdata2->replay = true; wdata2->nr_pages = nr_pages; wdata2->offset = page_offset(wdata2->pages[0]); wdata2->pagesz = PAGE_SIZE; @@ -1931,7 +1943,7 @@ cifs_writev_requeue(struct cifs_writedata *wdata) if (!wdata2->cfile) { cifs_dbg(VFS, "No writable handle to retry writepages rc=%d\n", rc); - if (!is_retryable_error(rc)) + if (!is_retryable_error(rc) && !is_replayable_error(rc)) rc = -EBADF; } else { wdata2->pid = wdata2->cfile->pid; @@ -1941,7 +1953,8 @@ cifs_writev_requeue(struct cifs_writedata *wdata) for (j = 0; j < nr_pages; j++) { unlock_page(wdata2->pages[j]); - if (rc != 0 && !is_retryable_error(rc)) { + if (rc != 0 && !is_retryable_error(rc) && + !is_replayable_error(rc)) { SetPageError(wdata2->pages[j]); end_page_writeback(wdata2->pages[j]); put_page(wdata2->pages[j]); @@ -1950,7 +1963,7 @@ cifs_writev_requeue(struct cifs_writedata *wdata) kref_put(&wdata2->refcount, cifs_writedata_release); if (rc) { - if (is_retryable_error(rc)) + if (is_retryable_error(rc) || is_replayable_error(rc)) continue; i += nr_pages; break; @@ -1967,7 +1980,7 @@ cifs_writev_requeue(struct cifs_writedata *wdata) put_page(wdata->pages[i]); } - if (rc != 0 && !is_retryable_error(rc)) + if (rc != 0 && !is_retryable_error(rc) && !is_replayable_error(rc)) mapping_set_error(inode->i_mapping, rc); kref_put(&wdata->refcount, cifs_writedata_release); } @@ -1986,12 +1999,13 @@ cifs_writev_complete(struct work_struct *work) spin_unlock(&inode->i_lock); cifs_stats_bytes_written(tlink_tcon(wdata->cfile->tlink), wdata->bytes); - } else if (wdata->sync_mode == WB_SYNC_ALL && wdata->result == -EAGAIN) + } else if (wdata->sync_mode == WB_SYNC_ALL && + is_replayable_error(wdata->result)) return cifs_writev_requeue(wdata); for (i = 0; i < wdata->nr_pages; i++) { struct page *page = wdata->pages[i]; - if (wdata->result == -EAGAIN) + if (is_replayable_error(wdata->result)) __set_page_dirty_nobuffers(page); else if (wdata->result < 0) SetPageError(page); @@ -1999,7 +2013,7 @@ cifs_writev_complete(struct work_struct *work) cifs_readpage_to_fscache(inode, page); put_page(page); } - if (wdata->result != -EAGAIN) + if (!is_replayable_error(wdata->result)) mapping_set_error(inode->i_mapping, wdata->result); kref_put(&wdata->refcount, cifs_writedata_release); } diff --git a/fs/cifs/file.c b/fs/cifs/file.c index 262a89c6c29d3..488dea9b1b3ce 100644 --- a/fs/cifs/file.c +++ b/fs/cifs/file.c @@ -2973,7 +2973,7 @@ cifs_resend_wdata(struct cifs_writedata *wdata, struct list_head *wdata_list, /* Roll back credits and retry if needed */ add_credits_and_wake_if(server, &wdata->credits, 0); - } while (rc == -EAGAIN); + } while (is_replayable_error(rc)); fail: kref_put(&wdata->refcount, cifs_uncached_writedata_release); @@ -2994,6 +2994,7 @@ cifs_write_from_iter(loff_t offset, size_t len, struct iov_iter *from, loff_t saved_offset = offset; pid_t pid; struct TCP_Server_Info *server; + bool replay = false; struct page **pagevec; size_t start; unsigned int xid; @@ -3003,7 +3004,6 @@ cifs_write_from_iter(loff_t offset, size_t len, struct iov_iter *from, else pid = current->tgid; - server = cifs_pick_channel(tlink_tcon(open_file->tlink)->ses); xid = get_xid(); do { @@ -3019,10 +3019,27 @@ cifs_write_from_iter(loff_t offset, size_t len, struct iov_iter *from, break; } + /* + * Choose an eligible channel before taking credits. The same + * server then owns the credits and the asynchronous send. + */ + server = cifs_pick_channel(tlink_tcon(open_file->tlink)->ses); + if (!server) { + rc = -EIO; + break; + } + rc = server->ops->wait_mtu_credits(server, cifs_sb->ctx->wsize, &wsize, credits); - if (rc) + if (rc) { + /* + * No request was sent yet. Repick on a replayable + * transport failure without marking a new WRITE as replay. + */ + if (is_replayable_error(rc)) + continue; break; + } cur_len = min_t(const size_t, len, wsize); @@ -3108,6 +3125,7 @@ cifs_write_from_iter(loff_t offset, size_t len, struct iov_iter *from, wdata->offset = (__u64)offset; wdata->cfile = cifsFileInfo_get(open_file); wdata->server = server; + wdata->replay = replay; wdata->pid = pid; wdata->bytes = cur_len; wdata->pagesz = PAGE_SIZE; @@ -3129,7 +3147,9 @@ cifs_write_from_iter(loff_t offset, size_t len, struct iov_iter *from, add_credits_and_wake_if(server, &wdata->credits, 0); kref_put(&wdata->refcount, cifs_uncached_writedata_release); - if (rc == -EAGAIN) { + if (is_replayable_error(rc)) { + /* Retry the same offset/data on another channel. */ + replay = true; *from = saved_from; iov_iter_advance(from, offset - saved_offset); continue; @@ -3137,6 +3157,8 @@ cifs_write_from_iter(loff_t offset, size_t len, struct iov_iter *from, break; } + /* The next chunk is a new WRITE, not a replay of this one. */ + replay = false; list_add_tail(&wdata->list, wdata_list); offset += cur_len; len -= cur_len; @@ -3184,7 +3206,7 @@ static void collect_uncached_write_data(struct cifs_aio_ctx *ctx) ctx->total_len += wdata->bytes; /* resend call if it's a retryable error */ - if (rc == -EAGAIN) { + if (is_replayable_error(rc)) { struct list_head tmp_list; struct iov_iter tmp_from = ctx->iter;