[PATCH] smb/client: fix chan_max race and rollback in smb3_reconfigure
From: DaeMyung Kang
Date: Wed Apr 15 2026 - 22:28:28 EST
smb3_reconfigure() has two bugs when handling multichannel remount:
1) smb3_sync_ses_chan_max() is called before acquiring
CIFS_SES_FLAG_SCALE_CHANNELS. If a concurrent operation (e.g.
smb2_reconnect) holds the flag, the current thread takes the
loser path and returns -EINVAL, but ses->chan_max has already
been updated to the new value. This leaves chan_max inconsistent
with the actual channel state.
2) When smb3_update_ses_channels() fails, chan_max is not rolled
back to its previous value and the error is not propagated to
the caller. The mount command returns success even though the
channel configuration was not applied, and repeated failures
cause chan_max to drift further from reality.
Fix both by moving smb3_sync_ses_chan_max() after the
CIFS_SES_FLAG_SCALE_CHANNELS acquisition so the loser path cannot
corrupt chan_max, and by restoring chan_max from old_chan_max on
update failure while still holding the scaling flag to prevent a
concurrent reconfigure from observing the intermediate state.
Propagate the error to the caller so userspace is informed.
Note: smb3_reconfigure() is not fully transactional — credentials
are committed before the multichannel block, so any early return
(including the existing CIFS_SES_FLAG_SCALE_CHANNELS loser path)
can leave ses->password updated while cifs_sb->ctx is not yet
synced. Making the entire function atomic is a larger refactor
and is left for a separate patch.
Tested with a QEMU VM (ksmbd + cifs) using module-parameter-based
fault injection:
- Forced smb3_update_ses_channels() failure via module param and
verified chan_max is preserved at the old value after remount
failure (fault-injection test for rollback).
- Pre-set CIFS_SES_FLAG_SCALE_CHANNELS before entering the
scaling path and verified the loser path returns -EINVAL
without corrupting chan_max (loser-path invariant test).
- Repeated 19 forced-failure remounts with varying max_channels
(range 2-8) and confirmed no chan_max drift (stress test).
All three scenarios pass with the patch and fail without it.
Fixes: ef529f655a2c ("cifs: client: allow changing multichannel mount options on remount")
Signed-off-by: DaeMyung Kang <charsyam@xxxxxxxxx>
---
fs/smb/client/fs_context.c | 19 ++++++++++++++++---
1 file changed, 16 insertions(+), 3 deletions(-)
diff --git a/fs/smb/client/fs_context.c b/fs/smb/client/fs_context.c
index b9544eb0381b..99e62c2dbf50 100644
--- a/fs/smb/client/fs_context.c
+++ b/fs/smb/client/fs_context.c
@@ -1085,6 +1085,7 @@ static int smb3_reconfigure(struct fs_context *fc)
struct dentry *root = fc->root;
struct cifs_sb_info *cifs_sb = CIFS_SB(root->d_sb);
struct cifs_ses *ses = cifs_sb_master_tcon(cifs_sb)->ses;
+ unsigned int old_chan_max;
unsigned int rsize = ctx->rsize, wsize = ctx->wsize;
char *new_password = NULL, *new_password2 = NULL;
bool need_recon = false;
@@ -1170,9 +1171,6 @@ static int smb3_reconfigure(struct fs_context *fc)
if ((ctx->multichannel != cifs_sb->ctx->multichannel) ||
(ctx->max_channels != cifs_sb->ctx->max_channels)) {
- /* Synchronize ses->chan_max with the new mount context */
- smb3_sync_ses_chan_max(ses, ctx->max_channels);
- /* Now update the session's channels to match the new configuration */
/* Prevent concurrent scaling operations */
spin_lock(&ses->ses_lock);
if (ses->flags & CIFS_SES_FLAG_SCALE_CHANNELS) {
@@ -1183,16 +1181,31 @@ static int smb3_reconfigure(struct fs_context *fc)
ses->flags |= CIFS_SES_FLAG_SCALE_CHANNELS;
spin_unlock(&ses->ses_lock);
+ old_chan_max = ses->chan_max;
+ /* Synchronize ses->chan_max with the new mount context */
+ smb3_sync_ses_chan_max(ses, ctx->max_channels);
+
mutex_unlock(&ses->session_mutex);
rc = smb3_update_ses_channels(ses, ses->server,
false /* from_reconnect */,
false /* disable_mchan */);
+ /*
+ * On failure, restore chan_max while still holding
+ * CIFS_SES_FLAG_SCALE_CHANNELS so a concurrent reconfigure
+ * cannot observe or race with the rollback.
+ */
+ if (rc < 0)
+ smb3_sync_ses_chan_max(ses, old_chan_max);
+
/* Clear scaling flag after operation */
spin_lock(&ses->ses_lock);
ses->flags &= ~CIFS_SES_FLAG_SCALE_CHANNELS;
spin_unlock(&ses->ses_lock);
+
+ if (rc < 0)
+ return rc;
} else {
mutex_unlock(&ses->session_mutex);
}
--
2.43.0