git: 93891ed2f794 - stable/12 - fusefs: make the mknod.cc tests a bit more general.

From: Alan Somers <asomers_at_FreeBSD.org>
Date: Sat, 20 Aug 2022 02:56:41 UTC
The branch stable/12 has been updated by asomers:

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

commit 93891ed2f794ba871f8cae5cc5fc04abced2d157
Author:     Alan Somers <asomers@FreeBSD.org>
AuthorDate: 2022-05-03 22:53:20 +0000
Commit:     Alan Somers <asomers@FreeBSD.org>
CommitDate: 2022-08-20 02:33:08 +0000

    fusefs: make the mknod.cc tests a bit more general.
    
    Reviewed by:    pfg
    
    (cherry picked from commit 8b582b16402102df10a715c626e212bbbc8e9d7c)
    
    fusefs: handle evil servers that return illegal inode numbers
    
    * If during FUSE_CREATE, FUSE_MKDIR, etc the server returns the same
      inode number for the new file as for its parent directory, reject it.
      Previously this would triggers a recurse-on-non-recursive lock panic.
    
    * If during FUSE_LINK the server returns a different inode number for
      the new name as for the old one, reject it.  Obviously, that can't be
      a hard link.
    
    * If during FUSE_LOOKUP the server returns the same inode number for the
      new file as for its parent directory, reject it.  Nothing good can
      come of this.
    
    PR:             263662
    Reported by:    Robert Morris <rtm@lcs.mit.edu>
    Reviewed by:    pfg
    Differential Revision: https://reviews.freebsd.org/D35128
    
    (cherry picked from commit 0bef4927ea858bb18b6f679bc0a36cff264dc842)
---
 sys/fs/fuse/fuse_ipc.h         |   1 +
 sys/fs/fuse/fuse_node.c        |   6 +++
 sys/fs/fuse/fuse_vnops.c       |  27 ++++++++--
 tests/sys/fs/fusefs/create.cc  |  41 +++++++++++++++
 tests/sys/fs/fusefs/link.cc    |  47 +++++++++++++++++
 tests/sys/fs/fusefs/lookup.cc  |  31 +++++++++++
 tests/sys/fs/fusefs/mkdir.cc   |  53 +++++++++++++++++++
 tests/sys/fs/fusefs/mknod.cc   | 117 ++++++++++++++++++++++++++++++++---------
 tests/sys/fs/fusefs/symlink.cc |  22 ++++++++
 9 files changed, 317 insertions(+), 28 deletions(-)

diff --git a/sys/fs/fuse/fuse_ipc.h b/sys/fs/fuse/fuse_ipc.h
index 980fb2541ca1..85a850ec916f 100644
--- a/sys/fs/fuse/fuse_ipc.h
+++ b/sys/fs/fuse/fuse_ipc.h
@@ -229,6 +229,7 @@ struct fuse_data {
 #define FSESS_WARN_LSEXTATTR_LONG 0x100000 /* Returned too many extattrs */
 #define FSESS_WARN_CACHE_INCOHERENT 0x200000	/* Read cache incoherent */
 #define FSESS_WARN_WB_CACHE_INCOHERENT 0x400000	/* WB cache incoherent */
+#define	FSESS_WARN_ILLEGAL_INODE  0x800000 /* Illegal inode for new file */
 #define FSESS_MNTOPTS_MASK	( \
 	FSESS_DAEMON_CAN_SPY | FSESS_PUSH_SYMLINKS_IN | \
 	FSESS_DEFAULT_PERMISSIONS | FSESS_INTR)
