Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

smb: client: guarantee refcounted children from parent session

Avoid potential use-after-free bugs when walking DFS referrals,
mounting and performing DFS failover by ensuring that all children
from parent @tcon->ses are also refcounted. They're all needed across
the entire DFS mount. Get rid of @tcon->dfs_ses_list while we're at
it, too.

Cc: stable@vger.kernel.org # 6.4+
Reported-by: kernel test robot <lkp@intel.com>
Closes: https://lore.kernel.org/oe-kbuild-all/202404021527.ZlRkIxgv-lkp@intel.com/
Signed-off-by: Paulo Alcantara (Red Hat) <pc@manguebit.com>
Signed-off-by: Steve French <stfrench@microsoft.com>

authored by

Paulo Alcantara and committed by
Steve French
062a7f0f e9e62243

+76 -72
-2
fs/smb/client/cifsglob.h
··· 1281 1281 struct cached_fids *cfids; 1282 1282 /* BB add field for back pointer to sb struct(s)? */ 1283 1283 #ifdef CONFIG_CIFS_DFS_UPCALL 1284 - struct list_head dfs_ses_list; 1285 1284 struct delayed_work dfs_cache_work; 1286 1285 #endif 1287 1286 struct delayed_work query_interfaces; /* query interfaces workqueue job */ ··· 1803 1804 struct TCP_Server_Info *server; 1804 1805 struct cifs_ses *ses; 1805 1806 struct cifs_tcon *tcon; 1806 - struct list_head dfs_ses_list; 1807 1807 }; 1808 1808 1809 1809 static inline void __free_dfs_info_param(struct dfs_info3_param *param)
+10 -10
fs/smb/client/cifsproto.h
··· 725 725 void cifs_put_tcon_super(struct super_block *sb); 726 726 int cifs_wait_for_server_reconnect(struct TCP_Server_Info *server, bool retry); 727 727 728 - /* Put references of @ses and @ses->dfs_root_ses */ 728 + /* Put references of @ses and its children */ 729 729 static inline void cifs_put_smb_ses(struct cifs_ses *ses) 730 730 { 731 - struct cifs_ses *rses = ses->dfs_root_ses; 731 + struct cifs_ses *next; 732 732 733 - __cifs_put_smb_ses(ses); 734 - if (rses) 735 - __cifs_put_smb_ses(rses); 733 + do { 734 + next = ses->dfs_root_ses; 735 + __cifs_put_smb_ses(ses); 736 + } while ((ses = next)); 736 737 } 737 738 738 - /* Get an active reference of @ses and @ses->dfs_root_ses. 739 + /* Get an active reference of @ses and its children. 739 740 * 740 741 * NOTE: make sure to call this function when incrementing reference count of 741 742 * @ses to ensure that any DFS root session attached to it (@ses->dfs_root_ses) 742 743 * will also get its reference count incremented. 743 744 * 744 - * cifs_put_smb_ses() will put both references, so call it when you're done. 745 + * cifs_put_smb_ses() will put all references, so call it when you're done. 745 746 */ 746 747 static inline void cifs_smb_ses_inc_refcount(struct cifs_ses *ses) 747 748 { 748 749 lockdep_assert_held(&cifs_tcp_ses_lock); 749 750 750 - ses->ses_count++; 751 - if (ses->dfs_root_ses) 752 - ses->dfs_root_ses->ses_count++; 751 + for (; ses; ses = ses->dfs_root_ses) 752 + ses->ses_count++; 753 753 } 754 754 755 755 static inline bool dfs_src_pathname_equal(const char *s1, const char *s2)
+20 -5
fs/smb/client/connect.c
··· 1866 1866 ctx->sectype != ses->sectype) 1867 1867 return 0; 1868 1868 1869 + if (ctx->dfs_root_ses != ses->dfs_root_ses) 1870 + return 0; 1871 + 1869 1872 /* 1870 1873 * If an existing session is limited to less channels than 1871 1874 * requested, it should not be reused ··· 2361 2358 * need to lock before changing something in the session. 2362 2359 */ 2363 2360 spin_lock(&cifs_tcp_ses_lock); 2361 + if (ctx->dfs_root_ses) 2362 + cifs_smb_ses_inc_refcount(ctx->dfs_root_ses); 2364 2363 ses->dfs_root_ses = ctx->dfs_root_ses; 2365 - if (ses->dfs_root_ses) 2366 - ses->dfs_root_ses->ses_count++; 2367 2364 list_add(&ses->smb_ses_list, &server->smb_ses_list); 2368 2365 spin_unlock(&cifs_tcp_ses_lock); 2369 2366 ··· 3314 3311 cifs_put_smb_ses(mnt_ctx->ses); 3315 3312 else if (mnt_ctx->server) 3316 3313 cifs_put_tcp_session(mnt_ctx->server, 0); 3314 + mnt_ctx->ses = NULL; 3315 + mnt_ctx->tcon = NULL; 3316 + mnt_ctx->server = NULL; 3317 3317 mnt_ctx->cifs_sb->mnt_cifs_flags &= ~CIFS_MOUNT_POSIX_PATHS; 3318 3318 free_xid(mnt_ctx->xid); 3319 3319 } ··· 3595 3589 bool isdfs; 3596 3590 int rc; 3597 3591 3598 - INIT_LIST_HEAD(&mnt_ctx.dfs_ses_list); 3599 - 3600 3592 rc = dfs_mount_share(&mnt_ctx, &isdfs); 3601 3593 if (rc) 3602 3594 goto error; ··· 3625 3621 return rc; 3626 3622 3627 3623 error: 3628 - dfs_put_root_smb_sessions(&mnt_ctx.dfs_ses_list); 3629 3624 cifs_mount_put_conns(&mnt_ctx); 3630 3625 return rc; 3631 3626 } ··· 3639 3636 goto error; 3640 3637 3641 3638 rc = cifs_mount_get_tcon(&mnt_ctx); 3639 + if (!rc) { 3640 + /* 3641 + * Prevent superblock from being created with any missing 3642 + * connections. 3643 + */ 3644 + if (WARN_ON(!mnt_ctx.server)) 3645 + rc = -EHOSTDOWN; 3646 + else if (WARN_ON(!mnt_ctx.ses)) 3647 + rc = -EACCES; 3648 + else if (WARN_ON(!mnt_ctx.tcon)) 3649 + rc = -ENOENT; 3650 + } 3642 3651 if (rc) 3643 3652 goto error; 3644 3653
+24 -27
fs/smb/client/dfs.c
··· 66 66 } 67 67 68 68 /* 69 - * Track individual DFS referral servers used by new DFS mount. 70 - * 71 - * On success, their lifetime will be shared by final tcon (dfs_ses_list). 72 - * Otherwise, they will be put by dfs_put_root_smb_sessions() in cifs_mount(). 69 + * Get an active reference of @ses so that next call to cifs_put_tcon() won't 70 + * release it as any new DFS referrals must go through its IPC tcon. 73 71 */ 74 - static int add_root_smb_session(struct cifs_mount_ctx *mnt_ctx) 72 + static void add_root_smb_session(struct cifs_mount_ctx *mnt_ctx) 75 73 { 76 74 struct smb3_fs_context *ctx = mnt_ctx->fs_ctx; 77 - struct dfs_root_ses *root_ses; 78 75 struct cifs_ses *ses = mnt_ctx->ses; 79 76 80 77 if (ses) { 81 - root_ses = kmalloc(sizeof(*root_ses), GFP_KERNEL); 82 - if (!root_ses) 83 - return -ENOMEM; 84 - 85 - INIT_LIST_HEAD(&root_ses->list); 86 - 87 78 spin_lock(&cifs_tcp_ses_lock); 88 79 cifs_smb_ses_inc_refcount(ses); 89 80 spin_unlock(&cifs_tcp_ses_lock); 90 - root_ses->ses = ses; 91 - list_add_tail(&root_ses->list, &mnt_ctx->dfs_ses_list); 92 81 } 93 - /* Select new DFS referral server so that new referrals go through it */ 94 82 ctx->dfs_root_ses = ses; 95 - return 0; 96 83 } 97 84 98 85 static inline int parse_dfs_target(struct smb3_fs_context *ctx, ··· 172 185 continue; 173 186 } 174 187 175 - if (is_refsrv) { 176 - rc = add_root_smb_session(mnt_ctx); 177 - if (rc) 178 - goto out; 179 - } 188 + if (is_refsrv) 189 + add_root_smb_session(mnt_ctx); 180 190 181 191 rc = ref_walk_advance(rw); 182 192 if (!rc) { ··· 216 232 struct smb3_fs_context *ctx = mnt_ctx->fs_ctx; 217 233 struct cifs_tcon *tcon; 218 234 char *origin_fullpath; 235 + bool new_tcon = true; 219 236 int rc; 220 237 221 238 origin_fullpath = dfs_get_path(cifs_sb, ctx->source); ··· 224 239 return PTR_ERR(origin_fullpath); 225 240 226 241 rc = dfs_referral_walk(mnt_ctx); 242 + if (!rc) { 243 + /* 244 + * Prevent superblock from being created with any missing 245 + * connections. 246 + */ 247 + if (WARN_ON(!mnt_ctx->server)) 248 + rc = -EHOSTDOWN; 249 + else if (WARN_ON(!mnt_ctx->ses)) 250 + rc = -EACCES; 251 + else if (WARN_ON(!mnt_ctx->tcon)) 252 + rc = -ENOENT; 253 + } 227 254 if (rc) 228 255 goto out; 229 256 ··· 244 247 if (!tcon->origin_fullpath) { 245 248 tcon->origin_fullpath = origin_fullpath; 246 249 origin_fullpath = NULL; 250 + } else { 251 + new_tcon = false; 247 252 } 248 253 spin_unlock(&tcon->tc_lock); 249 254 250 - if (list_empty(&tcon->dfs_ses_list)) { 251 - list_replace_init(&mnt_ctx->dfs_ses_list, &tcon->dfs_ses_list); 255 + if (new_tcon) { 252 256 queue_delayed_work(dfscache_wq, &tcon->dfs_cache_work, 253 257 dfs_cache_get_ttl() * HZ); 254 - } else { 255 - dfs_put_root_smb_sessions(&mnt_ctx->dfs_ses_list); 256 258 } 257 259 258 260 out: ··· 294 298 if (rc) 295 299 return rc; 296 300 297 - ctx->dfs_root_ses = mnt_ctx->ses; 298 301 /* 299 302 * If called with 'nodfs' mount option, then skip DFS resolving. Otherwise unconditionally 300 303 * try to get an DFS referral (even cached) to determine whether it is an DFS mount. ··· 319 324 320 325 *isdfs = true; 321 326 add_root_smb_session(mnt_ctx); 322 - return __dfs_mount_share(mnt_ctx); 327 + rc = __dfs_mount_share(mnt_ctx); 328 + dfs_put_root_smb_sessions(mnt_ctx); 329 + return rc; 323 330 } 324 331 325 332 /* Update dfs referral path of superblock */
+21 -12
fs/smb/client/dfs.h
··· 7 7 #define _CIFS_DFS_H 8 8 9 9 #include "cifsglob.h" 10 + #include "cifsproto.h" 10 11 #include "fs_context.h" 12 + #include "dfs_cache.h" 11 13 #include "cifs_unicode.h" 12 14 #include <linux/namei.h> 13 15 ··· 116 114 ref_walk_tit(rw)); 117 115 } 118 116 119 - struct dfs_root_ses { 120 - struct list_head list; 121 - struct cifs_ses *ses; 122 - }; 123 - 124 117 int dfs_parse_target_referral(const char *full_path, const struct dfs_info3_param *ref, 125 118 struct smb3_fs_context *ctx); 126 119 int dfs_mount_share(struct cifs_mount_ctx *mnt_ctx, bool *isdfs); ··· 130 133 { 131 134 struct smb3_fs_context *ctx = mnt_ctx->fs_ctx; 132 135 struct cifs_sb_info *cifs_sb = mnt_ctx->cifs_sb; 136 + struct cifs_ses *rses = ctx->dfs_root_ses ?: mnt_ctx->ses; 133 137 134 - return dfs_cache_find(mnt_ctx->xid, ctx->dfs_root_ses, cifs_sb->local_nls, 138 + return dfs_cache_find(mnt_ctx->xid, rses, cifs_sb->local_nls, 135 139 cifs_remap(cifs_sb), path, ref, tl); 136 140 } 137 141 138 - static inline void dfs_put_root_smb_sessions(struct list_head *head) 142 + /* 143 + * cifs_get_smb_ses() already guarantees an active reference of 144 + * @ses->dfs_root_ses when a new session is created, so we need to put extra 145 + * references of all DFS root sessions that were used across the mount process 146 + * in dfs_mount_share(). 147 + */ 148 + static inline void dfs_put_root_smb_sessions(struct cifs_mount_ctx *mnt_ctx) 139 149 { 140 - struct dfs_root_ses *root, *tmp; 150 + const struct smb3_fs_context *ctx = mnt_ctx->fs_ctx; 151 + struct cifs_ses *ses = ctx->dfs_root_ses; 152 + struct cifs_ses *cur; 141 153 142 - list_for_each_entry_safe(root, tmp, head, list) { 143 - list_del_init(&root->list); 144 - cifs_put_smb_ses(root->ses); 145 - kfree(root); 154 + if (!ses) 155 + return; 156 + 157 + for (cur = ses; cur; cur = cur->dfs_root_ses) { 158 + if (cur->dfs_root_ses) 159 + cifs_put_smb_ses(cur->dfs_root_ses); 146 160 } 161 + cifs_put_smb_ses(ses); 147 162 } 148 163 149 164 #endif /* _CIFS_DFS_H */
+1 -10
fs/smb/client/dfs_cache.c
··· 1278 1278 void dfs_cache_refresh(struct work_struct *work) 1279 1279 { 1280 1280 struct TCP_Server_Info *server; 1281 - struct dfs_root_ses *rses; 1282 1281 struct cifs_tcon *tcon; 1283 1282 struct cifs_ses *ses; 1284 1283 1285 1284 tcon = container_of(work, struct cifs_tcon, dfs_cache_work.work); 1286 - ses = tcon->ses; 1287 - server = ses->server; 1288 1285 1289 - mutex_lock(&server->refpath_lock); 1290 - if (server->leaf_fullpath) 1291 - __refresh_tcon(server->leaf_fullpath + 1, ses, false); 1292 - mutex_unlock(&server->refpath_lock); 1293 - 1294 - list_for_each_entry(rses, &tcon->dfs_ses_list, list) { 1295 - ses = rses->ses; 1286 + for (ses = tcon->ses; ses; ses = ses->dfs_root_ses) { 1296 1287 server = ses->server; 1297 1288 mutex_lock(&server->refpath_lock); 1298 1289 if (server->leaf_fullpath)
-6
fs/smb/client/misc.c
··· 138 138 atomic_set(&ret_buf->num_local_opens, 0); 139 139 atomic_set(&ret_buf->num_remote_opens, 0); 140 140 ret_buf->stats_from_time = ktime_get_real_seconds(); 141 - #ifdef CONFIG_CIFS_DFS_UPCALL 142 - INIT_LIST_HEAD(&ret_buf->dfs_ses_list); 143 - #endif 144 141 145 142 return ret_buf; 146 143 } ··· 153 156 atomic_dec(&tconInfoAllocCount); 154 157 kfree(tcon->nativeFileSystem); 155 158 kfree_sensitive(tcon->password); 156 - #ifdef CONFIG_CIFS_DFS_UPCALL 157 - dfs_put_root_smb_sessions(&tcon->dfs_ses_list); 158 - #endif 159 159 kfree(tcon->origin_fullpath); 160 160 kfree(tcon); 161 161 }