| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Copyright 2021 Google LLC |
| */ |
| |
| #include "test_fuse.h" |
| |
| #include <dirent.h> |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <gelf.h> |
| #include <libelf.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <unistd.h> |
| |
| #include <sys/mount.h> |
| #include <sys/stat.h> |
| #include <sys/statfs.h> |
| #include <sys/xattr.h> |
| |
| #include <linux/unistd.h> |
| |
| #include <include/uapi/linux/fuse.h> |
| #include <include/uapi/linux/bpf.h> |
| |
| struct _test_options test_options; |
| |
| struct s s(const char *s1) |
| { |
| struct s s = {0}; |
| |
| if (!s1) |
| return s; |
| |
| s.s = malloc(strlen(s1) + 1); |
| if (!s.s) |
| return s; |
| |
| strcpy(s.s, s1); |
| return s; |
| } |
| |
| struct s sn(const char *s1, const char *s2) |
| { |
| struct s s = {0}; |
| |
| if (!s1) |
| return s; |
| |
| s.s = malloc(s2 - s1 + 1); |
| if (!s.s) |
| return s; |
| |
| strncpy(s.s, s1, s2 - s1); |
| s.s[s2 - s1] = 0; |
| return s; |
| } |
| |
| int s_cmp(struct s s1, struct s s2) |
| { |
| int result = -1; |
| |
| if (!s1.s || !s2.s) |
| goto out; |
| result = strcmp(s1.s, s2.s); |
| out: |
| free(s1.s); |
| free(s2.s); |
| return result; |
| } |
| |
| struct s s_cat(struct s s1, struct s s2) |
| { |
| struct s s = {0}; |
| |
| if (!s1.s || !s2.s) |
| goto out; |
| |
| s.s = malloc(strlen(s1.s) + strlen(s2.s) + 1); |
| if (!s.s) |
| goto out; |
| |
| strcpy(s.s, s1.s); |
| strcat(s.s, s2.s); |
| out: |
| free(s1.s); |
| free(s2.s); |
| return s; |
| } |
| |
| struct s s_splitleft(struct s s1, char c) |
| { |
| struct s s = {0}; |
| char *split; |
| |
| if (!s1.s) |
| return s; |
| |
| split = strchr(s1.s, c); |
| if (split) |
| s = sn(s1.s, split); |
| |
| free(s1.s); |
| return s; |
| } |
| |
| struct s s_splitright(struct s s1, char c) |
| { |
| struct s s2 = {0}; |
| char *split; |
| |
| if (!s1.s) |
| return s2; |
| |
| split = strchr(s1.s, c); |
| if (split) |
| s2 = s(split + 1); |
| |
| free(s1.s); |
| return s2; |
| } |
| |
| struct s s_word(struct s s1, char c, size_t n) |
| { |
| while (n--) |
| s1 = s_splitright(s1, c); |
| return s_splitleft(s1, c); |
| } |
| |
| struct s s_path(struct s s1, struct s s2) |
| { |
| return s_cat(s_cat(s1, s("/")), s2); |
| } |
| |
| struct s s_pathn(size_t n, struct s s1, ...) |
| { |
| va_list argp; |
| |
| va_start(argp, s1); |
| while (--n) |
| s1 = s_path(s1, va_arg(argp, struct s)); |
| va_end(argp); |
| return s1; |
| } |
| |
| int s_link(struct s src_pathname, struct s dst_pathname) |
| { |
| int res; |
| |
| if (src_pathname.s && dst_pathname.s) { |
| res = link(src_pathname.s, dst_pathname.s); |
| } else { |
| res = -1; |
| errno = ENOMEM; |
| } |
| |
| free(src_pathname.s); |
| free(dst_pathname.s); |
| return res; |
| } |
| |
| int s_symlink(struct s src_pathname, struct s dst_pathname) |
| { |
| int res; |
| |
| if (src_pathname.s && dst_pathname.s) { |
| res = symlink(src_pathname.s, dst_pathname.s); |
| } else { |
| res = -1; |
| errno = ENOMEM; |
| } |
| |
| free(src_pathname.s); |
| free(dst_pathname.s); |
| return res; |
| } |
| |
| |
| int s_mkdir(struct s pathname, mode_t mode) |
| { |
| int res; |
| |
| if (!pathname.s) { |
| errno = ENOMEM; |
| return -1; |
| } |
| |
| res = mkdir(pathname.s, mode); |
| free(pathname.s); |
| return res; |
| } |
| |
| int s_rmdir(struct s pathname) |
| { |
| int res; |
| |
| if (!pathname.s) { |
| errno = ENOMEM; |
| return -1; |
| } |
| |
| res = rmdir(pathname.s); |
| free(pathname.s); |
| return res; |
| } |
| |
| int s_unlink(struct s pathname) |
| { |
| int res; |
| |
| if (!pathname.s) { |
| errno = ENOMEM; |
| return -1; |
| } |
| |
| res = unlink(pathname.s); |
| free(pathname.s); |
| return res; |
| } |
| |
| int s_open(struct s pathname, int flags, ...) |
| { |
| va_list ap; |
| int res; |
| |
| va_start(ap, flags); |
| if (!pathname.s) { |
| errno = ENOMEM; |
| return -1; |
| } |
| |
| if (flags & (O_CREAT | O_TMPFILE)) |
| res = open(pathname.s, flags, va_arg(ap, mode_t)); |
| else |
| res = open(pathname.s, flags); |
| |
| free(pathname.s); |
| va_end(ap); |
| return res; |
| } |
| |
| int s_openat(int dirfd, struct s pathname, int flags, ...) |
| { |
| va_list ap; |
| int res; |
| |
| va_start(ap, flags); |
| if (!pathname.s) { |
| errno = ENOMEM; |
| return -1; |
| } |
| |
| if (flags & (O_CREAT | O_TMPFILE)) |
| res = openat(dirfd, pathname.s, flags, va_arg(ap, mode_t)); |
| else |
| res = openat(dirfd, pathname.s, flags); |
| |
| free(pathname.s); |
| va_end(ap); |
| return res; |
| } |
| |
| int s_creat(struct s pathname, mode_t mode) |
| { |
| int res; |
| |
| if (!pathname.s) { |
| errno = ENOMEM; |
| return -1; |
| } |
| |
| res = open(pathname.s, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, mode); |
| free(pathname.s); |
| return res; |
| } |
| |
| int s_mkfifo(struct s pathname, mode_t mode) |
| { |
| int res; |
| |
| if (!pathname.s) { |
| errno = ENOMEM; |
| return -1; |
| } |
| |
| res = mknod(pathname.s, S_IFIFO | mode, 0); |
| free(pathname.s); |
| return res; |
| } |
| |
| int s_stat(struct s pathname, struct stat *st) |
| { |
| int res; |
| |
| if (!pathname.s) { |
| errno = ENOMEM; |
| return -1; |
| } |
| |
| res = stat(pathname.s, st); |
| free(pathname.s); |
| return res; |
| } |
| |
| int s_statfs(struct s pathname, struct statfs *st) |
| { |
| int res; |
| |
| if (!pathname.s) { |
| errno = ENOMEM; |
| return -1; |
| } |
| |
| res = statfs(pathname.s, st); |
| free(pathname.s); |
| return res; |
| } |
| |
| DIR *s_opendir(struct s pathname) |
| { |
| DIR *res; |
| |
| res = opendir(pathname.s); |
| free(pathname.s); |
| return res; |
| } |
| |
| int s_getxattr(struct s pathname, const char name[], void *value, size_t size, |
| ssize_t *ret_size) |
| { |
| if (!pathname.s) { |
| errno = ENOMEM; |
| return -1; |
| } |
| |
| *ret_size = getxattr(pathname.s, name, value, size); |
| free(pathname.s); |
| return *ret_size >= 0 ? 0 : -1; |
| } |
| |
| int s_listxattr(struct s pathname, void *list, size_t size, ssize_t *ret_size) |
| { |
| if (!pathname.s) { |
| errno = ENOMEM; |
| return -1; |
| } |
| |
| *ret_size = listxattr(pathname.s, list, size); |
| free(pathname.s); |
| return *ret_size >= 0 ? 0 : -1; |
| } |
| |
| int s_setxattr(struct s pathname, const char name[], const void *value, size_t size, int flags) |
| { |
| int res; |
| |
| if (!pathname.s) { |
| errno = ENOMEM; |
| return -1; |
| } |
| |
| res = setxattr(pathname.s, name, value, size, flags); |
| free(pathname.s); |
| return res; |
| } |
| |
| int s_removexattr(struct s pathname, const char name[]) |
| { |
| int res; |
| |
| if (!pathname.s) { |
| errno = ENOMEM; |
| return -1; |
| } |
| |
| res = removexattr(pathname.s, name); |
| free(pathname.s); |
| return res; |
| } |
| |
| int s_rename(struct s oldpathname, struct s newpathname) |
| { |
| int res; |
| |
| if (!oldpathname.s || !newpathname.s) { |
| errno = ENOMEM; |
| return -1; |
| } |
| |
| res = rename(oldpathname.s, newpathname.s); |
| free(oldpathname.s); |
| free(newpathname.s); |
| return res; |
| } |
| |
| int s_fuse_attr(struct s pathname, struct fuse_attr *fuse_attr_out) |
| { |
| |
| struct stat st; |
| int result = TEST_FAILURE; |
| |
| TESTSYSCALL(s_stat(pathname, &st)); |
| |
| fuse_attr_out->ino = st.st_ino; |
| fuse_attr_out->mode = st.st_mode; |
| fuse_attr_out->nlink = st.st_nlink; |
| fuse_attr_out->uid = st.st_uid; |
| fuse_attr_out->gid = st.st_gid; |
| fuse_attr_out->rdev = st.st_rdev; |
| fuse_attr_out->size = st.st_size; |
| fuse_attr_out->blksize = st.st_blksize; |
| fuse_attr_out->blocks = st.st_blocks; |
| fuse_attr_out->atime = st.st_atime; |
| fuse_attr_out->mtime = st.st_mtime; |
| fuse_attr_out->ctime = st.st_ctime; |
| fuse_attr_out->atimensec = UINT32_MAX; |
| fuse_attr_out->mtimensec = UINT32_MAX; |
| fuse_attr_out->ctimensec = UINT32_MAX; |
| |
| result = TEST_SUCCESS; |
| out: |
| return result; |
| } |
| |
| struct s tracing_folder(void) |
| { |
| struct s trace = {0}; |
| FILE *mounts = NULL; |
| char *line = NULL; |
| size_t size = 0; |
| |
| TEST(mounts = fopen("/proc/mounts", "re"), mounts); |
| while (getline(&line, &size, mounts) != -1) { |
| if (!s_cmp(s_word(sn(line, line + size), ' ', 2), |
| s("tracefs"))) { |
| trace = s_word(sn(line, line + size), ' ', 1); |
| break; |
| } |
| |
| if (!s_cmp(s_word(sn(line, line + size), ' ', 2), s("debugfs"))) |
| trace = s_path(s_word(sn(line, line + size), ' ', 1), |
| s("tracing")); |
| } |
| |
| out: |
| free(line); |
| fclose(mounts); |
| return trace; |
| } |
| |
| int tracing_on(void) |
| { |
| int result = TEST_FAILURE; |
| int tracing_on = -1; |
| |
| TEST(tracing_on = s_open(s_path(tracing_folder(), s("tracing_on")), |
| O_WRONLY | O_CLOEXEC), |
| tracing_on != -1); |
| TESTEQUAL(write(tracing_on, "1", 1), 1); |
| result = TEST_SUCCESS; |
| out: |
| close(tracing_on); |
| return result; |
| } |
| |
| char *concat_file_name(const char *dir, const char *file) |
| { |
| char full_name[FILENAME_MAX] = ""; |
| |
| if (snprintf(full_name, ARRAY_SIZE(full_name), "%s/%s", dir, file) < 0) |
| return NULL; |
| return strdup(full_name); |
| } |
| |
| char *setup_mount_dir(const char *name) |
| { |
| struct stat st; |
| char *current_dir = getcwd(NULL, 0); |
| char *mount_dir = concat_file_name(current_dir, name); |
| |
| free(current_dir); |
| if (stat(mount_dir, &st) == 0) { |
| if (S_ISDIR(st.st_mode)) |
| return mount_dir; |
| |
| ksft_print_msg("%s is a file, not a dir.\n", mount_dir); |
| return NULL; |
| } |
| |
| if (mkdir(mount_dir, 0777)) { |
| ksft_print_msg("Can't create mount dir."); |
| return NULL; |
| } |
| |
| return mount_dir; |
| } |
| |
| int delete_dir_tree(const char *dir_path, bool remove_root) |
| { |
| DIR *dir = NULL; |
| struct dirent *dp; |
| int result = 0; |
| |
| dir = opendir(dir_path); |
| if (!dir) { |
| result = -errno; |
| goto out; |
| } |
| |
| while ((dp = readdir(dir))) { |
| char *full_path; |
| |
| if (!strcmp(dp->d_name, ".") || !strcmp(dp->d_name, "..")) |
| continue; |
| |
| full_path = concat_file_name(dir_path, dp->d_name); |
| if (dp->d_type == DT_DIR) |
| result = delete_dir_tree(full_path, true); |
| else |
| result = unlink(full_path); |
| free(full_path); |
| if (result) |
| goto out; |
| } |
| |
| out: |
| if (dir) |
| closedir(dir); |
| if (!result && remove_root) |
| rmdir(dir_path); |
| return result; |
| } |
| |
| static int mount_fuse_maybe_init(const char *mount_dir, int bpf_fd, int dir_fd, |
| int *fuse_dev_ptr, bool init) |
| { |
| int result = TEST_FAILURE; |
| int fuse_dev = -1; |
| char options[FILENAME_MAX]; |
| uint8_t bytes_in[FUSE_MIN_READ_BUFFER]; |
| uint8_t bytes_out[FUSE_MIN_READ_BUFFER]; |
| |
| DECL_FUSE_IN(init); |
| |
| TEST(fuse_dev = open("/dev/fuse", O_RDWR | O_CLOEXEC), fuse_dev != -1); |
| snprintf(options, FILENAME_MAX, "fd=%d,user_id=0,group_id=0,rootmode=0040000", |
| fuse_dev); |
| if (bpf_fd != -1) |
| snprintf(options + strlen(options), |
| sizeof(options) - strlen(options), |
| ",root_bpf=%d", bpf_fd); |
| if (dir_fd != -1) |
| snprintf(options + strlen(options), |
| sizeof(options) - strlen(options), |
| ",root_dir=%d", dir_fd); |
| TESTSYSCALL(mount("ABC", mount_dir, "fuse", 0, options)); |
| |
| if (init) { |
| TESTFUSEIN(FUSE_INIT, init_in); |
| TESTEQUAL(init_in->major, FUSE_KERNEL_VERSION); |
| TESTEQUAL(init_in->minor, FUSE_KERNEL_MINOR_VERSION); |
| TESTFUSEOUT1(fuse_init_out, ((struct fuse_init_out) { |
| .major = FUSE_KERNEL_VERSION, |
| .minor = FUSE_KERNEL_MINOR_VERSION, |
| .max_readahead = 4096, |
| .flags = 0, |
| .max_background = 0, |
| .congestion_threshold = 0, |
| .max_write = 4096, |
| .time_gran = 1000, |
| .max_pages = 12, |
| .map_alignment = 4096, |
| })); |
| } |
| |
| *fuse_dev_ptr = fuse_dev; |
| fuse_dev = -1; |
| result = TEST_SUCCESS; |
| out: |
| close(fuse_dev); |
| return result; |
| } |
| |
| int mount_fuse(const char *mount_dir, int bpf_fd, int dir_fd, int *fuse_dev_ptr) |
| { |
| return mount_fuse_maybe_init(mount_dir, bpf_fd, dir_fd, fuse_dev_ptr, |
| true); |
| } |
| |
| int mount_fuse_no_init(const char *mount_dir, int bpf_fd, int dir_fd, |
| int *fuse_dev_ptr) |
| { |
| return mount_fuse_maybe_init(mount_dir, bpf_fd, dir_fd, fuse_dev_ptr, |
| false); |
| } |
| |
| struct fuse_bpf_map { |
| unsigned int map_type; |
| size_t key_size; |
| size_t value_size; |
| unsigned int max_entries; |
| }; |
| |
| static int install_maps(Elf_Data *maps, int maps_index, Elf *elf, |
| Elf_Data *symbols, int symbol_index, |
| struct map_relocation **mr, size_t *map_count) |
| { |
| int result = TEST_FAILURE; |
| int i; |
| GElf_Sym symbol; |
| |
| TESTNE((void *)symbols, NULL); |
| |
| for (i = 0; i < symbols->d_size / sizeof(symbol); ++i) { |
| TESTNE((void *)gelf_getsym(symbols, i, &symbol), 0); |
| if (symbol.st_shndx == maps_index) { |
| struct fuse_bpf_map *map; |
| union bpf_attr attr; |
| int map_fd; |
| |
| map = (struct fuse_bpf_map *) |
| ((char *)maps->d_buf + symbol.st_value); |
| |
| attr = (union bpf_attr) { |
| .map_type = map->map_type, |
| .key_size = map->key_size, |
| .value_size = map->value_size, |
| .max_entries = map->max_entries, |
| }; |
| |
| TEST(*mr = realloc(*mr, ++*map_count * |
| sizeof(struct fuse_bpf_map)), |
| *mr); |
| TEST(map_fd = syscall(__NR_bpf, BPF_MAP_CREATE, |
| &attr, sizeof(attr)), |
| map_fd != -1); |
| (*mr)[*map_count - 1] = (struct map_relocation) { |
| .name = strdup(elf_strptr(elf, symbol_index, |
| symbol.st_name)), |
| .fd = map_fd, |
| .value = symbol.st_value, |
| }; |
| } |
| } |
| |
| result = TEST_SUCCESS; |
| out: |
| return result; |
| } |
| |
| static inline int relocate_maps(GElf_Shdr *rel_header, Elf_Data *rel_data, |
| Elf_Data *prog_data, Elf_Data *symbol_data, |
| struct map_relocation *map_relocations, |
| size_t map_count) |
| { |
| int result = TEST_FAILURE; |
| int i; |
| struct bpf_insn *insns = (struct bpf_insn *) prog_data->d_buf; |
| |
| for (i = 0; i < rel_header->sh_size / rel_header->sh_entsize; ++i) { |
| GElf_Sym sym; |
| GElf_Rel rel; |
| unsigned int insn_idx; |
| int map_idx; |
| |
| gelf_getrel(rel_data, i, &rel); |
| insn_idx = rel.r_offset / sizeof(struct bpf_insn); |
| insns[insn_idx].src_reg = BPF_PSEUDO_MAP_FD; |
| |
| gelf_getsym(symbol_data, GELF_R_SYM(rel.r_info), &sym); |
| for (map_idx = 0; map_idx < map_count; map_idx++) { |
| if (map_relocations[map_idx].value == sym.st_value) { |
| insns[insn_idx].imm = |
| map_relocations[map_idx].fd; |
| break; |
| } |
| } |
| TESTNE(map_idx, map_count); |
| } |
| |
| result = TEST_SUCCESS; |
| out: |
| return result; |
| } |
| |
| int install_elf_bpf(const char *file, const char *section, int *fd, |
| struct map_relocation **map_relocations, size_t *map_count) |
| { |
| int result = TEST_FAILURE; |
| char path[PATH_MAX] = {}; |
| char *last_slash; |
| int filter_fd = -1; |
| union bpf_attr bpf_attr; |
| static char log[1 << 20]; |
| Elf *elf = NULL; |
| GElf_Ehdr ehdr; |
| Elf_Data *data_prog = NULL, *data_maps = NULL, *data_symbols = NULL; |
| int maps_index, symbol_index, prog_index; |
| int i; |
| |
| TESTNE(readlink("/proc/self/exe", path, PATH_MAX), -1); |
| TEST(last_slash = strrchr(path, '/'), last_slash); |
| strcpy(last_slash + 1, file); |
| TEST(filter_fd = open(path, O_RDONLY | O_CLOEXEC), filter_fd != -1); |
| TESTNE(elf_version(EV_CURRENT), EV_NONE); |
| TEST(elf = elf_begin(filter_fd, ELF_C_READ, NULL), elf); |
| TESTEQUAL((void *) gelf_getehdr(elf, &ehdr), &ehdr); |
| for (i = 1; i < ehdr.e_shnum; i++) { |
| char *shname; |
| GElf_Shdr shdr; |
| Elf_Scn *scn; |
| |
| TEST(scn = elf_getscn(elf, i), scn); |
| TESTEQUAL((void *)gelf_getshdr(scn, &shdr), &shdr); |
| TEST(shname = elf_strptr(elf, ehdr.e_shstrndx, shdr.sh_name), |
| shname); |
| |
| if (!strcmp(shname, "maps")) { |
| TEST(data_maps = elf_getdata(scn, 0), data_maps); |
| maps_index = i; |
| } else if (shdr.sh_type == SHT_SYMTAB) { |
| TEST(data_symbols = elf_getdata(scn, 0), data_symbols); |
| symbol_index = shdr.sh_link; |
| } else if (!strcmp(shname, section)) { |
| TEST(data_prog = elf_getdata(scn, 0), data_prog); |
| prog_index = i; |
| } |
| } |
| TESTNE((void *) data_prog, NULL); |
| |
| if (data_maps) |
| TESTEQUAL(install_maps(data_maps, maps_index, elf, |
| data_symbols, symbol_index, |
| map_relocations, map_count), 0); |
| |
| /* Now relocate maps */ |
| for (i = 1; i < ehdr.e_shnum; i++) { |
| GElf_Shdr rel_header; |
| Elf_Scn *scn; |
| Elf_Data *rel_data; |
| |
| TEST(scn = elf_getscn(elf, i), scn); |
| TESTEQUAL((void *)gelf_getshdr(scn, &rel_header), |
| &rel_header); |
| if (rel_header.sh_type != SHT_REL) |
| continue; |
| TEST(rel_data = elf_getdata(scn, 0), rel_data); |
| |
| if (rel_header.sh_info != prog_index) |
| continue; |
| TESTEQUAL(relocate_maps(&rel_header, rel_data, |
| data_prog, data_symbols, |
| *map_relocations, *map_count), |
| 0); |
| } |
| |
| bpf_attr = (union bpf_attr) { |
| .prog_type = BPF_PROG_TYPE_FUSE, |
| .insn_cnt = data_prog->d_size / 8, |
| .insns = ptr_to_u64(data_prog->d_buf), |
| .license = ptr_to_u64("GPL"), |
| .log_buf = test_options.verbose ? ptr_to_u64(log) : 0, |
| .log_size = test_options.verbose ? sizeof(log) : 0, |
| .log_level = test_options.verbose ? 2 : 0, |
| }; |
| *fd = syscall(__NR_bpf, BPF_PROG_LOAD, &bpf_attr, sizeof(bpf_attr)); |
| if (test_options.verbose) |
| ksft_print_msg("%s\n", log); |
| if (*fd == -1 && errno == ENOSPC) |
| ksft_print_msg("bpf log size too small!\n"); |
| TESTNE(*fd, -1); |
| |
| result = TEST_SUCCESS; |
| out: |
| close(filter_fd); |
| return result; |
| } |
| |
| |