diff --git a/sys/fs/fuse/fuse_node.c b/sys/fs/fuse/fuse_node.c
index 7b56b9233728..f6688e011364 100644
--- a/sys/fs/fuse/fuse_node.c
+++ b/sys/fs/fuse/fuse_node.c
@@ -296,6 +296,12 @@ fuse_vnode_get(struct mount *mp,
 	uint64_t generation = feo ? feo->generation : 0;
 	int err = 0;
 
+	if (dvp != NULL && VTOFUD(dvp)->nid == nodeid) {
+		fuse_warn(fuse_get_mpdata(mp), FSESS_WARN_ILLEGAL_INODE,
+			"Assigned same inode to both parent and child.");
+		return EIO;
+	}
+
 	err = fuse_vnode_alloc(mp, td, nodeid, vtyp, vpp);
 	if (err) {
 		return err;
diff --git a/sys/fs/fuse/fuse_vnops.c b/sys/fs/fuse/fuse_vnops.c
index c390c5d7792c..249894d4037f 100644
--- a/sys/fs/fuse/fuse_vnops.c
+++ b/sys/fs/fuse/fuse_vnops.c
@@ -949,6 +949,16 @@ fuse_vnop_link(struct vop_link_args *ap)
 	}
 	feo = fdi.answ;
 
+	if (fli.oldnodeid != feo->nodeid) {
+		struct fuse_data *data = fuse_get_mpdata(vnode_mount(vp));
+		fuse_warn(data, FSESS_WARN_ILLEGAL_INODE,
+			"Assigned wrong inode for a hard link.");
+		fuse_vnode_clear_attr_cache(vp);
+		fuse_vnode_clear_attr_cache(tdvp);
+		err = EIO;
+		goto out;
+	}
+
 	err = fuse_internal_checkentry(feo, vnode_vtype(vp));
 	if (!err) {
 		/* 
@@ -1008,6 +1018,7 @@ fuse_vnop_lookup(struct vop_lookup_args *ap)
 	struct mount *mp = vnode_mount(dvp);
 	struct fuse_data *data = fuse_get_mpdata(mp);
 	int default_permissions = data->dataflags & FSESS_DEFAULT_PERMISSIONS;
+	bool is_dot;
 
 	int err = 0;
 	int lookup_err = 0;
@@ -1036,6 +1047,7 @@ fuse_vnop_lookup(struct vop_lookup_args *ap)
 	else if ((err = fuse_internal_access(dvp, VEXEC, td, cred)))
 		return err;
 
+	is_dot = cnp->cn_namelen == 1 && *(cnp->cn_nameptr) == '.';
 	if ((flags & ISDOTDOT) && !(data->dataflags & FSESS_EXPORT_SUPPORT))
 	{
 		if (!(VTOFUD(dvp)->flag & FN_PARENT_NID)) {
@@ -1051,7 +1063,7 @@ fuse_vnop_lookup(struct vop_lookup_args *ap)
 		/* .. is obviously a directory */
 		vtyp = VDIR;
 		filesize = 0;
-	} else if (cnp->cn_namelen == 1 && *(cnp->cn_nameptr) == '.') {
+	} else if (is_dot) {
 		nid = VTOI(dvp);
 		/* . is obviously a directory */
 		vtyp = VDIR;
@@ -1172,8 +1184,17 @@ fuse_vnop_lookup(struct vop_lookup_args *ap)
 				&vp);
 			*vpp = vp;
 		} else if (nid == VTOI(dvp)) {
-			vref(dvp);
-			*vpp = dvp;
+			if (is_dot) {
+				vref(dvp);
+				*vpp = dvp;
+			} else {
+				fuse_warn(fuse_get_mpdata(mp),
+				    FSESS_WARN_ILLEGAL_INODE,
+				    "Assigned same inode to both parent and "
+				    "child.");
+				err = EIO;
+			}
+
 		} else {
 			struct fuse_vnode_data *fvdat;
 
diff --git a/tests/sys/fs/fusefs/create.cc b/tests/sys/fs/fusefs/create.cc
index df3225ed1837..9f5820a00b3a 100644
--- a/tests/sys/fs/fusefs/create.cc
+++ b/tests/sys/fs/fusefs/create.cc
@@ -370,6 +370,47 @@ TEST_F(Create, ok)
 	leak(fd);
 }
 
