Commit 062a7f0f authored by Paulo Alcantara's avatar Paulo Alcantara Committed by Steve French
Browse files

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: default avatarkernel test robot <lkp@intel.com>
Closes: https://lore.kernel.org/oe-kbuild-all/202404021527.ZlRkIxgv-lkp@intel.com/


Signed-off-by: default avatarPaulo Alcantara (Red Hat) <pc@manguebit.com>
Signed-off-by: default avatarSteve French <stfrench@microsoft.com>
parent e9e62243
Loading
Loading
Loading
Loading
+0 −2
Original line number Diff line number Diff line
@@ -1281,7 +1281,6 @@ struct cifs_tcon {
	struct cached_fids *cfids;
	/* BB add field for back pointer to sb struct(s)? */
#ifdef CONFIG_CIFS_DFS_UPCALL
	struct list_head dfs_ses_list;
	struct delayed_work dfs_cache_work;
#endif
	struct delayed_work	query_interfaces; /* query interfaces workqueue job */
@@ -1804,7 +1803,6 @@ struct cifs_mount_ctx {
	struct TCP_Server_Info *server;
	struct cifs_ses *ses;
	struct cifs_tcon *tcon;
	struct list_head dfs_ses_list;
};

static inline void __free_dfs_info_param(struct dfs_info3_param *param)
+10 −10
Original line number Diff line number Diff line
@@ -725,31 +725,31 @@ struct super_block *cifs_get_tcon_super(struct cifs_tcon *tcon);
void cifs_put_tcon_super(struct super_block *sb);
int cifs_wait_for_server_reconnect(struct TCP_Server_Info *server, bool retry);

/* Put references of @ses and @ses->dfs_root_ses */
/* Put references of @ses and its children */
static inline void cifs_put_smb_ses(struct cifs_ses *ses)
{
	struct cifs_ses *rses = ses->dfs_root_ses;
	struct cifs_ses *next;

	do {
		next = ses->dfs_root_ses;
		__cifs_put_smb_ses(ses);
	if (rses)
		__cifs_put_smb_ses(rses);
	} while ((ses = next));
}

/* Get an active reference of @ses and @ses->dfs_root_ses.
/* Get an active reference of @ses and its children.
 *
 * NOTE: make sure to call this function when incrementing reference count of
 * @ses to ensure that any DFS root session attached to it (@ses->dfs_root_ses)
 * will also get its reference count incremented.
 *
 * cifs_put_smb_ses() will put both references, so call it when you're done.
 * cifs_put_smb_ses() will put all references, so call it when you're done.
 */
static inline void cifs_smb_ses_inc_refcount(struct cifs_ses *ses)
{
	lockdep_assert_held(&cifs_tcp_ses_lock);

	for (; ses; ses = ses->dfs_root_ses)
		ses->ses_count++;
	if (ses->dfs_root_ses)
		ses->dfs_root_ses->ses_count++;
}

static inline bool dfs_src_pathname_equal(const char *s1, const char *s2)
+20 −5
Original line number Diff line number Diff line
@@ -1866,6 +1866,9 @@ static int match_session(struct cifs_ses *ses, struct smb3_fs_context *ctx)
	    ctx->sectype != ses->sectype)
		return 0;

	if (ctx->dfs_root_ses != ses->dfs_root_ses)
		return 0;

	/*
	 * If an existing session is limited to less channels than
	 * requested, it should not be reused
@@ -2358,9 +2361,9 @@ cifs_get_smb_ses(struct TCP_Server_Info *server, struct smb3_fs_context *ctx)
	 * need to lock before changing something in the session.
	 */
	spin_lock(&cifs_tcp_ses_lock);
	if (ctx->dfs_root_ses)
		cifs_smb_ses_inc_refcount(ctx->dfs_root_ses);
	ses->dfs_root_ses = ctx->dfs_root_ses;
	if (ses->dfs_root_ses)
		ses->dfs_root_ses->ses_count++;
	list_add(&ses->smb_ses_list, &server->smb_ses_list);
	spin_unlock(&cifs_tcp_ses_lock);

