git: 30758138f2e3 - stable/15 - unix: Preserve FD_RESOLVE_BENEATH when passing an fd

Mark Johnston <[email protected]>
Newsgroups gmane.os.freebsd.devel.cvs.src
Message-ID <6a74a1cb.45e56.769fc93e__28896.7509333574$1786028592$gmane$org@gitrepo.freebsd.org>
The branch stable/15 has been updated by markj:

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

commit 30758138f2e36d89bc30cb686796ccc4dfa8b475
Author:     Mark Johnston <[email protected]>
AuthorDate: 2026-07-24 20:06:05 +0000
Commit:     Mark Johnston <[email protected]>
CommitDate: 2026-08-06 12:52:28 +0000

    unix: Preserve FD_RESOLVE_BENEATH when passing an fd
    
    The FD_RESOLVE_BENEATH flag is supposed to be sticky.  It's set when you
    receive an fd from a different jail and preserved by openat(<dfd>) etc..
    However, if you send the fd to yourself, the flag is stripped since
    SCM_RIGHTS message don't preserve file descriptor flags.
    
    Fix this by preserving those flags and checking for UF_RESOLVE_BENEATH
    in restrict_rights().
    
    Fixes:          350ba9672a7f ("unix: Set O_RESOLVE_BENEATH on fds transferred between jails")
    Reviewed by:    kib
    MFC after:      1 week
    Sponsored by:   The FreeBSD Foundation
    Differential Revision:  https://reviews.freebsd.org/D58317
    
    (cherry picked from commit 586e2b3d89d6e70ab7e4a88497b5f36d78719423)
---
 sys/kern/uipc_usrreq.c            | 27 +++++++++++++++++++--------
 tests/sys/kern/unix_passfd_test.c | 31 +++++++++++++++++++++++++++++++
 2 files changed, 50 insertions(+), 8 deletions(-)

diff --git a/sys/kern/uipc_usrreq.c b/sys/kern/uipc_usrreq.c
index bbea5ef3914c..ed3e22ad63f6 100644
--- a/sys/kern/uipc_usrreq.c
+++ b/sys/kern/uipc_usrreq.c
@@ -3488,15 +3488,25 @@ unp_freerights(struct filedescent **fdep, int fdcount)
 	free(fdep[0], M_FILECAPS);
 }
 
-static bool
-restrict_rights(struct file *fp, struct thread *td)
+/*
+ * Flags to set on the receiving side when externalizing a file descriptor.
+ * When transferring fds between jails, ensure that the receiver cannot use
+ * a dirfd to escape the jail chroot.
+ */
+static int
+externalize_fdflags(struct filedescent *fde, struct thread *td)
 {
 	struct prison *prison1, *prison2;
 
-	prison1 = fp->f_cred->cr_prison;
+	if ((fde->fde_flags & UF_RESOLVE_BENEATH) != 0)
+		return (O_RESOLVE_BENEATH);
+	prison1 = fde->fde_file->f_cred->cr_prison;
 	prison2 = td->td_ucred->cr_prison;
-	return (prison1 != prison2 && prison1->pr_root != prison2->pr_root &&
-	    prison2 != &prison0);
+	if (prison1 != prison2 && prison1->pr_root != prison2->pr_root &&
+	    prison2 != &prison0)
+		return (O_RESOLVE_BENEATH);
+	else
+		return (0);
 }
 
 static int
@@ -3562,9 +3572,9 @@ unp_externalize(struct mbuf *control, struct mbuf **controlp, int flags)
 				struct file *fp;
 
 				fp = fdep[i]->fde_file;
-				_finstall(fdesc, fp, *fdp, fdflags |
-				    (restrict_rights(fp, td) ?
-				    O_RESOLVE_BENEATH : 0), &fdep[i]->fde_caps);
+				_finstall(fdesc, fp, *fdp,
+				    fdflags | externalize_fdflags(fdep[i], td),
+				    &fdep[i]->fde_caps);
 				unp_externalize_fp(fp);
 			}
 
@@ -3800,6 +3810,7 @@ unp_internalize(struct mbuf *control, struct mchain *mc, struct thread *td)
 				fdep[i]->fde_file = fde->fde_file;
 				filecaps_copy(&fde->fde_caps,
 				    &fdep[i]->fde_caps, true);
+				fdep[i]->fde_flags = fde->fde_flags;
 				unp_internalize_fp(fdep[i]->fde_file);
 			}
 			FILEDESC_SUNLOCK(fdesc);
diff --git a/tests/sys/kern/unix_passfd_test.c b/tests/sys/kern/unix_passfd_test.c
index 66bb406ea14e..566591522784 100644
--- a/tests/sys/kern/unix_passfd_test.c
+++ b/tests/sys/kern/unix_passfd_test.c
@@ -1189,6 +1189,36 @@ ATF_TC_CLEANUP(cross_jail_dirfd, tc)
 		err(1, "jail_remove");
 }
 
+/*
+ * Verify that FD_RESOLVE_BENEATH is preserved when an fd is passed over a UNIX
+ * domain socket.
+ */
+ATF_TC_WITHOUT_HEAD(resolve_beneath_preserved);
+ATF_TC_BODY(resolve_beneath_preserved, tc)
+{
+	int fd[2], getfd, putfd, fdflags;
+
+	domainsocketpair(fd);
+	tempfile(&putfd);
+
+	fdflags = fcntl(putfd, F_GETFD);
+	ATF_REQUIRE(fdflags != -1);
+	ATF_REQUIRE(fcntl(putfd, F_SETFD, fdflags | FD_RESOLVE_BENEATH) != -1);
+	ATF_REQUIRE((fcntl(putfd, F_GETFD) & FD_RESOLVE_BENEATH) != 0);
+
+	sendfd(fd[0], putfd);
+	recvfd(fd[1], &getfd, 0);
+
+	fdflags = fcntl(getfd, F_GETFD);
+	ATF_REQUIRE(fdflags != -1);
+	ATF_REQUIRE_MSG((fdflags & FD_RESOLVE_BENEATH) != 0,
+	    "FD_RESOLVE_BENEATH was not preserved across SCM_RIGHTS transfer");
+
+	ATF_REQUIRE(close(putfd) == 0);
+	ATF_REQUIRE(close(getfd) == 0);
+	closesocketpair(fd);
+}
+
 ATF_TC_WITHOUT_HEAD(listening_socket);
 ATF_TC_BODY(listening_socket, tc)
 {
@@ -1239,6 +1269,7 @@ ATF_TP_ADD_TCS(tp)
 	ATF_TP_ADD_TC(tp, empty_rights_message);
 	ATF_TP_ADD_TC(tp, control_creates_records);
 	ATF_TP_ADD_TC(tp, cross_jail_dirfd);
+	ATF_TP_ADD_TC(tp, resolve_beneath_preserved);
 	ATF_TP_ADD_TC(tp, listening_socket);
 
 	return (atf_no_error());
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.