+/*
+ * Nothing bad should happen if the server returns the parent's inode number
+ * for the newly created file.  Regression test for bug 263662
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=263662
+ */
+TEST_F(Create, parent_inode)
+{
+	const char FULLPATH[] = "mountpoint/some_dir/some_file.txt";
+	const char RELDIRPATH[] = "some_dir";
+	const char RELPATH[] = "some_file.txt";
+	mode_t mode = 0755;
+	uint64_t ino = 42;
+	int fd;
+
+	expect_lookup(RELDIRPATH, ino, S_IFDIR | mode, 0, 1);
+	EXPECT_LOOKUP(ino, RELPATH)
+		.WillOnce(Invoke(ReturnErrno(ENOENT)));
+	expect_create(RELPATH, S_IFREG | mode,
+		ReturnImmediate([=](auto in __unused, auto& out) {
+		SET_OUT_HEADER_LEN(out, create);
+		out.body.create.entry.attr.mode = S_IFREG | mode;
+		/* Return the same inode as the parent dir */
+		out.body.create.entry.nodeid = ino;
+		out.body.create.entry.entry_valid = UINT64_MAX;
+		out.body.create.entry.attr_valid = UINT64_MAX;
+	}));
+	// FUSE_RELEASE happens asynchronously, so it may or may not arrive
+	// before the test completes.
+	EXPECT_CALL(*m_mock, process(
+		ResultOf([=](auto in) {
+			return (in.header.opcode == FUSE_RELEASE);
+		}, Eq(true)),
+		_)
+	).Times(AtMost(1))
+	.WillOnce(Invoke([=](auto in __unused, auto &out __unused) { }));
+
+	fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
+	ASSERT_EQ(-1, fd);
+	EXPECT_EQ(EIO, errno);
+}
+
 /*
  * A regression test for a bug that affected old FUSE implementations:
  * open(..., O_WRONLY | O_CREAT, 0444) should work despite the seeming
diff --git a/tests/sys/fs/fusefs/link.cc b/tests/sys/fs/fusefs/link.cc
index 3d9a5a4e0e8f..789d3dcc3494 100644
--- a/tests/sys/fs/fusefs/link.cc
+++ b/tests/sys/fs/fusefs/link.cc
@@ -176,6 +176,53 @@ TEST_F(Link, emlink)
 	EXPECT_EQ(EMLINK, errno);
 }
 
+/*
+ * A hard link should always have the same inode as its source.  If it doesn't,
+ * then it's not a hard link.
+ */
+TEST_F(Link, bad_inode)
+{
+	const char FULLPATH[] = "mountpoint/src";
+	const char RELPATH[] = "src";
+	const char FULLDST[] = "mountpoint/dst";
+	const char RELDST[] = "dst";
+	const uint64_t src_ino = 42;
+	const uint64_t dst_ino = 43;
+	mode_t mode = S_IFREG | 0644;
+
+	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+	.WillOnce(Invoke(ReturnErrno(ENOENT)));
+	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
+	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+		SET_OUT_HEADER_LEN(out, entry);
+		out.body.entry.attr.mode = mode;
+		out.body.entry.nodeid = dst_ino;
+		out.body.entry.attr.nlink = 1;
+		out.body.entry.attr_valid = UINT64_MAX;
+		out.body.entry.entry_valid = UINT64_MAX;
+	})));
+	EXPECT_CALL(*m_mock, process(
+		ResultOf([=](auto in) {
+			const char *name = (const char*)in.body.bytes
+				+ sizeof(struct fuse_link_in);
+			return (in.header.opcode == FUSE_LINK &&
+				in.body.link.oldnodeid == dst_ino &&
+				(0 == strcmp(name, RELPATH)));
+		}, Eq(true)),
+		_)
+	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+		SET_OUT_HEADER_LEN(out, entry);
+		out.body.entry.nodeid = src_ino;
+		out.body.entry.attr.mode = mode;
+		out.body.entry.attr.nlink = 2;
+		out.body.entry.attr_valid = UINT64_MAX;
+		out.body.entry.entry_valid = UINT64_MAX;
+	})));
+
+	ASSERT_EQ(-1, link(FULLDST, FULLPATH));
+	ASSERT_EQ(EIO, errno);
+}
+
 TEST_F(Link, ok)
 {
 	const char FULLPATH[] = "mountpoint/src";
diff --git a/tests/sys/fs/fusefs/lookup.cc b/tests/sys/fs/fusefs/lookup.cc
index 0ec02913f66a..c654dd46bae5 100644
--- a/tests/sys/fs/fusefs/lookup.cc
+++ b/tests/sys/fs/fusefs/lookup.cc
@@ -430,6 +430,37 @@ TEST_F(Lookup, ok)
 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
 }
 
