From nobody Fri Nov 21 15:19:23 2025 X-Original-To: dev-commits-src-branches@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4dCf5R48lZz6HLlw for ; Fri, 21 Nov 2025 15:19:23 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "R12" (verified OK)) by mx1.freebsd.org (Postfix) with ESMTPS id 4dCf5R2pJMz3nhH for ; Fri, 21 Nov 2025 15:19:23 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1763738363; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=pvcrkeGgUykL95KZHXJf9Cl+iUCuP4Rk6loSk1jDiO4=; b=fPcTIyYeEvMPEwtMJR7luB4gi9rMmpaHeyFdVRjm6eJ5LmUEd4n3CkkZ5Q2m0JkARba4Ty gEKMTasjWtql1wL5rYVD97UcVhVAqZG9KIc4AE1w0JJZm0hdykz17ODPSJP1Xco4ey68Cf 4TzdelxMrg4YUf+wRDxcL3UyXAbwjIea8in7miR028o5w3+OaYylB3NPVTvqjGlE/U8EwT M5Nyh1CaDjcGExoUORbawloz9P9Cjqd7R1h8KY6mMF9/OS5YJlh1NuLkIcwfkk0FgSXu0u EVM5zUKdqr4NUr0cLu6WAFeAmRjz8BihaQLFKWUcaGBcMBMVRxrNOIzTCLsWWA== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1763738363; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=pvcrkeGgUykL95KZHXJf9Cl+iUCuP4Rk6loSk1jDiO4=; b=wQ/i9cmVMyc2cS+Lk+yZvvuecBQ7ms4e3pwTJSm2jWJn1DibXLFIcLgR9uD0Lhg95uUSme 32k4hKWNc/f9RAO2LM7n5fnLOuac/0VtsXPt6e4nliVkgXuYeXK5+VHFkO5hDhaaUN/uR+ W14qiWgX0VMj4fUCTkDHfn7h2X/6FVSeJ05tphZVxQLGduqD5tp9XT67p0APGL1W5ebQCA awx4BYAQJi6wktTDuMSH1lvOrRZ8dDbcxqJzWpv+zdoZhmjZd1F//vRAmcpDyLWYIkLgVN Nwf0WZ69hdJr968fkrSrOgMVw0EmKl9UXiWDL5Tn2mF2N15oFkvvg5bfB/ipNw== ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1763738363; a=rsa-sha256; cv=none; b=pEZmva/YXZUUXBVXsaUiOxA2LMDfL5dmbBAcTyIuOeFF4oJvQVfRLN9A3sXE9kbxVwEntR ng9s8drYeXJr6oaw23U5yD3abf9l0sRNvn152xCNkoOXxfFH5Ye5BRLR5DbzNX6ltkdSDr fV97ryAQJ10YE9BEXtd3DyZaM+iFX1oh2hN0Tn8BF4/WsgxRMlX8+wpSTwrFH9Omt8I092 Fy/VcVt3yODcHvxelY2YOIqpZ+22iiERO6W+TLM+FOWWQiGPtgXqUskqnS1nStS96YDLYu 5OhBnztBTPRpiAqAizKUl59G1/YWcedASF5MfoVQgJ7W2pfQslASYKNHAqmszw== ARC-Authentication-Results: i=1; mx1.freebsd.org; none Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) by mxrelay.nyi.freebsd.org (Postfix) with ESMTP id 4dCf5R2JjXz2qw for ; Fri, 21 Nov 2025 15:19:23 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 31c89 by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Fri, 21 Nov 2025 15:19:23 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-branches@FreeBSD.org From: Mark Johnston Subject: git: 592de68e328b - stable/13 - namei: Make stackable filesystems check harder for jail roots List-Id: Commits to the stable branches of the FreeBSD src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-branches List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-branches@freebsd.org Sender: owner-dev-commits-src-branches@FreeBSD.org MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: markj X-Git-Repository: src X-Git-Refname: refs/heads/stable/13 X-Git-Reftype: branch X-Git-Commit: 592de68e328b4edf8acdb2a0b0b66a813747b440 Auto-Submitted: auto-generated Date: Fri, 21 Nov 2025 15:19:23 +0000 Message-Id: <692082fb.31c89.36c90828@gitrepo.freebsd.org> The branch stable/13 has been updated by markj: URL: https://cgit.FreeBSD.org/src/commit/?id=592de68e328b4edf8acdb2a0b0b66a813747b440 commit 592de68e328b4edf8acdb2a0b0b66a813747b440 Author: Mark Johnston AuthorDate: 2025-05-23 12:52:24 +0000 Commit: Mark Johnston CommitDate: 2025-11-19 20:47:50 +0000 namei: Make stackable filesystems check harder for jail roots Suppose a process has its cwd pointing to a nullfs directory, where the lower directory is also visible in the jail's filesystem namespace. Suppose that the lower directory vnode is moved out from under the nullfs mount. The nullfs vnode still shadows the lower vnode, and dotdot lookups relative to that directory will instantiate new nullfs vnodes outside of the nullfs mountpoint, effectively shadowing the lower filesystem. This phenomenon can be abused to escape a chroot, since the nullfs vnodes instantiated by these dotdot lookups defeat the root vnode check in vfs_lookup(), which uses vnode pointer equality to test for the process root. Fix this by extending nullfs and unionfs to perform the same check, exploiting the fact that the passed componentname is embedded in a nameidata structure to avoid changing the VOP_LOOKUP interface. That is, add a flag to indicate that containerof can be used to get the full nameidata structure, and perform the root vnode check on the lower vnode when performing a dotdot lookup. PR: 262180 Reviewed by: olce, kib MFC after: 2 weeks Differential Revision: https://reviews.freebsd.org/D50418 (cherry picked from commit 7587f6d4840f8d363e457cddc14c184cf1fe7cc1) --- sys/fs/nullfs/null_vnops.c | 28 +++++++++++++++++---------- sys/fs/unionfs/union_vnops.c | 19 ++++++++++++++++++ sys/kern/vfs_cache.c | 11 +---------- sys/kern/vfs_lookup.c | 46 +++++++++++++++++++++++++++++++------------- sys/sys/namei.h | 5 ++++- 5 files changed, 75 insertions(+), 34 deletions(-) diff --git a/sys/fs/nullfs/null_vnops.c b/sys/fs/nullfs/null_vnops.c index f2c426d41f61..90fe043f8b33 100644 --- a/sys/fs/nullfs/null_vnops.c +++ b/sys/fs/nullfs/null_vnops.c @@ -407,17 +407,25 @@ null_lookup(struct vop_lookup_args *ap) /* * Renames in the lower mounts might create an inconsistent - * configuration where lower vnode is moved out of the - * directory tree remounted by our null mount. Do not try to - * handle it fancy, just avoid VOP_LOOKUP() with DOTDOT name - * which cannot be handled by VOP, at least passing over lower - * root. + * configuration where lower vnode is moved out of the directory tree + * remounted by our null mount. + * + * Do not try to handle it fancy, just avoid VOP_LOOKUP() with DOTDOT + * name which cannot be handled by the VOP. */ - if ((ldvp->v_vflag & VV_ROOT) != 0 && (flags & ISDOTDOT) != 0) { - KASSERT((dvp->v_vflag & VV_ROOT) == 0, - ("ldvp %p fl %#x dvp %p fl %#x flags %#jx", - ldvp, ldvp->v_vflag, dvp, dvp->v_vflag, (uintmax_t)flags)); - return (ENOENT); + if ((flags & ISDOTDOT) != 0) { + struct nameidata *ndp; + + if ((ldvp->v_vflag & VV_ROOT) != 0) { + KASSERT((dvp->v_vflag & VV_ROOT) == 0, + ("ldvp %p fl %#x dvp %p fl %#x flags %#jx", + ldvp, ldvp->v_vflag, dvp, dvp->v_vflag, + (uintmax_t)flags)); + return (ENOENT); + } + ndp = lookup_nameidata(cnp); + if (ndp != NULL && lookup_isroot(ndp, ldvp)) + return (ENOENT); } /* diff --git a/sys/fs/unionfs/union_vnops.c b/sys/fs/unionfs/union_vnops.c index cd57a5cae459..153a5db323d9 100644 --- a/sys/fs/unionfs/union_vnops.c +++ b/sys/fs/unionfs/union_vnops.c @@ -76,6 +76,21 @@ KASSERT(((vp)->v_op == &unionfs_vnodeops), \ ("unionfs: it is not unionfs-vnode")) +static bool +unionfs_lookup_isroot(struct componentname *cnp, struct vnode *dvp) +{ + struct nameidata *ndp; + + if (dvp == NULL) + return (false); + if ((dvp->v_vflag & VV_ROOT) != 0) + return (true); + ndp = lookup_nameidata(cnp); + if (ndp == NULL) + return (false); + return (lookup_isroot(ndp, dvp)); +} + static int unionfs_lookup(struct vop_cachedlookup_args *ap) { @@ -124,6 +139,10 @@ unionfs_lookup(struct vop_cachedlookup_args *ap) if (LOOKUP != nameiop && udvp == NULLVP) return (EROFS); + if (unionfs_lookup_isroot(cnp, udvp) || + unionfs_lookup_isroot(cnp, ldvp)) + return (ENOENT); + if (udvp != NULLVP) { dtmpvp = udvp; if (ldvp != NULLVP) diff --git a/sys/kern/vfs_cache.c b/sys/kern/vfs_cache.c index aacbd43403e1..7f3a3c9532f1 100644 --- a/sys/kern/vfs_cache.c +++ b/sys/kern/vfs_cache.c @@ -5129,7 +5129,6 @@ cache_fplookup_dotdot(struct cache_fpl *fpl) struct componentname *cnp; struct namecache *ncp; struct vnode *dvp; - struct prison *pr; u_char nc_flag; ndp = fpl->ndp; @@ -5141,15 +5140,7 @@ cache_fplookup_dotdot(struct cache_fpl *fpl) /* * XXX this is racy the same way regular lookup is */ - for (pr = cnp->cn_cred->cr_prison; pr != NULL; - pr = pr->pr_parent) - if (dvp == pr->pr_root) - break; - - if (dvp == ndp->ni_rootdir || - dvp == ndp->ni_topdir || - dvp == rootvnode || - pr != NULL) { + if (lookup_isroot(ndp, dvp)) { fpl->tvp = dvp; fpl->tvp_seqc = vn_seqc_read_any(dvp); if (seqc_in_modify(fpl->tvp_seqc)) { diff --git a/sys/kern/vfs_lookup.c b/sys/kern/vfs_lookup.c index cb013eb7ff83..dc8b7b92ccd4 100644 --- a/sys/kern/vfs_lookup.c +++ b/sys/kern/vfs_lookup.c @@ -530,12 +530,12 @@ namei(struct nameidata *ndp) cnp->cn_origflags = cnp->cn_flags; #endif ndp->ni_cnd.cn_cred = ndp->ni_cnd.cn_thread->td_ucred; - KASSERT(ndp->ni_resflags == 0, ("%s: garbage in ni_resflags: %x\n", + KASSERT(ndp->ni_resflags == 0, ("%s: garbage in ni_resflags: %x", __func__, ndp->ni_resflags)); KASSERT(cnp->cn_cred && td->td_proc, ("namei: bad cred/proc")); KASSERT((cnp->cn_flags & NAMEI_INTERNAL_FLAGS) == 0, - ("namei: unexpected flags: %" PRIx64 "\n", - cnp->cn_flags & NAMEI_INTERNAL_FLAGS)); + ("namei: unexpected flags: %#jx", + (uintmax_t)(cnp->cn_flags & NAMEI_INTERNAL_FLAGS))); if (cnp->cn_flags & NOCACHE) KASSERT(cnp->cn_nameiop != LOOKUP, ("%s: NOCACHE passed with LOOKUP", __func__)); @@ -761,6 +761,31 @@ needs_exclusive_leaf(struct mount *mp, int flags) _Static_assert(MAXNAMLEN == NAME_MAX, "MAXNAMLEN and NAME_MAX have different values"); + +struct nameidata * +lookup_nameidata(struct componentname *cnp) +{ + if ((cnp->cn_flags & NAMEILOOKUP) == 0) + return (NULL); + return (__containerof(cnp, struct nameidata, ni_cnd)); +} + +/* + * Would a dotdot lookup relative to dvp cause this lookup to cross a jail or + * chroot boundary? + */ +bool +lookup_isroot(struct nameidata *ndp, struct vnode *dvp) +{ + for (struct prison *pr = ndp->ni_cnd.cn_cred->cr_prison; pr != NULL; + pr = pr->pr_parent) { + if (dvp == pr->pr_root) + return (true); + } + return (dvp == ndp->ni_rootdir || dvp == ndp->ni_topdir || + dvp == rootvnode); +} + /* * Search a pathname. * This is a very central and rather complicated routine. @@ -808,7 +833,6 @@ lookup(struct nameidata *ndp) struct vnode *dp = NULL; /* the directory we are searching */ struct vnode *tdp; /* saved dp */ struct mount *mp; /* mount table entry */ - struct prison *pr; size_t prev_ni_pathlen; /* saved ndp->ni_pathlen */ int docache; /* == 0 do not cache last component */ int wantparent; /* 1 => wantparent or lockparent flag */ @@ -1008,15 +1032,11 @@ dirloop: goto bad; } for (;;) { - for (pr = cnp->cn_cred->cr_prison; pr != NULL; - pr = pr->pr_parent) - if (dp == pr->pr_root) - break; - bool isroot = dp == ndp->ni_rootdir || - dp == ndp->ni_topdir || dp == rootvnode || - pr != NULL; - if (isroot && (ndp->ni_lcf & - NI_LCF_STRICTRELATIVE) != 0) { + bool isroot; + + isroot = lookup_isroot(ndp, dp); + if (__predict_false(isroot && (ndp->ni_lcf & + NI_LCF_STRICTRELATIVE) != 0)) { error = ENOTCAPABLE; goto capdotdot; } diff --git a/sys/sys/namei.h b/sys/sys/namei.h index 1d0fd1c96449..053a64367ff9 100644 --- a/sys/sys/namei.h +++ b/sys/sys/namei.h @@ -154,6 +154,7 @@ int cache_fplookup(struct nameidata *ndp, enum cache_fpl_status *status, #define LOCKSHARED 0x0100 /* Shared lock leaf */ #define NOFOLLOW 0x0000 /* do not follow symbolic links (pseudo) */ #define RBENEATH 0x100000000ULL /* No escape, even tmp, from start dir */ +#define NAMEILOOKUP 0x200000000ULL /* cnp is embedded in nameidata */ #define MODMASK 0xf000001ffULL /* mask of operational modifiers */ /* @@ -254,7 +255,7 @@ do { \ NDINIT_PREFILL(_ndp); \ NDINIT_DBG(_ndp); \ _ndp->ni_cnd.cn_nameiop = op; \ - _ndp->ni_cnd.cn_flags = flags; \ + _ndp->ni_cnd.cn_flags = (flags) | NAMEILOOKUP; \ _ndp->ni_segflg = segflg; \ _ndp->ni_dirp = namep; \ _ndp->ni_dirfd = dirfd; \ @@ -312,6 +313,8 @@ void NDVALIDATE(struct nameidata *); int namei(struct nameidata *ndp); int lookup(struct nameidata *ndp); +bool lookup_isroot(struct nameidata *ndp, struct vnode *dvp); +struct nameidata *lookup_nameidata(struct componentname *cnp); int relookup(struct vnode *dvp, struct vnode **vpp, struct componentname *cnp); #endif