@@ -3311,6 +3314,9 @@ void cifs_mount_put_conns(struct cifs_mount_ctx *mnt_ctx)
		cifs_put_smb_ses(mnt_ctx->ses);
	else if (mnt_ctx->server)
		cifs_put_tcp_session(mnt_ctx->server, 0);
	mnt_ctx->ses = NULL;
	mnt_ctx->tcon = NULL;
	mnt_ctx->server = NULL;
	mnt_ctx->cifs_sb->mnt_cifs_flags &= ~CIFS_MOUNT_POSIX_PATHS;
	free_xid(mnt_ctx->xid);
}
@@ -3589,8 +3595,6 @@ int cifs_mount(struct cifs_sb_info *cifs_sb, struct smb3_fs_context *ctx)
	bool isdfs;
	int rc;

	INIT_LIST_HEAD(&mnt_ctx.dfs_ses_list);

	rc = dfs_mount_share(&mnt_ctx, &isdfs);
	if (rc)
		goto error;
@@ -3621,7 +3625,6 @@ int cifs_mount(struct cifs_sb_info *cifs_sb, struct smb3_fs_context *ctx)
	return rc;

error:
	dfs_put_root_smb_sessions(&mnt_ctx.dfs_ses_list);
	cifs_mount_put_conns(&mnt_ctx);
	return rc;
}
@@ -3636,6 +3639,18 @@ int cifs_mount(struct cifs_sb_info *cifs_sb, struct smb3_fs_context *ctx)
		goto error;

	rc = cifs_mount_get_tcon(&mnt_ctx);
	if (!rc) {
		/*
		 * Prevent superblock from being created with any missing
		 * connections.
		 */
		if (WARN_ON(!mnt_ctx.server))
			rc = -EHOSTDOWN;
		else if (WARN_ON(!mnt_ctx.ses))
			rc = -EACCES;
		else if (WARN_ON(!mnt_ctx.tcon))
			rc = -ENOENT;
	}
	if (rc)
		goto error;

+24 −27
Original line number Diff line number Diff line
@@ -66,33 +66,20 @@ static int get_session(struct cifs_mount_ctx *mnt_ctx, const char *full_path)
}

/*
 * Track individual DFS referral servers used by new DFS mount.
 *
 * On success, their lifetime will be shared by final tcon (dfs_ses_list).
 * Otherwise, they will be put by dfs_put_root_smb_sessions() in cifs_mount().
 * Get an active reference of @ses so that next call to cifs_put_tcon() won't
 * release it as any new DFS referrals must go through its IPC tcon.
 */
static int add_root_smb_session(struct cifs_mount_ctx *mnt_ctx)
static void add_root_smb_session(struct cifs_mount_ctx *mnt_ctx)
{
	struct smb3_fs_context *ctx = mnt_ctx->fs_ctx;
	struct dfs_root_ses *root_ses;
	struct cifs_ses *ses = mnt_ctx->ses;

	if (ses) {
		root_ses = kmalloc(sizeof(*root_ses), GFP_KERNEL);
		if (!root_ses)
			return -ENOMEM;

		INIT_LIST_HEAD(&root_ses->list);

		spin_lock(&cifs_tcp_ses_lock);
		cifs_smb_ses_inc_refcount(ses);
		spin_unlock(&cifs_tcp_ses_lock);
		root_ses->ses = ses;
		list_add_tail(&root_ses->list, &mnt_ctx->dfs_ses_list);
	}
	/* Select new DFS referral server so that new referrals go through it */
	ctx->dfs_root_ses = ses;
	return 0;
}

