executor/common_linux.h: add syz_fuse_handle_req()

At the moment syzkaller is able to respond to FUSE with a syntactically
correct response using the specific write$FUSE_*() syscalls, but most of
the times these responses are not related to the type of request that
was received.
With this pseudo-syscall we are able to provide the correct response
type while still allowing the fuzzer to fuzz its content. This is done
by requiring each type of response as an input parameter and then
choosing the correct one based on the request opcode.
Notice that the fuzzer is still free to mix write$FUSE_*() and
syz_fuse_handle_req() syscalls, so it is not losing any degree of
freedom.

syz_fuse_handle_req() retrieves the FUSE request and resource
fuse_unique internally (by performing a read() on the /dev/fuse file
descriptor provided as input). For this reason, a new template argument has
been added to fuse_out (renamed to _fuse_out) so that the unique field
can be both an int64 (used by syz_fuse_handle_req()) and a fuse_unique
resource (used by the write$FUSE_*() syscalls) without any code
duplication.
This commit is contained in:
Stefano Duo 2020-07-22 13:47:01 +00:00 committed by Dmitry Vyukov
parent 3d9b8afae8
commit 19b6584f71
5 changed files with 405 additions and 4 deletions

View File

@ -4259,3 +4259,184 @@ static void setup_usb()
// The macro is used in generated C code.
#define CAST(f) ({void* p = (void*)f; p; })
#endif
#if SYZ_EXECUTOR || __NR_syz_fuse_handle_req
#include <fcntl.h>
#include <linux/fuse.h>
#include <stddef.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
// Struct shared between syz_fuse_handle_req() and the fuzzer. Used to provide
// a fuzzed response for each request type.
struct syz_fuse_req_out {
struct fuse_out_header* init;
struct fuse_out_header* lseek;
struct fuse_out_header* bmap;
struct fuse_out_header* poll;
struct fuse_out_header* getxattr;
struct fuse_out_header* lk;
struct fuse_out_header* statfs;
struct fuse_out_header* write;
struct fuse_out_header* read;
struct fuse_out_header* open;
struct fuse_out_header* attr;
struct fuse_out_header* entry;
struct fuse_out_header* dirent;
struct fuse_out_header* direntplus;
struct fuse_out_header* create_open;
struct fuse_out_header* ioctl;
};
// Link the reponse to the request and send it to /dev/fuse.
static int
fuse_send_response(int fd,
const struct fuse_in_header* in_hdr,
struct fuse_out_header* out_hdr)
{
if (!out_hdr) {
debug("fuse_send_response: received a NULL out_hdr\n");
return -1;
}
out_hdr->unique = in_hdr->unique;
if (write(fd, out_hdr, out_hdr->len) == -1) {
debug("fuse_send_response > write failed: %d\n", errno);
return -1;
}
return 0;
}
// This function reads a request from /dev/fuse and tries to pick the correct
// response from the input struct syz_fuse_req_out (a3). Responses are still
// generated by the fuzzer.
static volatile long syz_fuse_handle_req(volatile long a0, // /dev/fuse fd.
volatile long a1, // Read buffer.
volatile long a2, // Buffer len.
volatile long a3) // syz_fuse_req_out.
{
struct syz_fuse_req_out* req_out = (struct syz_fuse_req_out*)a3;
struct fuse_out_header* out_hdr = NULL;
char* buf = (char*)a1;
int buf_len = (int)a2;
int fd = (int)a0;
if (!req_out) {
debug("syz_fuse_handle_req: received a NULL syz_fuse_req_out\n");
return -1;
}
if (buf_len < FUSE_MIN_READ_BUFFER) {
debug("FUSE requires the read buffer to be at least %u\n", FUSE_MIN_READ_BUFFER);
return -1;
}
int ret = read(fd, buf, buf_len);
if (ret == -1) {
debug("syz_fuse_handle_req > read failed: %d\n", errno);
return -1;
}
// Safe to do because ret > 0 (!= -1) and < FUSE_MIN_READ_BUFFER (= 8192).
if ((size_t)ret < sizeof(struct fuse_in_header)) {
debug("syz_fuse_handle_req: received a truncated FUSE header\n");
return -1;
}
const struct fuse_in_header* in_hdr = (const struct fuse_in_header*)buf;
debug("syz_fuse_handle_req: received opcode %d\n", in_hdr->opcode);
if (in_hdr->len > (uint32)ret) {
debug("syz_fuse_handle_req: received a truncated message\n");
return -1;
}
switch (in_hdr->opcode) {
case FUSE_GETATTR:
case FUSE_SETATTR:
out_hdr = req_out->attr;
break;
case FUSE_LOOKUP:
case FUSE_SYMLINK:
case FUSE_LINK:
case FUSE_MKNOD:
case FUSE_MKDIR:
out_hdr = req_out->entry;
break;
case FUSE_OPEN:
case FUSE_OPENDIR:
out_hdr = req_out->open;
break;
case FUSE_STATFS:
out_hdr = req_out->statfs;
break;
case FUSE_RMDIR:
case FUSE_RENAME:
case FUSE_RENAME2:
case FUSE_FALLOCATE:
case FUSE_SETXATTR:
case FUSE_REMOVEXATTR:
case FUSE_FSYNCDIR:
case FUSE_FSYNC:
case FUSE_SETLKW:
case FUSE_SETLK:
case FUSE_ACCESS:
case FUSE_FLUSH:
case FUSE_RELEASE:
case FUSE_RELEASEDIR:
// These opcodes do not have any reply data. Hence, we pick
// another response and only use the shared header.
out_hdr = req_out->init;
if (!out_hdr) {
debug("syz_fuse_handle_req: received a NULL out_hdr\n");
return -1;
}
out_hdr->len = sizeof(struct fuse_out_header);
break;
case FUSE_READ:
out_hdr = req_out->read;
break;
case FUSE_READDIR:
out_hdr = req_out->dirent;
break;
case FUSE_READDIRPLUS:
out_hdr = req_out->direntplus;
break;
case FUSE_INIT:
out_hdr = req_out->init;
break;
case FUSE_LSEEK:
out_hdr = req_out->lseek;
break;
case FUSE_GETLK:
out_hdr = req_out->lk;
break;
case FUSE_BMAP:
out_hdr = req_out->bmap;
break;
case FUSE_POLL:
out_hdr = req_out->poll;
break;
case FUSE_GETXATTR:
case FUSE_LISTXATTR:
out_hdr = req_out->getxattr;
break;
case FUSE_WRITE:
out_hdr = req_out->write;
break;
case FUSE_FORGET:
// FUSE_FORGET expects no reply.
return 0;
case FUSE_CREATE:
out_hdr = req_out->create_open;
break;
case FUSE_IOCTL:
out_hdr = req_out->ioctl;
break;
default:
debug("syz_fuse_handle_req: unknown FUSE opcode\n");
return -1;
}
return fuse_send_response(fd, in_hdr, out_hdr);
}
#endif

