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 68b9b382d44bf..c9d5ec9ca7c46 100644 --- a/fs/cifs/cifsglob.h +++ b/fs/cifs/cifsglob.h @@ -106,13 +106,26 @@ * 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, CifsExiting, CifsNeedReconnect, - CifsNeedNegotiate + CifsNeedNegotiate, + CifsInNegotiate, + 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 { @@ -690,6 +703,17 @@ 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; + /* + * 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; @@ -897,6 +921,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]; }; @@ -906,12 +931,14 @@ 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; + 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 */ @@ -959,11 +986,33 @@ 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_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; 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 ======== */ }; @@ -1357,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; @@ -1707,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 @@ -1799,7 +1855,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 @@ -2042,12 +2098,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; + + spin_lock(&ses->ses_lock); + status = ses->ses_status; + spin_unlock(&ses->ses_lock); + return status; +} - ret = ses->status == CifsExiting; - return ret; +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 7d00802f97223..0c41aac220073 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 *); @@ -594,6 +596,30 @@ 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); +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); +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/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/connect.c b/fs/cifs/connect.c index da386ae1c1416..b8f243e00865e 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); @@ -162,51 +166,143 @@ 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) { + if (cifs_ses_exiting(ses)) + continue; + 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); - ses->need_reconnect = true; - list_for_each(tmp2, &ses->tcon_list) { - tcon = list_entry(tmp2, struct cifs_tcon, tcon_list); + 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); + 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)) { + 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); + + 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); + + 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; + } } 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; } @@ -217,7 +313,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); @@ -247,46 +342,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)) @@ -300,17 +404,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; @@ -372,7 +481,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; @@ -396,7 +506,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(); @@ -417,12 +530,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) @@ -440,20 +554,50 @@ 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) { + if (!server->is_dfs_conn) { 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); + /* + * 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 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 @@ -541,7 +685,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; } @@ -574,7 +718,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; } @@ -589,7 +733,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; } @@ -608,7 +752,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; } } @@ -1122,7 +1266,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 */ @@ -1428,6 +1572,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) { @@ -1461,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); @@ -1722,7 +1868,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; @@ -1739,40 +1885,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); @@ -1783,19 +1930,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); } } @@ -1977,7 +2124,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); @@ -1988,7 +2135,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); @@ -1999,6 +2148,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); @@ -2053,6 +2204,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); @@ -2060,8 +2212,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) @@ -3614,6 +3768,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; @@ -3803,18 +3962,36 @@ 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->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); rc = server->ops->negotiate(xid, ses); - if (rc == 0) { - spin_lock(&GlobalMid_Lock); - if (server->tcpStatus == CifsNeedNegotiate) + if (!rc) { + spin_lock(&cifs_tcp_ses_lock); + if (server->tcpStatus == CifsInNegotiate) server->tcpStatus = CifsGood; 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; @@ -3825,9 +4002,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); - if (!ses->binding) { + /* + * 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(&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; + } + + 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); @@ -3847,9 +4067,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) { + if (!is_binding && ses->ses_status == SES_IN_SETUP) + ses->ses_status = SES_NEED_RECON; + } else { + cifs_chan_clear_need_reconnect(ses, server); + 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; } @@ -4118,14 +4359,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/file.c b/fs/cifs/file.c index aabd80088f334..488dea9b1b3ce 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 @@ -2960,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); @@ -2981,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; @@ -2990,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 { @@ -3006,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); @@ -3095,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; @@ -3116,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; @@ -3124,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; @@ -3171,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; @@ -3640,9 +3675,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 +3686,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 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; /* diff --git a/fs/cifs/misc.c b/fs/cifs/misc.c index a3d37e7769e61..1eccd668b6aec 100644 --- a/fs/cifs/misc.c +++ b/fs/cifs/misc.c @@ -69,11 +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 0fbd0f78f361b..1c00f496e4402 100644 --- a/fs/cifs/sess.c +++ b/fs/cifs/sess.c @@ -65,6 +65,107 @@ 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; +} + +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) +{ + 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 +294,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(); @@ -223,6 +325,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; @@ -271,10 +376,20 @@ 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); chan->server->is_channel = true; + chan->server->primary_server = ses->server; spin_unlock(&cifs_tcp_ses_lock); /* @@ -288,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; @@ -307,14 +424,22 @@ 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); + } + + 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) @@ -930,10 +1055,23 @@ sess_establish_session(struct sess_data *sess_data) mutex_unlock(&ses->server->srv_mutex); cifs_dbg(FYI, "CIFS session established successfully\n"); - spin_lock(&GlobalMid_Lock); - ses->status = CifsGood; - ses->need_reconnect = false; - spin_unlock(&GlobalMid_Lock); + + spin_lock(&ses->chan_lock); + if (ses->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); + + 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/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/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 302c08dfb686d..8fa28f4b3e68d 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) @@ -143,61 +164,38 @@ 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; } } - 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; - /* - * Give demultiplex thread up to 10 seconds to each target available for - * reconnect -- should be greater than cifs socket timeout which is 7 - * seconds. - */ +again: + retries = server->nr_targets; 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 +204,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 +227,83 @@ 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(); + mutex_lock(&ses->session_mutex); /* - * need to prevent multiple threads trying to simultaneously reconnect - * the same SMB session - */ - mutex_lock(&tcon->ses->session_mutex); - - /* - * 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. + * 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) { - rc = -EHOSTDOWN; - mutex_unlock(&tcon->ses->session_mutex); - goto out; + spin_unlock(&cifs_tcp_ses_lock); + mutex_unlock(&ses->session_mutex); + unload_nls(nls_codepage); + if (tcon->retry) + goto again; + return -EHOSTDOWN; } + spin_unlock(&cifs_tcp_ses_lock); - /* - * If we are reconnecting an extra channel, bind - */ - if (server->is_channel) { - ses->binding = true; - ses->binding_chan = cifs_ses_find_chan(ses, server); + spin_lock(&ses->ses_lock); + spin_lock(&ses->chan_lock); + if (!cifs_chan_needs_reconnect(ses, server) && + ses->ses_status == SES_GOOD) { + spin_unlock(&ses->chan_lock); + spin_unlock(&ses->ses_lock); + if (tcon->need_reconnect) + goto skip_sess_setup; + mutex_unlock(&ses->session_mutex); + goto out; } - 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; + binding = !CIFS_ALL_CHANS_NEED_RECONNECT(ses); + if (binding) { + ses->binding = true; + ses->binding_chan = cifs_ses_find_chan_locked(ses, server); + if (!ses->binding_chan) { ses->binding = false; - ses->binding_chan = NULL; - mutex_unlock(&tcon->ses->session_mutex); - goto failed; - } else if (rc) { + 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; } - /* - * End of channel binding - */ - 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 (rc || !tcon->need_reconnect) { - mutex_unlock(&tcon->ses->session_mutex); + /* Negotiate can fail before cifs_setup_session clears binding state. */ + if (rc) { + spin_lock(&ses->chan_lock); + ses->binding = false; + ses->binding_chan = NULL; + spin_unlock(&ses->chan_lock); + mutex_unlock(&ses->session_mutex); + if ((rc == -EACCES) && !tcon->retry) + rc = -EHOSTDOWN; + goto failed; + } + +skip_sess_setup: + if (!tcon->need_reconnect) { + mutex_unlock(&ses->session_mutex); goto out; } @@ -293,12 +312,11 @@ 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); + cifs_dbg(VFS, "reconnect tcon failed rc = %d\n", rc); goto out; } @@ -307,14 +325,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: @@ -1264,6 +1274,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, @@ -1271,8 +1282,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; @@ -1378,12 +1390,29 @@ 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 */ - if (!ses->binding) { - spin_lock(&GlobalMid_Lock); - ses->status = CifsGood; - ses->need_reconnect = false; - spin_unlock(&GlobalMid_Lock); + + { + bool is_binding; + + 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; @@ -1398,10 +1427,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)) { @@ -1425,7 +1456,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) { @@ -1446,7 +1477,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); } @@ -1482,6 +1513,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 @@ -1497,6 +1529,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); @@ -1544,7 +1577,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); } @@ -1574,10 +1607,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; @@ -1605,7 +1640,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); } @@ -1795,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"); @@ -1926,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]; @@ -1940,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) @@ -1960,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) @@ -3752,44 +3789,66 @@ 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) { + if (cifs_ses_exiting(ses)) + continue; + 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); @@ -3807,13 +3866,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); } @@ -3829,7 +3918,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; @@ -3893,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); @@ -3927,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; } @@ -4386,6 +4508,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; 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 c5c1e743359dd..e345d3877e03f 100644 --- a/fs/cifs/transport.c +++ b/fs/cifs/transport.c @@ -426,17 +426,23 @@ __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); } 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); @@ -729,14 +735,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; @@ -1030,32 +1036,63 @@ 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) { uint index = 0; + unsigned int min_in_flight = UINT_MAX; + struct TCP_Server_Info *server = NULL; + int i, start, cur; 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) { - /* round robin */ - if (ses->chan_count > 1) { - index = (uint)atomic_inc_return(&ses->chan_seq); - index %= ses->chan_count; + 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 @@ -1185,7 +1222,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); @@ -1254,7 +1292,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