static inline int parse_dfs_target(struct smb3_fs_context *ctx,
@@ -185,11 +172,8 @@ static int __dfs_referral_walk(struct cifs_mount_ctx *mnt_ctx,
					continue;
			}

			if (is_refsrv) {
				rc = add_root_smb_session(mnt_ctx);
				if (rc)
					goto out;
			}
			if (is_refsrv)
				add_root_smb_session(mnt_ctx);

			rc = ref_walk_advance(rw);
			if (!rc) {
@@ -232,6 +216,7 @@ static int __dfs_mount_share(struct cifs_mount_ctx *mnt_ctx)
	struct smb3_fs_context *ctx = mnt_ctx->fs_ctx;
	struct cifs_tcon *tcon;
	char *origin_fullpath;
	bool new_tcon = true;
	int rc;

	origin_fullpath = dfs_get_path(cifs_sb, ctx->source);
@@ -239,6 +224,18 @@ static int __dfs_mount_share(struct cifs_mount_ctx *mnt_ctx)
		return PTR_ERR(origin_fullpath);

	rc = dfs_referral_walk(mnt_ctx);
	if (!rc) {
		/*
		 * Prevent superblock from being created with any missing
		 * connections.
		 */
		if (WARN_ON(!mnt_ctx->server))
			rc = -EHOSTDOWN;
		else if (WARN_ON(!mnt_ctx->ses))
			rc = -EACCES;
		else if (WARN_ON(!mnt_ctx->tcon))
			rc = -ENOENT;
	}
	if (rc)
		goto out;

@@ -247,15 +244,14 @@ static int __dfs_mount_share(struct cifs_mount_ctx *mnt_ctx)
	if (!tcon->origin_fullpath) {
		tcon->origin_fullpath = origin_fullpath;
		origin_fullpath = NULL;
	} else {
		new_tcon = false;
	}
	spin_unlock(&tcon->tc_lock);

	if (list_empty(&tcon->dfs_ses_list)) {
		list_replace_init(&mnt_ctx->dfs_ses_list, &tcon->dfs_ses_list);
	if (new_tcon) {
		queue_delayed_work(dfscache_wq, &tcon->dfs_cache_work,
				   dfs_cache_get_ttl() * HZ);
	} else {
		dfs_put_root_smb_sessions(&mnt_ctx->dfs_ses_list);
	}

out:
@@ -298,7 +294,6 @@ int dfs_mount_share(struct cifs_mount_ctx *mnt_ctx, bool *isdfs)
	if (rc)
		return rc;

	ctx->dfs_root_ses = mnt_ctx->ses;
	/*
	 * If called with 'nodfs' mount option, then skip DFS resolving.  Otherwise unconditionally
	 * try to get an DFS referral (even cached) to determine whether it is an DFS mount.
@@ -324,7 +319,9 @@ int dfs_mount_share(struct cifs_mount_ctx *mnt_ctx, bool *isdfs)

	*isdfs = true;
	add_root_smb_session(mnt_ctx);
	return __dfs_mount_share(mnt_ctx);
	rc = __dfs_mount_share(mnt_ctx);
	dfs_put_root_smb_sessions(mnt_ctx);
	return rc;
}

/* Update dfs referral path of superblock */
+21 −12
Original line number Diff line number Diff line
@@ -7,7 +7,9 @@
#define _CIFS_DFS_H

#include "cifsglob.h"
#include "cifsproto.h"
#include "fs_context.h"
#include "dfs_cache.h"
#include "cifs_unicode.h"
#include <linux/namei.h>

@@ -114,11 +116,6 @@ static inline void ref_walk_set_tgt_hint(struct dfs_ref_walk *rw)
				       ref_walk_tit(rw));
}

struct dfs_root_ses {
	struct list_head list;
	struct cifs_ses *ses;
};

int dfs_parse_target_referral(const char *full_path, const struct dfs_info3_param *ref,
			      struct smb3_fs_context *ctx);
int dfs_mount_share(struct cifs_mount_ctx *mnt_ctx, bool *isdfs);
@@ -133,20 +130,32 @@ static inline int dfs_get_referral(struct cifs_mount_ctx *mnt_ctx, const char *p
{
	struct smb3_fs_context *ctx = mnt_ctx->fs_ctx;
	struct cifs_sb_info *cifs_sb = mnt_ctx->cifs_sb;
	struct cifs_ses *rses = ctx->dfs_root_ses ?: mnt_ctx->ses;

	return dfs_cache_find(mnt_ctx->xid, ctx->dfs_root_ses, cifs_sb->local_nls,
	return dfs_cache_find(mnt_ctx->xid, rses, cifs_sb->local_nls,
			      cifs_remap(cifs_sb), path, ref, tl);
}

static inline void dfs_put_root_smb_sessions(struct list_head *head)
/*
 * cifs_get_smb_ses() already guarantees an active reference of
 * @ses->dfs_root_ses when a new session is created, so we need to put extra
 * references of all DFS root sessions that were used across the mount process
 * in dfs_mount_share().
 */
static inline void dfs_put_root_smb_sessions(struct cifs_mount_ctx *mnt_ctx)
{
	struct dfs_root_ses *root, *tmp;
	const struct smb3_fs_context *ctx = mnt_ctx->fs_ctx;
	struct cifs_ses *ses = ctx->dfs_root_ses;
	struct cifs_ses *cur;

	if (!ses)
		return;

	list_for_each_entry_safe(root, tmp, head, list) {
		list_del_init(&root->list);
		cifs_put_smb_ses(root->ses);
		kfree(root);
	for (cur = ses; cur; cur = cur->dfs_root_ses) {
		if (cur->dfs_root_ses)
			cifs_put_smb_ses(cur->dfs_root_ses);
	}
	cifs_put_smb_ses(ses);
}

#endif /* _CIFS_DFS_H */
Loading