View File

@ -8926,6 +8926,174 @@ static void setup_usb()
#define CAST(f) ({void* p = (void*)f; p; })
#endif
#if SYZ_EXECUTOR || __NR_syz_fuse_handle_req
#include <fcntl.h>
#include <linux/fuse.h>
#include <stddef.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
struct syz_fuse_req_out {
struct fuse_out_header* init;
struct fuse_out_header* lseek;
struct fuse_out_header* bmap;
struct fuse_out_header* poll;
struct fuse_out_header* getxattr;
struct fuse_out_header* lk;
struct fuse_out_header* statfs;
struct fuse_out_header* write;
struct fuse_out_header* read;
struct fuse_out_header* open;
struct fuse_out_header* attr;
struct fuse_out_header* entry;
struct fuse_out_header* dirent;
struct fuse_out_header* direntplus;
struct fuse_out_header* create_open;
struct fuse_out_header* ioctl;
};
static int
fuse_send_response(int fd,
const struct fuse_in_header* in_hdr,
struct fuse_out_header* out_hdr)
{
if (!out_hdr) {
debug("fuse_send_response: received a NULL out_hdr\n");
return -1;
}
out_hdr->unique = in_hdr->unique;
if (write(fd, out_hdr, out_hdr->len) == -1) {
debug("fuse_send_response > write failed: %d\n", errno);
return -1;
}
return 0;
}
static volatile long syz_fuse_handle_req(volatile long a0,
volatile long a1,
volatile long a2,
volatile long a3)
{
struct syz_fuse_req_out* req_out = (struct syz_fuse_req_out*)a3;
struct fuse_out_header* out_hdr = NULL;
char* buf = (char*)a1;
int buf_len = (int)a2;
int fd = (int)a0;
if (!req_out) {
debug("syz_fuse_handle_req: received a NULL syz_fuse_req_out\n");
return -1;
}
if (buf_len < FUSE_MIN_READ_BUFFER) {
debug("FUSE requires the read buffer to be at least %u\n", FUSE_MIN_READ_BUFFER);
return -1;
}
int ret = read(fd, buf, buf_len);
if (ret == -1) {
debug("syz_fuse_handle_req > read failed: %d\n", errno);
return -1;
}
if ((size_t)ret < sizeof(struct fuse_in_header)) {
debug("syz_fuse_handle_req: received a truncated FUSE header\n");
return -1;
}
const struct fuse_in_header* in_hdr = (const struct fuse_in_header*)buf;
debug("syz_fuse_handle_req: received opcode %d\n", in_hdr->opcode);
if (in_hdr->len > (uint32)ret) {
debug("syz_fuse_handle_req: received a truncated message\n");
return -1;
}
switch (in_hdr->opcode) {
case FUSE_GETATTR:
case FUSE_SETATTR:
out_hdr = req_out->attr;
break;
case FUSE_LOOKUP:
case FUSE_SYMLINK:
case FUSE_LINK:
case FUSE_MKNOD:
case FUSE_MKDIR:
out_hdr = req_out->entry;
break;
case FUSE_OPEN:
case FUSE_OPENDIR:
out_hdr = req_out->open;
break;
case FUSE_STATFS:
out_hdr = req_out->statfs;
break;
case FUSE_RMDIR:
case FUSE_RENAME:
case FUSE_RENAME2:
case FUSE_FALLOCATE:
case FUSE_SETXATTR:
case FUSE_REMOVEXATTR:
case FUSE_FSYNCDIR:
case FUSE_FSYNC:
case FUSE_SETLKW:
case FUSE_SETLK:
case FUSE_ACCESS:
case FUSE_FLUSH:
case FUSE_RELEASE:
case FUSE_RELEASEDIR:
out_hdr = req_out->init;
if (!out_hdr) {
debug("syz_fuse_handle_req: received a NULL out_hdr\n");
return -1;
}
out_hdr->len = sizeof(struct fuse_out_header);
break;
case FUSE_READ:
out_hdr = req_out->read;
break;
case FUSE_READDIR:
out_hdr = req_out->dirent;
break;
case FUSE_READDIRPLUS:
out_hdr = req_out->direntplus;
break;
case FUSE_INIT:
out_hdr = req_out->init;
break;
case FUSE_LSEEK:
out_hdr = req_out->lseek;
break;
case FUSE_GETLK:
out_hdr = req_out->lk;
break;
case FUSE_BMAP:
out_hdr = req_out->bmap;
break;
case FUSE_POLL:
out_hdr = req_out->poll;
break;
case FUSE_GETXATTR:
case FUSE_LISTXATTR:
out_hdr = req_out->getxattr;
break;
case FUSE_WRITE:
out_hdr = req_out->write;
break;
case FUSE_FORGET:
return 0;
case FUSE_CREATE:
out_hdr = req_out->create_open;
break;
case FUSE_IOCTL:
out_hdr = req_out->ioctl;
break;
default:
debug("syz_fuse_handle_req: unknown FUSE opcode\n");
return -1;
}
return fuse_send_response(fd, in_hdr, out_hdr);
}
#endif
#elif GOOS_test
#include <stdlib.h>