+/*
+ * Lookup in a subdirectory of the fuse mount.  The naughty server returns the
+ * same inode for the child as for the parent.
+ */
+TEST_F(Lookup, parent_inode)
+{
+	const char FULLPATH[] = "mountpoint/some_dir/some_file.txt";
+	const char DIRPATH[] = "some_dir";
+	const char RELPATH[] = "some_file.txt";
+	uint64_t dir_ino = 2;
+
+	EXPECT_LOOKUP(FUSE_ROOT_ID, DIRPATH)
+	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+		SET_OUT_HEADER_LEN(out, entry);
+		out.body.entry.attr.mode = S_IFDIR | 0755;
+		out.body.entry.nodeid = dir_ino;
+	})));
+	EXPECT_LOOKUP(dir_ino, RELPATH)
+	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+		SET_OUT_HEADER_LEN(out, entry);
+		out.body.entry.attr.mode = S_IFREG | 0644;
+		out.body.entry.nodeid = dir_ino;
+	})));
+	/*
+	 * access(2) is one of the few syscalls that will not (always) follow
+	 * up a successful VOP_LOOKUP with another VOP.
+	 */
+	ASSERT_EQ(-1, access(FULLPATH, F_OK));
+	ASSERT_EQ(EIO, errno);
+}
+
 // Lookup in a subdirectory of the fuse mount
 TEST_F(Lookup, subdir)
 {
diff --git a/tests/sys/fs/fusefs/mkdir.cc b/tests/sys/fs/fusefs/mkdir.cc
index 45efd08cfc80..f47189d9bf53 100644
--- a/tests/sys/fs/fusefs/mkdir.cc
+++ b/tests/sys/fs/fusefs/mkdir.cc
@@ -193,6 +193,59 @@ TEST_F(Mkdir, ok)
 	ASSERT_EQ(0, mkdir(FULLPATH, mode)) << strerror(errno);
 }
 
