git: dd08b84e35b6 - main - nfscl: Fix a use after free in nfscl_cleanupkext()

From: Rick Macklem <rmacklem_at_FreeBSD.org>
Date: Tue, 22 Feb 2022 22:24:20 UTC
The branch main has been updated by rmacklem:

URL: https://cgit.FreeBSD.org/src/commit/?id=dd08b84e35b6fdee0df5fd0e0533cd361dec71cb

commit dd08b84e35b6fdee0df5fd0e0533cd361dec71cb
Author:     Rick Macklem <rmacklem@FreeBSD.org>
AuthorDate: 2022-02-22 22:21:43 +0000
Commit:     Rick Macklem <rmacklem@FreeBSD.org>
CommitDate: 2022-02-22 22:21:43 +0000

    nfscl: Fix a use after free in nfscl_cleanupkext()
    
    ler@, markj@ reported a use after free in nfscl_cleanupkext().
    They also provided two possible causes:
    - In nfscl_cleanup_common(), "own" is the owner string
      owp->nfsow_owner.  If we free that particular
      owner structure, than in subsequent comparisons
      "own" will point to freed memory.
    - nfscl_cleanup_common() can free more than one owner, so the use
      of LIST_FOREACH_SAFE() in nfscl_cleanupkext() is not sufficient.
    
    I also believe there is a 3rd:
    - If nfscl_freeopenowner() or nfscl_freelockowner() is called
      without the NFSCLSTATE mutex held, this could race with
      nfscl_cleanupkext().
      This could happen when the exclusive lock is held
      on the client, such as when delegations are being returned.
    
    This patch fixes them as follows:
    1 - Copy the owner string to a local variable before the
        nfscl_cleanup_common() call.
    2 - Modify nfscl_cleanup_common() to return whether or not a
        free was done.
        When a free was done, do a goto to restart the loop, instead
        of using FOREACH_SAFE, which was not safe in this case.
    3 - Acquire the NFSCLSTATE mutex in nfscl_freeopenowner()
        and nfscl_freelockowner(), if it not already held.
        This serializes all of these calls with the ones done in
        nfscl_cleanup_common().
    
    Reported by:    ler
    Reviewed by:    markj
    MFC after:      1 week
    Differential Revision:  https://reviews.freebsd.org/D34334
---
 sys/fs/nfsclient/nfs_clstate.c | 70 +++++++++++++++++++++++++++++++++++-------
 1 file changed, 59 insertions(+), 11 deletions(-)

