⬢ DragonFlyBSD Kernel Audit
DF-5788 / fix.diff
← back to finding ↓ download raw
--- a/sys/kern/vfs_cache.c
+++ b/sys/kern/vfs_cache.c
@@ -5070,10 +5070,22 @@ kern_getcwd(char *buf, size_t buflen, int *error)
 	fdp = p->p_fd;
 	slash_prefixed = 0;
 
+	/*
+	 * DF-5788: Snapshot the cwd nchandle under fd_spin (the same lock
+	 * chdir/fchdir/fdfree use to swap fd_ncdir) and take a reference on
+	 * the mount as well as the ncp.  The walk below dereferences
+	 * nch.mount (mnt_ncmountpt / mnt_ncmounton) on every component;
+	 * without a mount reference a concurrent umount -f can free the
+	 * mount structure out from under the walk (use-after-free), and
+	 * without fd_spin the snapshot races chdir's cache_drop of the old
+	 * handle.
+	 */
+	spin_lock_shared(&fdp->fd_spin);
 	nch = fdp->fd_ncdir;
 	ncp = nch.ncp;
 	if (ncp)
 		_cache_hold(ncp);
+	if (nch.mount)
+		_cache_mntref(nch.mount);
+	spin_unlock_shared(&fdp->fd_spin);
 
 	while (ncp && (ncp != fdp->fd_nrdir.ncp ||
 	       nch.mount != fdp->fd_nrdir.mount)
@@ -5088,10 +5100,26 @@ kern_getcwd(char *buf, size_t buflen, int *error)
 		 * While traversing upwards if we encounter the root
 		 * of the current mount we have to skip to the mount point
 		 * in the underlying filesystem.
+		 *
+		 * nch.mount is referenced by us so the mount structure
+		 * cannot be freed while we read it.  dounmount() may be
+		 * concurrently clearing mnt_ncmounton (cache_zero), so
+		 * reference the new mount before releasing the old one and
+		 * end the walk gracefully if we catch the transition
+		 * half-done (this also fixes a latent NULL deref in the
+		 * original code, which only avoided it by reading freed
+		 * memory).
 		 */
 		if (ncp == nch.mount->mnt_ncmountpt.ncp) {
+			struct mount *cur_mp = nch.mount;
+
 			nch = nch.mount->mnt_ncmounton;
+			if (nch.ncp == NULL || nch.mount == NULL) {
+				/* mount being torn down mid-read; end the
+				 * walk, releasing the ref we still hold */
+				_cache_mntrel(cur_mp);
+				nch.ncp = NULL;
+				nch.mount = NULL;
+				_cache_drop(ncp);
+				ncp = NULL;
+				break;
+			}
+			_cache_mntref(nch.mount);
 			_cache_drop(ncp);
 			ncp = nch.ncp;
 			if (ncp)
 				_cache_hold(ncp);
+			_cache_mntrel(cur_mp);
 			continue;
 		}
 
@@ -5156,6 +5184,10 @@ kern_getcwd(char *buf, size_t buflen, int *error)
 	*error = 0;
 done:
 	if (ncp)
 		_cache_drop(ncp);
+	/* DF-5788: release the mount reference held for the walk (none
+	 * if we never took one or broke out at a torn transition). */
+	if (nch.mount)
+		_cache_mntrel(nch.mount);
 	return (bp);
 }