View File

@ -250,6 +250,16 @@ func isBtfVmlinuxSupported(c *prog.Syscall, target *prog.Target, sandbox string)
return onlySandboxNone(sandbox)
}
func isSyzFuseSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
if ok, reason := isSupportedFilesystem("fuse"); !ok {
return ok, reason
}
if ok, reason := onlySandboxNoneOrNamespace(sandbox); !ok {
return false, reason
}
return true, ""
}
var syzkallSupport = map[string]func(*prog.Syscall, *prog.Target, string) (bool, string){
"syz_open_dev": isSyzOpenDevSupported,
"syz_open_procfs": alwaysSupported,
@ -274,8 +284,9 @@ var syzkallSupport = map[string]func(*prog.Syscall, *prog.Target, string) (bool,
"syz_io_uring_setup": isSyzIoUringSupported,
// syz_memcpy_off is only used for io_uring descriptions, thus, enable it
// only if io_uring syscalls are enabled.
"syz_memcpy_off": isSyzIoUringSupported,
"syz_btf_id_by_name": isBtfVmlinuxSupported,
"syz_memcpy_off": isSyzIoUringSupported,
"syz_btf_id_by_name": isBtfVmlinuxSupported,
"syz_fuse_handle_req": isSyzFuseSupported,
}
func isSupportedSyzkall(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {

View File

@ -41,6 +41,8 @@ write$FUSE_NOTIFY_STORE(fd fd_fuse, arg ptr[in, fuse_notify[FUSE_NOTIFY_STORE, f
write$FUSE_NOTIFY_RETRIEVE(fd fd_fuse, arg ptr[in, fuse_notify[FUSE_NOTIFY_RETRIEVE, fuse_notify_retrieve_out]], len bytesize[arg])
write$FUSE_NOTIFY_DELETE(fd fd_fuse, arg ptr[in, fuse_notify[FUSE_NOTIFY_DELETE, fuse_notify_delete_out]], len bytesize[arg])
syz_fuse_handle_req(fd fd_fuse, buf ptr[in, read_buffer], len bytesize[buf], res ptr[in, syz_fuse_req_out])
type fuse_ino int64[0:6]
type fuse_gen int64[0:3]
@ -62,13 +64,20 @@ type fuse_in[PAYLOAD] {
payload PAYLOAD
} [packed]
type fuse_out[PAYLOAD] {
type fuse_out_t[UNIQUE, PAYLOAD] {
len len[parent, int32]
err flags[fuse_errors, int32]
unique fuse_unique
unique UNIQUE
payload PAYLOAD
} [packed]
type fuse_out[PAYLOAD] fuse_out_t[fuse_unique, PAYLOAD]
# This response header is used by syz_fuse_handle_req(). It defines the FUSE unique
# identifier as int64 because syz_fuse_handle_req() retrieves it internally
# (defining it as a resource would create a dependency with read$FUSE() which is
# incorrect).
type syz_fuse_out[PAYLOAD] fuse_out_t[int64, PAYLOAD]
# -ENOENT, -EAGAIN, -ENOSYS
fuse_errors = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -11, -38
@ -154,6 +163,10 @@ fuse_write_out {
padding const[0, int32]
}
fuse_read_out {
content string
}
fuse_open_out {
fh const[0, int64]
open_flags flags[fuse_open_flags, int32]
@ -278,3 +291,23 @@ fuse_opts [
fuse_mode = S_IFREG, S_IFCHR, S_IFBLK, S_IFIFO, S_IFSOCK, S_IFLNK, S_IFDIR
fuse_block_sizes = 512, 1024, 2048, 4096
# Used by syz_fuse_handle_req() to mimic a FUSE daemon.
syz_fuse_req_out {
init ptr[in, syz_fuse_out[fuse_init_out]]
lseek ptr[in, syz_fuse_out[fuse_lseek_out]]
bmap ptr[in, syz_fuse_out[fuse_bmap_out]]
poll ptr[in, syz_fuse_out[fuse_poll_out]]
getxattr ptr[in, syz_fuse_out[fuse_getxattr_out]]
lk ptr[in, syz_fuse_out[fuse_lk_out]]
statfs ptr[in, syz_fuse_out[fuse_statfs_out]]
write ptr[in, syz_fuse_out[fuse_write_out]]
read ptr[in, syz_fuse_out[fuse_read_out]]
open ptr[in, syz_fuse_out[fuse_open_out]]
attr ptr[in, syz_fuse_out[fuse_attr_out]]
entry ptr[in, syz_fuse_out[fuse_entry_out]]
dirent ptr[in, syz_fuse_out[array[fuse_dirent]]]
direntplus ptr[in, syz_fuse_out[array[fuse_direntplus]]]
create_open ptr[in, syz_fuse_out[fuse_create_open_out]]
ioctl ptr[in, syz_fuse_out[fuse_ioctl_out]]
}

View File

@ -0,0 +1,8 @@
mkdirat(0xffffffffffffff9c, &AUTO='./file0\x00', 0x0)
r0 = openat$fuse(0xffffffffffffff9c, &AUTO='/dev/fuse\x00', 0x2, 0x0)
mount$fuse(0x0, &AUTO='./file0\x00', &AUTO='fuse\x00', 0x0, &AUTO={{'fd', 0x3d, r0}, 0x2c, {'rootmode', 0x3d, 0x4000}, 0x2c, {'user_id', 0x3d, 0x0}, 0x2c, {'group_id', 0x3d, 0x0}, 0x2c, {[], [], 0x0}})
r1 = openat$dir(0xffffffffffffff9c, &AUTO='./file0\x00', 0x0, 0x0)
# FUSE_INIT
syz_fuse_handle_req(r0, &AUTO=""/8192, AUTO, &AUTO={&AUTO={AUTO, 0x0, 0x0, {AUTO, AUTO, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, AUTO, AUTO, [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]}}, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0})
# FUSE_OPENDIR
syz_fuse_handle_req(r0, &AUTO=""/8192, AUTO, &AUTO={0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, &AUTO={AUTO, 0x0, 0x0, {0x0, 0x0, 0x0}}, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0})