diff --git a/sys/fs/nfsclient/nfs_clstate.c b/sys/fs/nfsclient/nfs_clstate.c
index 5262986645cd..c37ff8a8ab11 100644
--- a/sys/fs/nfsclient/nfs_clstate.c
+++ b/sys/fs/nfsclient/nfs_clstate.c
@@ -158,7 +158,7 @@ static int nfsrpc_reopen(struct nfsmount *, u_int8_t *, int, u_int32_t,
 static void nfscl_freedeleg(struct nfscldeleghead *, struct nfscldeleg *,
     bool);
 static int nfscl_errmap(struct nfsrv_descript *, u_int32_t);
-static void nfscl_cleanup_common(struct nfsclclient *, u_int8_t *);
+static u_int nfscl_cleanup_common(struct nfsclclient *, u_int8_t *);
 static int nfscl_recalldeleg(struct nfsclclient *, struct nfsmount *,
     struct nfscldeleg *, vnode_t, struct ucred *, NFSPROC_T *, int,
     vnode_t *);
@@ -1642,8 +1642,21 @@ nfscl_expireopen(struct nfsclclient *clp, struct nfsclopen *op,
 static void
 nfscl_freeopenowner(struct nfsclowner *owp, int local)
 {
+	int owned;
 
+	/*
+	 * Make sure the NFSCLSTATE mutex is held, to avoid races with
+	 * calls in nfscl_renewthread() that do not hold a reference
+	 * count on the nfsclclient and just the mutex.
+	 * The mutex will not be held for calls done with the exclusive
+	 * nfsclclient lock held.
+	 */
+	owned = mtx_owned(NFSCLSTATEMUTEXPTR);
+	if (owned == 0)
+		NFSLOCKCLSTATE();
 	LIST_REMOVE(owp, nfsow_list);
+	if (owned == 0)
+		NFSUNLOCKCLSTATE();
 	free(owp, M_NFSCLOWNER);
 	if (local)
 		nfsstatsv1.cllocalopenowners--;
@@ -1658,8 +1671,21 @@ void
 nfscl_freelockowner(struct nfscllockowner *lp, int local)
 {
 	struct nfscllock *lop, *nlop;
+	int owned;
 
+	/*
+	 * Make sure the NFSCLSTATE mutex is held, to avoid races with
+	 * calls in nfscl_renewthread() that do not hold a reference
+	 * count on the nfsclclient and just the mutex.
+	 * The mutex will not be held for calls done with the exclusive
+	 * nfsclclient lock held.
+	 */
+	owned = mtx_owned(NFSCLSTATEMUTEXPTR);
+	if (owned == 0)
+		NFSLOCKCLSTATE();
 	LIST_REMOVE(lp, nfsl_list);
+	if (owned == 0)
+		NFSUNLOCKCLSTATE();
 	LIST_FOREACH_SAFE(lop, &lp->nfsl_lock, nfslo_list, nlop) {
 		nfscl_freelock(lop, local);
 	}
@@ -1841,17 +1867,24 @@ nfscl_expireclient(struct nfsclclient *clp, struct nfsmount *nmp,
 	}
 }
 
+#define	FREED_OPENOWNER	0x1
+#define	FREED_LOCKOWNER	0x2
+
 /*
  * This function must be called after the process represented by "own" has
  * exited. Must be called with CLSTATE lock held.
+ * Return the FREED_xxx flag bits, since the caller needs to know if either
+ * the open owner or lock owner lists have changed.
  */
-static void
+static u_int
 nfscl_cleanup_common(struct nfsclclient *clp, u_int8_t *own)
 {
 	struct nfsclowner *owp, *nowp;
 	struct nfscllockowner *lp, *nlp;
 	struct nfscldeleg *dp;
+	u_int didfree;
 
+	didfree = 0;
 	/* First, get rid of local locks on delegations. */
 	TAILQ_FOREACH(dp, &clp->nfsc_deleg, nfsdl_list) {
 		LIST_FOREACH_SAFE(lp, &dp->nfsdl_lock, nfsl_list, nlp) {
@@ -1859,6 +1892,7 @@ nfscl_cleanup_common(struct nfsclclient *clp, u_int8_t *own)
 			if ((lp->nfsl_rwlock.nfslock_lock & NFSV4LOCK_WANTED))
 			    panic("nfscllckw");
 			nfscl_freelockowner(lp, 1);
+			didfree |= FREED_LOCKOWNER;
 		    }
 		}
 	}
@@ -1873,13 +1907,15 @@ nfscl_cleanup_common(struct nfsclclient *clp, u_int8_t *own)
 			 * here. For that case, let the renew thread clear
 			 * out the OpenOwner later.
 			 */
-			if (LIST_EMPTY(&owp->nfsow_open))
+			if (LIST_EMPTY(&owp->nfsow_open)) {
 				nfscl_freeopenowner(owp, 0);
-			else
+				didfree |= FREED_OPENOWNER;
+			} else
 				owp->nfsow_defunct = 1;
 		}
 		owp = nowp;
 	}
+	return (didfree);
 }
 
 /*
@@ -1888,10 +1924,11 @@ nfscl_cleanup_common(struct nfsclclient *clp, u_int8_t *own)
 static void
 nfscl_cleanupkext(struct nfsclclient *clp, struct nfscllockownerfhhead *lhp)
 {
-	struct nfsclowner *owp, *nowp;
+	struct nfsclowner *owp;
 	struct nfsclopen *op;
 	struct nfscllockowner *lp, *nlp;
 	struct nfscldeleg *dp;
+	uint8_t own[NFSV4CL_LOCKNAMELEN];
 
 	/*
 	 * All the pidhash locks must be acquired, since they are sx locks
@@ -1901,15 +1938,20 @@ nfscl_cleanupkext(struct nfsclclient *clp, struct nfscllockownerfhhead *lhp)
 	 */
 	pidhash_slockall();
 	NFSLOCKCLSTATE();
-	LIST_FOREACH_SAFE(owp, &clp->nfsc_owner, nfsow_list, nowp) {
+tryagain:
+	LIST_FOREACH(owp, &clp->nfsc_owner, nfsow_list) {
 		LIST_FOREACH(op, &owp->nfsow_open, nfso_list) {
 			LIST_FOREACH_SAFE(lp, &op->nfso_lock, nfsl_list, nlp) {
 				if (LIST_EMPTY(&lp->nfsl_lock))
 					nfscl_emptylockowner(lp, lhp);
 			}
 		}
-		if (nfscl_procdoesntexist(owp->nfsow_owner))
-			nfscl_cleanup_common(clp, owp->nfsow_owner);
+		if (nfscl_procdoesntexist(owp->nfsow_owner)) {
+			memcpy(own, owp->nfsow_owner, NFSV4CL_LOCKNAMELEN);
+			if ((nfscl_cleanup_common(clp, own) &
+			    FREED_OPENOWNER) != 0)
+				goto tryagain;
+		}
 	}
 
 	/*
@@ -1920,9 +1962,15 @@ nfscl_cleanupkext(struct nfsclclient *clp, struct nfscllockownerfhhead *lhp)
 	 * nfscl_cleanup_common().
 	 */
 	TAILQ_FOREACH(dp, &clp->nfsc_deleg, nfsdl_list) {
-		LIST_FOREACH_SAFE(lp, &dp->nfsdl_lock, nfsl_list, nlp) {
-			if (nfscl_procdoesntexist(lp->nfsl_owner))
-				nfscl_cleanup_common(clp, lp->nfsl_owner);
+tryagain2:
+		LIST_FOREACH(lp, &dp->nfsdl_lock, nfsl_list) {
+			if (nfscl_procdoesntexist(lp->nfsl_owner)) {
+				memcpy(own, lp->nfsl_owner,
+				    NFSV4CL_LOCKNAMELEN);
+				if ((nfscl_cleanup_common(clp, own) &
+				    FREED_LOCKOWNER) != 0)
+					goto tryagain2;
+			}
 		}
 	}
 	NFSUNLOCKCLSTATE();