fusefs: sanitize FUSE_READLINK results for embedded NULs

If VOP_READLINK returns a path that contains a NUL, it will trigger an
assertion in vfs_lookup.  Sanitize such paths in fusefs, rejecting any
and warning the user about the misbehaving server.

PR:		274268
Sponsored by:	Axcient
Reviewed by:	mjg, markj
Differential Revision: https://reviews.freebsd.org/D42081

(cherry picked from commit 662ec2f781521c36b76af748d74bb0a3c2e27a76)
This commit is contained in:
Alan Somers 2023-10-04 12:48:01 -06:00
parent 29de7af6ee
commit 8fca98f688
3 changed files with 47 additions and 0 deletions

View file

@ -239,6 +239,7 @@ struct fuse_data {
#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_WARN_READLINK_EMBEDDED_NUL 0x1000000 /* corrupt READLINK output */
#define FSESS_MNTOPTS_MASK ( \
FSESS_DAEMON_CAN_SPY | FSESS_PUSH_SYMLINKS_IN | \
FSESS_DEFAULT_PERMISSIONS | FSESS_INTR)

View file

@ -2007,6 +2007,13 @@ fuse_vnop_readlink(struct vop_readlink_args *ap)
if (err) {
goto out;
}
if (strnlen(fdi.answ, fdi.iosize) + 1 < fdi.iosize) {
struct fuse_data *data = fuse_get_mpdata(vnode_mount(vp));
fuse_warn(data, FSESS_WARN_READLINK_EMBEDDED_NUL,
"Returned an embedded NUL from FUSE_READLINK.");
err = EIO;
goto out;
}
if (((char *)fdi.answ)[0] == '/' &&
fuse_get_mpdata(vnode_mount(vp))->dataflags & FSESS_PUSH_SYMLINKS_IN) {
char *mpth = vnode_mount(vp)->mnt_stat.f_mntonname;

View file

@ -79,6 +79,45 @@ TEST_F(Readlink, eloop)
EXPECT_EQ(ELOOP, errno);
}
/*
* If a malicious or buggy server returns a NUL in the FUSE_READLINK result, it
* should be handled gracefully.
* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=274268
*/
TEST_F(Readlink, embedded_nul)
{
const char FULLPATH[] = "mountpoint/src";
const char RELPATH[] = "src";
const char dst[] = "dst\0stuff";
char buf[80];
const uint64_t ino = 42;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFLNK | 0777;
out.body.entry.nodeid = ino;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_READLINK &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
memcpy(out.body.str, dst, sizeof(dst));
out.header.len = sizeof(out.header) + sizeof(dst) + 1;
})));
EXPECT_EQ(-1, readlink(FULLPATH, buf, sizeof(buf)));
EXPECT_EQ(EIO, errno);
EXPECT_EQ(-1, access(FULLPATH, R_OK));
EXPECT_EQ(EIO, errno);
}
TEST_F(Readlink, ok)
{
const char FULLPATH[] = "mountpoint/src";