+/*
+ * Nothing bad should happen if the server returns the parent's inode number
+ * for the newly created directory.  Regression test for bug 263662.
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=263662
+ */
+TEST_F(Mkdir, parent_inode)
+{
+	const char FULLPATH[] = "mountpoint/parent/some_dir";
+	const char PPATH[] = "parent";
+	const char RELPATH[] = "some_dir";
+	mode_t mode = 0755;
+	uint64_t ino = 42;
+	mode_t mask;
+
+	mask = umask(0);
+	(void)umask(mask);
+
+	expect_lookup(PPATH, ino, S_IFDIR | 0755, 0, 1);
+	EXPECT_LOOKUP(ino, RELPATH)
+	.WillOnce(Invoke(ReturnErrno(ENOENT)));
+
+	EXPECT_CALL(*m_mock, process(
+		ResultOf([=](auto in) {
+			const char *name = (const char*)in.body.bytes +
+				sizeof(fuse_mkdir_in);
+			return (in.header.opcode == FUSE_MKDIR &&
+				in.body.mkdir.mode == (S_IFDIR | mode) &&
+				in.body.mkdir.umask == mask &&
+				(0 == strcmp(RELPATH, name)));
+		}, Eq(true)),
+		_)
+	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+		SET_OUT_HEADER_LEN(out, entry);
+		out.body.create.entry.attr.mode = S_IFDIR | mode;
+		out.body.create.entry.nodeid = ino;
+		out.body.create.entry.entry_valid = UINT64_MAX;
+		out.body.create.entry.attr_valid = UINT64_MAX;
+	})));
+	// FUSE_FORGET happens asynchronously, so it may or may not arrive
+	// before the test completes.
+	EXPECT_CALL(*m_mock, process(
+		ResultOf([=](auto in) {
+			return (in.header.opcode == FUSE_FORGET);
+		}, Eq(true)),
+		_)
+	).Times(AtMost(1))
+	.WillOnce(Invoke([=](auto in __unused, auto &out __unused) { }));
+
+	ASSERT_EQ(-1, mkdir(FULLPATH, mode));
+	ASSERT_EQ(EIO, errno);
+	usleep(100000);
+}
+
 TEST_F(Mkdir_7_8, ok)
 {
 	const char FULLPATH[] = "mountpoint/some_dir";
diff --git a/tests/sys/fs/fusefs/mknod.cc b/tests/sys/fs/fusefs/mknod.cc
index 5e16472cbe90..75f7c2c46adc 100644
--- a/tests/sys/fs/fusefs/mknod.cc
+++ b/tests/sys/fs/fusefs/mknod.cc
@@ -45,9 +45,6 @@ using namespace testing;
 #define VNOVAL (-1)	/* Defined in sys/vnode.h */
 #endif
 
-const char FULLPATH[] = "mountpoint/some_file.txt";
-const char RELPATH[] = "some_file.txt";
-
 class Mknod: public FuseTest {
 
 mode_t m_oldmask;
@@ -72,21 +69,19 @@ virtual void TearDown() {
 }
 
 /* Test an OK creation of a file with the given mode and device number */
-void expect_mknod(mode_t mode, dev_t dev) {
-	uint64_t ino = 42;
-
-	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
-	.WillOnce(Invoke(ReturnErrno(ENOENT)));
-
+void expect_mknod(uint64_t parent_ino, const char* relpath, uint64_t ino,
+		mode_t mode, dev_t dev)
+{
 	EXPECT_CALL(*m_mock, process(
 		ResultOf([=](auto in) {
 			const char *name = (const char*)in.body.bytes +
 				sizeof(fuse_mknod_in);
-			return (in.header.opcode == FUSE_MKNOD &&
+			return (in.header.nodeid == parent_ino &&
+				in.header.opcode == FUSE_MKNOD &&
 				in.body.mknod.mode == mode &&
 				in.body.mknod.rdev == (uint32_t)dev &&
 				in.body.mknod.umask == c_umask &&
-				(0 == strcmp(RELPATH, name)));
+				(0 == strcmp(relpath, name)));
 		}, Eq(true)),
 		_)
 	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
@@ -117,20 +112,18 @@ void expect_lookup(const char *relpath, uint64_t ino, uint64_t size)
 }
 
 /* Test an OK creation of a file with the given mode and device number */
-void expect_mknod(mode_t mode, dev_t dev) {
-	uint64_t ino = 42;
-
-	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
-	.WillOnce(Invoke(ReturnErrno(ENOENT)));
-
+void expect_mknod(uint64_t parent_ino, const char* relpath, uint64_t ino,
+		mode_t mode, dev_t dev)
+{
 	EXPECT_CALL(*m_mock, process(
 		ResultOf([=](auto in) {
 			const char *name = (const char*)in.body.bytes +
 				FUSE_COMPAT_MKNOD_IN_SIZE;
-			return (in.header.opcode == FUSE_MKNOD &&
+			return (in.header.nodeid == parent_ino &&
+				in.header.opcode == FUSE_MKNOD &&
 				in.body.mknod.mode == mode &&
 				in.body.mknod.rdev == (uint32_t)dev &&
-				(0 == strcmp(RELPATH, name)));
+				(0 == strcmp(relpath, name)));
 		}, Eq(true)),
 		_)
 	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
@@ -152,17 +145,31 @@ void expect_mknod(mode_t mode, dev_t dev) {
  */
 TEST_F(Mknod, blk)
 {
+	const char FULLPATH[] = "mountpoint/some_node";
+	const char RELPATH[] = "some_node";
 	mode_t mode = S_IFBLK | 0755;
 	dev_t rdev = 0xfe00; /* /dev/vda's device number on Linux */
-	expect_mknod(mode, rdev);
+	uint64_t ino = 42;
+
+	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+	.WillOnce(Invoke(ReturnErrno(ENOENT)));
+	expect_mknod(FUSE_ROOT_ID, RELPATH, ino, mode, rdev);
+
 	EXPECT_EQ(0, mknod(FULLPATH, mode, rdev)) << strerror(errno);
 }
 
 TEST_F(Mknod, chr)
 {
+	const char FULLPATH[] = "mountpoint/some_node";
+	const char RELPATH[] = "some_node";
 	mode_t mode = S_IFCHR | 0755;
 	dev_t rdev = 54;			/* /dev/fuse's device number */
-	expect_mknod(mode, rdev);
+	uint64_t ino = 42;
+
+	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+	.WillOnce(Invoke(ReturnErrno(ENOENT)));
+	expect_mknod(FUSE_ROOT_ID, RELPATH, ino, mode, rdev);
+
 	EXPECT_EQ(0, mknod(FULLPATH, mode, rdev)) << strerror(errno);
 }
 
@@ -172,6 +179,8 @@ TEST_F(Mknod, chr)
  */
 TEST_F(Mknod, eperm)
 {
+	const char FULLPATH[] = "mountpoint/some_node";
+	const char RELPATH[] = "some_node";
 	mode_t mode = S_IFIFO | 0755;
 
 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
@@ -193,9 +202,16 @@ TEST_F(Mknod, eperm)
 
 TEST_F(Mknod, fifo)
 {
+	const char FULLPATH[] = "mountpoint/some_node";
+	const char RELPATH[] = "some_node";
 	mode_t mode = S_IFIFO | 0755;
 	dev_t rdev = VNOVAL;		/* Fifos don't have device numbers */
-	expect_mknod(mode, rdev);
+	uint64_t ino = 42;
+
+	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+	.WillOnce(Invoke(ReturnErrno(ENOENT)));
+	expect_mknod(FUSE_ROOT_ID, RELPATH, ino, mode, rdev);
+
 	EXPECT_EQ(0, mkfifo(FULLPATH, mode)) << strerror(errno);
 }
 
@@ -206,12 +222,17 @@ TEST_F(Mknod, fifo)
  */
 TEST_F(Mknod, socket)
 {
+	const char FULLPATH[] = "mountpoint/some_node";
+	const char RELPATH[] = "some_node";
 	mode_t mode = S_IFSOCK | 0755;
 	struct sockaddr_un sa;
 	int fd;
 	dev_t rdev = -1;	/* Really it's a don't care */
+	uint64_t ino = 42;
 
-	expect_mknod(mode, rdev);
+	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+	.WillOnce(Invoke(ReturnErrno(ENOENT)));
+	expect_mknod(FUSE_ROOT_ID, RELPATH, ino, mode, rdev);
 
 	fd = socket(AF_UNIX, SOCK_STREAM, 0);
 	ASSERT_LE(0, fd) << strerror(errno);
@@ -224,23 +245,69 @@ TEST_F(Mknod, socket)
 	leak(fd);
 }
 
+/*
+ * Nothing bad should happen if the server returns the parent's inode number
+ * for the newly created file.  Regression test for bug 263662.
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=263662
+ */
+TEST_F(Mknod, parent_inode)
+{
+	const char FULLPATH[] = "mountpoint/parent/some_node";
+	const char PPATH[] = "parent";
+	const char RELPATH[] = "some_node";
+	mode_t mode = S_IFSOCK | 0755;
+	struct sockaddr_un sa;
+	int fd;
+	dev_t rdev = -1;	/* Really it's a don't care */
+	uint64_t ino = 42;
+
+	expect_lookup(PPATH, ino, S_IFDIR | 0755, 0, 1);
+	EXPECT_LOOKUP(ino, RELPATH)
+	.WillOnce(Invoke(ReturnErrno(ENOENT)));
+	expect_mknod(ino, RELPATH, ino, mode, rdev);
+
+	fd = socket(AF_UNIX, SOCK_STREAM, 0);
+	ASSERT_LE(0, fd) << strerror(errno);
+	sa.sun_family = AF_UNIX;
+	strlcpy(sa.sun_path, FULLPATH, sizeof(sa.sun_path));
+	sa.sun_len = sizeof(FULLPATH);
+	ASSERT_EQ(-1, bind(fd, (struct sockaddr*)&sa, sizeof(sa)));
+	ASSERT_EQ(EIO, errno);
+
+	leak(fd);
+}
+
 /* 
  * fusefs(5) lacks VOP_WHITEOUT support.  No bugzilla entry, because that's a
  * feature, not a bug
  */
 TEST_F(Mknod, DISABLED_whiteout)
 {
+	const char FULLPATH[] = "mountpoint/some_node";
+	const char RELPATH[] = "some_node";
 	mode_t mode = S_IFWHT | 0755;
 	dev_t rdev = VNOVAL;	/* whiteouts don't have device numbers */
-	expect_mknod(mode, rdev);
+	uint64_t ino = 42;
+
+	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+	.WillOnce(Invoke(ReturnErrno(ENOENT)));
+	expect_mknod(FUSE_ROOT_ID, RELPATH, ino, mode, rdev);
+
 	EXPECT_EQ(0, mknod(FULLPATH, mode, 0)) << strerror(errno);
 }
 
 /* A server built at protocol version 7.11 or earlier can still use mknod */
 TEST_F(Mknod_7_11, fifo)
 {
+	const char FULLPATH[] = "mountpoint/some_node";
+	const char RELPATH[] = "some_node";
 	mode_t mode = S_IFIFO | 0755;
 	dev_t rdev = VNOVAL;
-	expect_mknod(mode, rdev);
+	uint64_t ino = 42;
+
+	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+	.WillOnce(Invoke(ReturnErrno(ENOENT)));
+	expect_mknod(FUSE_ROOT_ID, RELPATH, ino, mode, rdev);
+
 	EXPECT_EQ(0, mkfifo(FULLPATH, mode)) << strerror(errno);
 }
diff --git a/tests/sys/fs/fusefs/symlink.cc b/tests/sys/fs/fusefs/symlink.cc
index 1ee5f79f91fb..bef06c90c3db 100644
--- a/tests/sys/fs/fusefs/symlink.cc
+++ b/tests/sys/fs/fusefs/symlink.cc
@@ -165,6 +165,28 @@ TEST_F(Symlink, ok)
 	EXPECT_EQ(0, symlink(dst, FULLPATH)) << strerror(errno);
 }
 
+/*
+ * Nothing bad should happen if the server returns the parent's inode number
+ * for the newly created symlink.  Regression test for bug 263662.
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=263662
+ */
+TEST_F(Symlink, parent_ino)
+{
+	const char FULLPATH[] = "mountpoint/parent/src";
+	const char PPATH[] = "parent";
+	const char RELPATH[] = "src";
+	const char dst[] = "dst";
+	const uint64_t ino = 42;
+
+	expect_lookup(PPATH, ino, S_IFDIR | 0755, 0, 1);
+	EXPECT_LOOKUP(ino, RELPATH)
+	.WillOnce(Invoke(ReturnErrno(ENOENT)));
+	expect_symlink(ino, dst, RELPATH);
+
+	EXPECT_EQ(-1, symlink(dst, FULLPATH));
+	EXPECT_EQ(EIO, errno);
+}
+
 TEST_F(Symlink_7_8, ok)
 {
 	const char FULLPATH[] = "mountpoint/src";