loongbedrocklinux-userlandd/src/etcfs/etcfs.c

2221 lines
51 KiB
C

/*
* etcfs.c
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* version 2 as published by the Free Software Foundation.
*
* Copyright (c) 2013-2020 Daniel Thau <danthau@bedrocklinux.org>
*
* This program will pass filesystem requests through to either the global
* stratum's instance of a file, or the calling process' local stratum instance
* of a file. It may also modify files as needed to enforce specific file
* content, such as configuration file values.
*/
#define FUSE_USE_VERSION 32
#define _GNU_SOURCE
#include <dirent.h>
#include <errno.h>
#include <fuse3/fuse.h>
#include <fuse3/fuse_lowlevel.h>
#include <libgen.h>
#include <linux/limits.h>
#include <pthread.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <unistd.h>
#define ARRAY_LEN(x) (sizeof(x) / sizeof(x[0]))
#define MIN(x, y) (x < y ? x : y)
#define STRATUM_XATTR "user.bedrock.stratum"
#define STRATUM_XATTR_LEN strlen(STRATUM_XATTR)
#define LPATH_XATTR "user.bedrock.localpath"
#define LPATH_XATTR_LEN strlen(LPATH_XATTR)
#define CFG_NAME ".bedrock-config-filesystem"
#define CFG_NAME_LEN strlen(CFG_NAME)
#define global_root "/proc/1/root/bedrock/strata/bedrock"
#define GLOBAL_STRATUM "global"
#define ROOTDIR "/"
#define CMD_ADD_GLOBAL "add_global"
#define CMD_ADD_GLOBAL_LEN strlen(CMD_ADD_GLOBAL)
#define CMD_RM_GLOBAL "rm_global"
#define CMD_RM_GLOBAL_LEN strlen(CMD_RM_GLOBAL)
#define CMD_ADD_OVERRIDE "add_override"
#define CMD_ADD_OVERRIDE_LEN strlen(CMD_ADD_OVERRIDE)
#define CMD_RM_OVERRIDE "rm_override"
#define CMD_RM_OVERRIDE_LEN strlen(CMD_RM_OVERRIDE)
#define ATOMIC_UPDATE_SUFFIX "-bedrock-backup"
#define ATOMIC_UPDATE_SUFFIX_LEN strlen(ATOMIC_UPDATE_SUFFIX)
/*
* Various permissions related POSIX functions are per-process, not per thread.
* The underlying Linux filesystem calls, however, are per-thread. We can
* bypass libc and directly talk to the kernel in order to set the desired
* properties per thread.
*/
#ifdef SYS_setreuid32
#define SET_THREAD_EUID(euid) syscall(SYS_setreuid32, -1, euid)
#elif defined SYS_setreuid
#define SET_THREAD_EUID(euid) syscall(SYS_setreuid, -1, euid)
#else
#error "SYS_setreuid unavailable on this system"
#endif
#ifdef SYS_setregid32
#define SET_THREAD_EGID(egid) syscall(SYS_setregid32, -1, egid)
#elif defined SYS_setregid
#define SET_THREAD_EGID(egid) syscall(SYS_setregid, -1, egid)
#else
#error "SYS_setregid unavailable on this system"
#endif
#ifdef SYS_setgroups32
#define SET_THREAD_GROUPS(size, gids) syscall(SYS_setgroups32, size, gids)
#elif defined SYS_setgroups
#define SET_THREAD_GROUPS(size, gids) syscall(SYS_setgroups, size, gids)
#else
#error "SYS_setgroups unavailable on this system"
#endif
/*
* There does not appear to be a libc wrapper for gettid.
*/
#ifdef SYS_gettid
#define GETTID() syscall(SYS_gettid)
#else
#error "SYS_gettid unavailable on this system"
#endif
/*
* The bulk of this program consists of filesystem calls which share a fair bit
* of structure.
*/
#define DEBUG(name, arg) \
if (debug) { \
print_debug(name, arg); \
}
/*
* Set up permissions, rpath/ref_fd, override, and lock.
*
* Assumes path is populated. If it is null, error out.
*/
#define FS_IMP_SETUP(path) \
if (path == NULL) { \
return -EINVAL; \
} \
if (SET_THREAD_EUID(0) < 0) { \
return -EPERM; \
} \
int ref_fd = get_ref_fd(path); \
if (ref_fd < 0) { \
return -EDOM; \
} \
const char *const rpath = (path && path[1]) ? path + 1 : "."; \
if (apply_override(ref_fd, path, rpath) < 0) { \
return -ERANGE; \
} \
if (set_caller_permissions() < 0) { \
return -EPERM; \
} \
pthread_rwlock_rdlock(&cfg_lock); \
int rv;
/*
* Set up permissions and lock.
*
* If operating on CFG_NAME, no actual operation can be done. Early exit with
* default.
*/
#define FS_IMP_SETUP_FD(fd, path, default) \
(void)fd; \
if (strcmp(path+1, CFG_NAME) == 0) { \
return default; \
} \
if (SET_THREAD_EUID(0) < 0) { \
return -EPERM; \
} \
if (set_caller_permissions() < 0) { \
return -EPERM; \
} \
pthread_rwlock_rdlock(&cfg_lock); \
int rv;
/*
* Unlock and translate rv/errno from UNIX format to FUSE format.
*/
#define FS_IMP_RETURN(rv) \
pthread_rwlock_unlock(&cfg_lock); \
return rv >= 0 ? rv : -errno
/*
* Operation is disallowed on CFG_NAME. Error out if requested.
*/
#define DISALLOW_ON_CFG(rpath) \
if (strcmp(rpath, CFG_NAME) == 0) { \
errno = EINVAL; \
FS_IMP_RETURN(-1); \
}
/*
* Override type.
*/
enum o_type {
TYPE_SYMLINK,
TYPE_DIRECTORY,
TYPE_INJECT,
};
const char *const o_type_str[] = {
"symlink",
"directory",
"inject",
};
/*
* Override the underlying contents at a given file path with something else.
*/
struct override {
/*
* The filepath to overwrite.
*/
char *path;
/*
* The type of overwrite operation desired.
*/
enum o_type type;
/*
* The new content at the file path.
*/
char *content;
size_t content_len;
/*
* If the type is TYPE_INJECT, the content to inject.
*/
char *inject;
size_t inject_len;
/*
* The last timestamp the override was last (re)applied.
*/
time_t last_override;
};
/*
* File descriptors to which into filesystem calls will be directed
*/
/*
* File descriptors referring to directories.
*/
int global_ref_fd = -1;
int local_ref_fd = -1;
/*
* Paths which should be global.
*/
char **globals = NULL;
size_t global_cnt = 0;
size_t global_alloc = 0;
/*
* Overrides
*/
struct override *overrides = NULL;
size_t override_cnt = 0;
size_t override_alloc = 0;
/*
* Config file's stat information
*/
struct stat cfg_stat;
/*
* The path onto which this filesystem is mounted.
*/
static char *mntpt = NULL;
/*
* Local stratum name
*/
char local_name[PATH_MAX];
/*
* Lock around configuration access. The vast majority of config access is
* non-conflicting read-only, and thus a read-write lock is preferred.
*/
pthread_rwlock_t cfg_lock;
/*
* Tracks whether or not to enable debug output.
*/
int debug = 0;
/*
* Set the thread's euid, egid, and grouplist to that of the
* process calling a given FUSE filesystem call.
*
* This should be called at the beginning of every implemented filesystem call
* before any internal filesystem calls are made.
*
* SET_THREAD_EUID(0) should be called before this function to ensure this
* function has adequate permissions to run.
*/
static inline int set_caller_permissions(void)
{
int rv;
/*
* Set group list. For performance, try on the stack before heap.
*/
gid_t list[64];
if ((rv = fuse_getgroups(ARRAY_LEN(list), list)) < 0) {
/*
* fuse_getgroups() is implemented by reading /proc/<pid>/.
* This can fail if the request is not being made by something
* with a /proc-visible pid such as by a kernel process, an
* internal libfuse call, or a process outside of the visible
* PID namespace.
*
* We need to support kernel requests and internal libfuse
* requests, and thus we cannot abort when fuse_getgroups()
* fails.
*
* In this situation, simply continue with an empty group list.
* This minimizes the possibility that someone finds a way to
* abuse fuse_getgroups() failing (we're providing no
* priviledges) while still allowing legitimate requests to
* succeed via UID=0.
*/
if ((rv = SET_THREAD_GROUPS(0, list)) < 0) {
return -errno;
}
} else if (rv <= (int)ARRAY_LEN(list)) {
if ((rv = SET_THREAD_GROUPS(rv, list)) < 0) {
return -errno;
}
} else {
gid_t *list_heap = malloc(rv * sizeof(gid_t));
if (list_heap == NULL) {
return -ENOMEM;
}
int rv_heap;
if ((rv_heap = fuse_getgroups(rv, list)) < 0) {
free(list_heap);
return rv;
}
if (rv_heap > rv) {
free(list_heap);
return -ENOMEM;
}
if ((rv = SET_THREAD_GROUPS(rv, list_heap)) < 0) {
free(list_heap);
return -errno;
}
free(list_heap);
}
/*
* Set euid and egid
*/
struct fuse_context *context = fuse_get_context();
if ((rv = SET_THREAD_EGID(context->gid)) < 0) {
return -errno;
}
if ((rv = SET_THREAD_EUID(context->uid)) < 0) {
return -errno;
}
return 0;
}
static inline void print_debug(const char *const name, const char *const arg)
{
struct fuse_context *context = fuse_get_context();
char path[PATH_MAX];
char exe[PATH_MAX];
ssize_t s = snprintf(path, sizeof(path), "/proc/%d/exe", context->pid);
if (s < 0 || s >= (int)sizeof(path)) {
strcpy(exe, "(unknown)");
} else if ((s = readlink(path, exe, sizeof(path) - 1)) < 0) {
strcpy(exe, "(unknown)");
} else {
exe[s] = '\0';
}
char cmdline[PATH_MAX];
int fd = -1;
s = snprintf(path, sizeof(path), "/proc/%d/cmdline", context->pid);
if (s < 0 || s >= (int)sizeof(path)) {
strcpy(cmdline, "(unknown)");
} else if ((fd = open(path, O_RDONLY)) < 0) {
strcpy(cmdline, "(unknown)");
} else if (read(fd, cmdline, sizeof(cmdline) - 1) < 0) {
strcpy(cmdline, "(unknown)");
}
if (fd >= 0) {
close(fd);
}
char out[PATH_MAX * 5];
/*
* Print debug in one atomic statement to avoid thread interleaving
* output.
*/
s = snprintf(out, sizeof(out), "etcfs: %s(\"%s\") called at time=%ld "
"by UID=%d PID=%d exe=\"%s\" cmdline=\"%s\"\n",
name, arg, time(NULL), context->uid, context->pid, exe, cmdline);
if (s < 0 || s >= (int)sizeof(out)) {
fprintf(stderr, "etcfs: (error printing debug)\n");
} else {
fprintf(stderr, "%s", out);
}
fflush(NULL);
}
static inline int get_ref_fd(const char *const path)
{
/*
* Check if file is global
*/
for (size_t i = 0; i < global_cnt; i++) {
if (strcmp(globals[i], path) == 0) {
return global_ref_fd;
}
}
return local_ref_fd;
}
/*
* Search for string in a file.
*
* To remain performant, minimizes number of read() calls for a given buffer
* size.
*
* Supports files and search strings that contain null bytes.
*/
int file_search(int fd, const char *str, const size_t len)
{
size_t buf_size = PATH_MAX;
if (len > buf_size) {
buf_size = len;
}
char buf[buf_size];
size_t off = 0;
size_t bytes_read;
while ((bytes_read = read(fd, buf + off, buf_size - off)) > 0) {
if (memmem(buf, off + bytes_read, str, len) != NULL) {
return 1;
}
memmove(buf, buf + off + bytes_read - (len - 1), len - 1);
off = len - 1;
}
return 0;
}
/*
* Linux fails to provide system calls which apply to file descriptors and
* directly (without following) on symlinks. For example, neither lgetxattr()
* nor fgetxattr() have both of these attributes.
*
* For a subset of cases where this is needed, we can use /proc as a work
* around to get a path dependent only on a file descriptor which can be fed
* into system calls that do not follow symlinks (e.g. lgetxattr).
*/
static inline int procpath(const int fd, char *buf, size_t size)
{
int s = snprintf(buf, size, "/proc/self/fd/%d", fd);
if (s < 0 || s >= (int)size) {
return -1;
}
return 0;
}
/*
* Ensure a given file path contains a specific string.
*/
static int inject(int ref_fd, const char *rpath, const char *inject, const size_t inject_len)
{
int rv = -1;
int fd = -1;
int tmp_fd = -1;
char tmp_file[PATH_MAX];
tmp_file[0] = '\0';
if ((fd = openat(ref_fd, rpath, O_NONBLOCK | O_RDONLY)) < 0) {
goto clean_up_and_return;
}
struct stat stbuf;
if (fstat(fd, &stbuf) < 0) {
goto clean_up_and_return;
}
size_t init_len = stbuf.st_size;
/*
* Some package managers sanity check a file was created before writing
* to it. Issues arise if we inject between those steps, and so only
* inject non-empty files.
*/
if (init_len == 0) {
DEBUG("skipping injection, empty file", rpath);
rv = 0;
goto clean_up_and_return;
}
/*
* If the file already contains the target contents, we can skip
* writing to disk.
*/
if (init_len >= inject_len && file_search(fd, inject, inject_len)) {
DEBUG("skipping injection, already injected", rpath);
rv = 0;
goto clean_up_and_return;
}
/*
* File lacks intended content. Add it atomically.
*/
DEBUG("injecting", rpath);
/*
* Create a temporary file
*/
int s = snprintf(tmp_file, sizeof(tmp_file), "%s%s", rpath,
ATOMIC_UPDATE_SUFFIX);
if (s < 0 || s >= (int)sizeof(tmp_file)) {
goto clean_up_and_return;
}
unlinkat(ref_fd, tmp_file, 0);
if ((tmp_fd = openat(ref_fd, tmp_file, O_NONBLOCK | O_CREAT | O_RDWR | O_NOFOLLOW, stbuf.st_mode)) < 0) {
goto clean_up_and_return;
}
/*
* Copy the original file into it
*/
ssize_t bytes_read;
ssize_t bytes_written;
char buf[PATH_MAX];
if (lseek(fd, 0, SEEK_SET) < 0) {
goto clean_up_and_return;
}
while ((bytes_read = read(fd, buf, sizeof(buf))) > 0) {
if ((bytes_written = write(tmp_fd, buf, bytes_read)) < 0) {
goto clean_up_and_return;
}
}
/*
* Append
*/
if (write(tmp_fd, inject, inject_len) < 0) {
goto clean_up_and_return;
}
/*
* Atomically rename over original
*/
if (renameat(ref_fd, tmp_file, ref_fd, rpath) < 0) {
goto clean_up_and_return;
}
rv = 0;
clean_up_and_return:
if (tmp_fd >= 0) {
close(tmp_fd);
if (tmp_file[0] != '\0') {
unlinkat(ref_fd, tmp_file, 0);
}
}
if (fd >= 0) {
close(fd);
}
return rv;
}
/*
* Remove up to one instance of a given string from a file.
*/
static int uninject(int ref_fd, const char *rpath, const char *inject, const size_t inject_len)
{
int rv = -1;
int fd = -1;
int tmp_fd = -1;
char tmp_file[PATH_MAX];
tmp_file[0] = '\0';
char buf[inject_len * 2];
if ((fd = openat(ref_fd, rpath, O_NONBLOCK | O_RDONLY)) < 0) {
goto clean_up_and_return;
}
struct stat stbuf;
if (fstat(fd, &stbuf) < 0) {
goto clean_up_and_return;
}
size_t init_len = stbuf.st_size;
/*
* If the file is too small to hold inject content, we know it's
* missing and can skip operation.
*/
if (init_len < inject_len) {
rv = 0;
goto clean_up_and_return;
}
/*
* Search for string in file
*/
ssize_t bytes_read;
ssize_t multiple = 0;
off_t offset = -1;
/* TODO: replace with file_search() */
while ((bytes_read = read(fd, buf, sizeof(buf))) > 0) {
char *start = strstr(buf, inject);
if (start) {
offset = (start - buf) + (multiple * inject_len);
break;
}
multiple++;
if (lseek(fd, inject_len * multiple, SEEK_SET) < 0) {
break;
}
}
if (offset < 0) {
rv = 0;
goto clean_up_and_return;
}
/*
* Found string in file. Remove it atomically.
*/
/*
* Create a temporary file
*/
int s = snprintf(tmp_file, sizeof(tmp_file), "%s%s", rpath,
ATOMIC_UPDATE_SUFFIX);
if (s < 0 || s >= (int)sizeof(tmp_file)) {
goto clean_up_and_return;
}
unlinkat(ref_fd, tmp_file, 0);
if ((tmp_fd = openat(ref_fd, tmp_file, O_NONBLOCK | O_CREAT | O_RDWR | O_NOFOLLOW, stbuf.st_mode)) < 0) {
goto clean_up_and_return;
}
/*
* Copy the file
*/
ssize_t bytes_written;
if (lseek(fd, 0, SEEK_SET) < 0) {
goto clean_up_and_return;
}
while ((bytes_read = read(fd, buf, sizeof(buf))) > 0) {
if ((bytes_written = write(tmp_fd, buf, bytes_read)) < 0) {
goto clean_up_and_return;
}
}
/*
* Truncate to target size
*/
if (ftruncate(tmp_fd, stbuf.st_size - inject_len) < 0) {
goto clean_up_and_return;
}
/*
* Shift post-match region over region we want to remove
*/
if (lseek(fd, offset + inject_len, SEEK_SET) < 0) {
goto clean_up_and_return;
}
if (lseek(tmp_fd, offset, SEEK_SET) < 0) {
goto clean_up_and_return;
}
while ((bytes_read = read(fd, buf, sizeof(buf))) > 0) {
if ((bytes_written = write(tmp_fd, buf, bytes_read)) < 0) {
goto clean_up_and_return;
}
}
/*
* Atomically rename over original
*/
if (renameat(ref_fd, tmp_file, ref_fd, rpath) < 0) {
goto clean_up_and_return;
}
rv = 0;
clean_up_and_return:
if (tmp_fd >= 0) {
close(tmp_fd);
if (tmp_file[0] != '\0') {
unlinkat(ref_fd, tmp_file, 0);
}
}
if (fd >= 0) {
close(fd);
}
return rv;
}
/*
* Requires root.
*/
static inline int apply_override(const int ref_fd, const char *const path, const char *const rpath)
{
/*
* Find override
*/
size_t i = 0;
for (; i < override_cnt; i++) {
if (strcmp(overrides[i].path, path) == 0) {
break;
}
}
/*
* No override, nothing to do
*/
if (i == override_cnt) {
return 0;
}
/*
* Enforce override
*/
int rv = 0;
char buf[PATH_MAX];
struct stat stbuf;
/*
* Do not re-apply the same override twice in close succession, as that
* can confuse some software.
*
* For example, xbps-install calls:
*
* unlink(path)
* openat(AT_FDCWD, path, ...O_CREAT...)
* unlink(path)
* openat(AT_FDCWD, path, ...O_CREAT...)
*
* then gives up after the second openat() fails due to
* the previously existing file.
*/
time_t now;
pthread_rwlock_wrlock(&cfg_lock);
switch (overrides[i].type) {
case TYPE_SYMLINK:
if (readlinkat(ref_fd, rpath, buf, sizeof(buf) - 1) >= 0 && strcmp(buf, overrides[i].content) == 0) {
break;
}
now = time(NULL);
if ((now - overrides[i].last_override) <= 1) {
break;
}
overrides[i].last_override = now;
unlinkat(ref_fd, rpath, 0);
unlinkat(ref_fd, rpath, AT_REMOVEDIR);
rv = symlinkat(overrides[i].content, ref_fd, rpath);
break;
case TYPE_DIRECTORY:
if (fstatat(ref_fd, rpath, &stbuf, AT_SYMLINK_NOFOLLOW) >= 0 && S_ISDIR(stbuf.st_mode)) {
break;
}
now = time(NULL);
if ((now - overrides[i].last_override) <= 1) {
break;
}
overrides[i].last_override = now;
unlinkat(ref_fd, rpath, 0);
unlinkat(ref_fd, rpath, AT_REMOVEDIR);
rv = mkdirat(ref_fd, rpath, 0755);
break;
case TYPE_INJECT:
if (fstatat(ref_fd, rpath, &stbuf, AT_SYMLINK_NOFOLLOW) < 0 || !S_ISREG(stbuf.st_mode)) {
break;
}
now = time(NULL);
if ((now - overrides[i].last_override) <= 1) {
break;
}
overrides[i].last_override = now;
rv = inject(ref_fd, rpath, overrides[i].inject, overrides[i].inject_len);
break;
}
pthread_rwlock_unlock(&cfg_lock);
return rv;
}
static int cfg_add_global(const char *const buf, size_t size)
{
/*
* Ensure there is a trailing null so that sscanf doesn't overflow if
* we get bad input.
*/
char nbuf[PIPE_BUF];
if (size > sizeof(nbuf) - 1) {
return -ENAMETOOLONG;
}
memcpy(nbuf, buf, size);
nbuf[size] = '\0';
/*
* Tokenize
*/
char buf_cmd[PIPE_BUF];
char space;
char buf_global[PIPE_BUF];
char newline;
if (sscanf(nbuf, "%s%c%s%c", buf_cmd, &space, buf_global, &newline) != 4) {
return -EINVAL;
}
/*
* Sanity check
*/
if (strcmp(buf_cmd, CMD_ADD_GLOBAL) != 0 || space != ' ' || newline != '\n' || strchr(buf_global, '/') == NULL) {
return -EINVAL;
}
/*
* Don't double add.
*/
for (size_t i = 0; i < global_cnt; i++) {
if (strcmp(globals[i], buf_global) == 0) {
return 0;
}
}
if (global_alloc < global_cnt + 1) {
char **new_globals = realloc(globals, (global_cnt + 1) * sizeof(char *));
if (new_globals == NULL) {
return -ENOMEM;
}
globals = new_globals;
global_alloc = global_cnt + 1;
}
char *global = malloc(strlen(buf_global) + 1);
if (global == NULL) {
return -ENOMEM;
}
strcpy(global, buf_global);
globals[global_cnt] = global;
global_cnt++;
cfg_stat.st_size += strlen("global ") + strlen(global) + strlen("\n");
return size;
}
static int cfg_rm_global(const char *const buf, size_t size)
{
/*
* Ensure there is a trailing null so that sscanf doesn't overflow if
* we get bad input.
*/
char nbuf[PIPE_BUF];
if (size > sizeof(nbuf) - 1) {
return -ENAMETOOLONG;
}
memcpy(nbuf, buf, size);
nbuf[size] = '\0';
/*
* Tokenize
*/
char buf_cmd[PIPE_BUF];
char space;
char buf_global[PIPE_BUF];
char newline;
if (sscanf(nbuf, "%s%c%s%c", buf_cmd, &space, buf_global, &newline) != 4) {
return -EINVAL;
}
/*
* Sanity check
*/
if (strcmp(buf_cmd, CMD_RM_GLOBAL) != 0 || space != ' ' || newline != '\n' || strchr(buf_global, '/') == NULL) {
return -EINVAL;
}
size_t i;
for (i = 0; i < global_cnt; i++) {
if (strcmp(globals[i], buf_global) == 0) {
break;
}
}
if (i == global_cnt) {
return size;
}
cfg_stat.st_size -= strlen("global ") + strlen(globals[i]) + strlen("\n");
free(globals[i]);
global_cnt--;
if (i != global_cnt) {
globals[i] = globals[global_cnt];
}
return size;
}
static int cfg_add_override(const char *const buf, size_t size)
{
/*
* Ensure there is a trailing null so that sscanf doesn't overflow if
* we get bad input.
*/
char nbuf[PIPE_BUF];
if (size > sizeof(nbuf) - 1) {
return -ENAMETOOLONG;
}
memcpy(nbuf, buf, size);
nbuf[size] = '\0';
/*
* Tokenize
*/
char buf_cmd[PIPE_BUF];
char space1;
char buf_type[PIPE_BUF];
char space2;
char buf_path[PIPE_BUF];
char space3;
char buf_content[PIPE_BUF];
char newline;
if (sscanf(nbuf, "%s%c%s%c%s%c%s%c", buf_cmd, &space1, buf_type,
&space2, buf_path, &space3, buf_content, &newline) != 8) {
return -EINVAL;
}
/*
* Sanity check
*/
if (strcmp(buf_cmd, CMD_ADD_OVERRIDE) != 0 || space1 != ' ' ||
space2 != ' ' || space3 != ' ' || newline != '\n' || strchr(buf_path, '/') == NULL) {
return -EINVAL;
}
/*
* Determine type
*/
enum o_type type = ARRAY_LEN(o_type_str);
for (size_t i = 0; i < ARRAY_LEN(o_type_str); i++) {
if (strcmp(buf_type, o_type_str[i]) == 0) {
type = i;
break;
}
}
if (type == ARRAY_LEN(o_type_str)) {
return -EINVAL;
}
int fd = -1;
char *path = NULL;
char *content = NULL;
char *inject = NULL;
size_t inject_len = 0;
if (type == TYPE_INJECT) {
fd = open(buf_content, O_RDONLY);
struct stat stbuf;
if (fstat(fd, &stbuf) < 0) {
goto free_and_abort_enomem;
}
inject_len = stbuf.st_size;
inject = malloc(inject_len + 1);
if (inject == NULL) {
goto free_and_abort_enomem;
}
if (read(fd, inject, inject_len) != (ssize_t) inject_len) {
goto free_and_abort_enomem;
}
inject[inject_len] = '\0';
close(fd);
} else {
inject = NULL;
}
if (type == TYPE_INJECT) {
for (size_t i = 0; i < override_cnt; i++) {
if (strcmp(overrides[i].path, buf_path) != 0) {
continue;
}
if (overrides[i].type != type) {
continue;
}
/*
* double add inject indicates replace old content with
* new
*/
(void)uninject(local_ref_fd, overrides[i].path + 1,
overrides[i].inject, overrides[i].inject_len);
free(overrides[i].inject);
overrides[i].inject = inject;
overrides[i].inject_len = inject_len;
return 0;
}
}
/*
* Avoid duplicate entries
*/
for (size_t i = 0; i < override_cnt; i++) {
if (strcmp(overrides[i].path, buf_path) == 0) {
return 0;
}
}
if (override_alloc < override_cnt + 1) {
struct override *new_overrides = realloc(overrides,
(override_cnt + 1) * sizeof(struct override));
if (new_overrides == NULL) {
goto free_and_abort_enomem;
}
overrides = new_overrides;
override_alloc = override_cnt + 1;
}
path = malloc(strlen(buf_path) + 1);
if (path == NULL) {
goto free_and_abort_enomem;
}
strcpy(path, buf_path);
content = malloc(strlen(buf_content) + 1);
if (content == NULL) {
goto free_and_abort_enomem;
}
strcpy(content, buf_content);
overrides[override_cnt].path = path;
overrides[override_cnt].type = type;
overrides[override_cnt].content = content;
overrides[override_cnt].content_len = strlen(content);
overrides[override_cnt].inject = inject;
overrides[override_cnt].inject_len = inject_len;
overrides[override_cnt].last_override = 0;
override_cnt++;
cfg_stat.st_size += strlen("override ") + strlen(o_type_str[type]) +
strlen(" ") + strlen(path) + strlen(" ") + strlen(content) + strlen("\n");
return size;
free_and_abort_enomem:
if (content != NULL) {
free(content);
}
if (path != NULL) {
free(path);
}
if (inject != NULL) {
free(inject);
}
if (fd > 0) {
close(fd);
}
return -EINVAL;
}
static int cfg_rm_override(const char *const buf, size_t size)
{
/*
* Ensure there is a trailing null so that sscanf doesn't overflow if
* we get bad input.
*/
char nbuf[PIPE_BUF];
if (size > sizeof(nbuf) - 1) {
return -ENAMETOOLONG;
}
memcpy(nbuf, buf, size);
nbuf[size] = '\0';
/*
* Tokenize
*/
char buf_cmd[PIPE_BUF];
char space;
char buf_path[PIPE_BUF];
char newline;
if (sscanf(nbuf, "%s%c%s%c", buf_cmd, &space, buf_path, &newline) != 4) {
return -EINVAL;
}
/*
* Sanity check
*/
if (strcmp(buf_cmd, CMD_RM_OVERRIDE) != 0 || space != ' ' || newline != '\n') {
return -EINVAL;
}
size_t i;
for (i = 0; i < override_cnt; i++) {
if (strcmp(overrides[i].path, buf_path) == 0) {
break;
}
}
if (i == override_cnt) {
return size;
}
if (overrides[i].type == TYPE_INJECT) {
(void)uninject(local_ref_fd, overrides[i].path + 1, overrides[i].inject, overrides[i].inject_len);
}
cfg_stat.st_size -= strlen("override ") +
strlen(o_type_str[overrides[i].type]) +
strlen(" ") + strlen(overrides[i].path) + strlen(" ") + strlen(overrides[i].content) + strlen("\n");
free(overrides[i].path);
free(overrides[i].content);
if (overrides[i].inject != NULL) {
free(overrides[i].inject);
}
override_cnt--;
if (i != override_cnt) {
overrides[i] = overrides[override_cnt];
}
return size;
}
static int cfg_read(char *buf, size_t size, off_t offset)
{
char *str = malloc(cfg_stat.st_size + 1);
if (str == NULL) {
return -ENOMEM;
}
memset(str, 0, cfg_stat.st_size);
for (size_t i = 0; i < global_cnt; i++) {
strcat(str, "global ");
strcat(str, globals[i]);
strcat(str, "\n");
}
for (size_t i = 0; i < override_cnt; i++) {
strcat(str, "override ");
strcat(str, o_type_str[overrides[i].type]);
strcat(str, " ");
strcat(str, overrides[i].path);
strcat(str, " ");
strcat(str, overrides[i].content);
strcat(str, "\n");
}
int rv = MIN(strlen(str + offset), size);
memcpy(buf, str + offset, rv);
free(str);
return rv;
}
static void *m_init(struct fuse_conn_info *conn, struct fuse_config *cfg)
{
(void)conn;
/*
* Do not allow requests to be interrupted.
*/
cfg->intr = 0;
/*
* Honor provided st_ino field in getattr() and fill_dir().
*/
cfg->use_ino = 1;
/*
* Always populate path.
*/
cfg->nullpath_ok = 0;
/*
* Pick up changes from lower filesystem immediately.
*/
cfg->entry_timeout = 0;
cfg->attr_timeout = 0;
cfg->negative_timeout = 0;
return NULL;
}
static int m_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi)
{
(void)fi;
DEBUG("m_getattr", path);
FS_IMP_SETUP(path);
if (strcmp(rpath, CFG_NAME) == 0) {
*stbuf = cfg_stat;
rv = 0;
} else {
rv = fstatat(ref_fd, rpath, stbuf, AT_SYMLINK_NOFOLLOW);
}
FS_IMP_RETURN(rv);
}
static int m_access(const char *path, int mask)
{
DEBUG("m_access", path);
FS_IMP_SETUP(path);
DISALLOW_ON_CFG(rpath);
/*
* TODO: musl does not currently support AT_SYMLINK_NOFOLLOW on
* faccessat. Ask them to add support then utilize it, if possible.
*/
rv = faccessat(ref_fd, rpath, mask, AT_EACCESS);
FS_IMP_RETURN(rv);
}
static int m_readlink(const char *path, char *buf, size_t size)
{
DEBUG("m_readlink", path);
FS_IMP_SETUP(path);
DISALLOW_ON_CFG(rpath);
ssize_t bytes_read = readlinkat(ref_fd, rpath, buf, size);
if (bytes_read < 0 || (size_t)bytes_read >= size) {
rv = -1;
} else {
rv = 0;
buf[bytes_read] = '\0';
}
FS_IMP_RETURN(rv);
}
/*
* Ideally we should track the DIR so we can closedir() it in m_releasedir().
* However, libfuse's expectation to do so require ugly casting. For the time
* being, just close the DIR here and assume releasedir() succeeds.
*/
static int m_opendir(const char *path, struct fuse_file_info *fi)
{
(void)fi;
DEBUG("m_opendir", path);
FS_IMP_SETUP(path);
DISALLOW_ON_CFG(rpath);
DIR *d;
int fd = openat(ref_fd, rpath, O_NONBLOCK | O_DIRECTORY | O_RDONLY | O_NOFOLLOW);
if (fd < 0) {
rv = -1;
} else if ((d = fdopendir(fd)) == NULL) {
rv = -1;
close(fd);
} else {
rv = 0;
close(fd);
closedir(d);
}
FS_IMP_RETURN(rv);
}
static int m_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t
offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags)
{
(void)offset;
(void)fi;
(void)flags;
DEBUG("m_readdir", path);
FS_IMP_SETUP(path);
DISALLOW_ON_CFG(rpath);
size_t path_len = strlen(path);
int dir_exists = 0;
int fd = openat(global_ref_fd, rpath, O_NONBLOCK | O_DIRECTORY | O_RDONLY);
DIR *d = fdopendir(fd);
if (d) {
dir_exists = 1;
struct dirent *dir;
while ((dir = readdir(d)) != NULL) {
char full_path[PATH_MAX];
int s = snprintf(full_path, sizeof(full_path),
path[1] ? "%s/%s" : "%s%s", path, dir->d_name);
if (s < 0 || s >= (int)sizeof(full_path)) {
continue;
}
for (size_t i = 0; i < global_cnt; i++) {
if (strcmp(full_path, globals[i]) == 0) {
filler(buf, dir->d_name, NULL, 0, 0);
break;
}
}
}
closedir(d);
}
if (fd >= 0) {
close(fd);
}
for (size_t i = 0; i < override_cnt; i++) {
if (overrides[i].type == TYPE_INJECT) {
continue;
}
if (strncmp(overrides[i].path, path, path_len) != 0) {
continue;
}
if (path[1] == '\0') {
if (strchr(overrides[i].path + 1, '/') == NULL) {
filler(buf, overrides[i].path + path_len, NULL, 0, 0);
}
} else {
if (overrides[i].path[path_len] != '/') {
continue;
}
if (strchr(overrides[i].path + path_len + 1, '/')
== NULL) {
filler(buf, overrides[i].path + path_len + 1, NULL, 0, 0);
}
}
}
fd = openat(ref_fd, rpath, O_NONBLOCK | O_DIRECTORY | O_RDONLY);
d = fdopendir(fd);
if (d) {
dir_exists = 1;
struct dirent *dir;
while ((dir = readdir(d)) != NULL) {
char full_path[PATH_MAX];
int s = snprintf(full_path, sizeof(full_path),
path[1] ? "%s/%s" : "%s%s", path, dir->d_name);
if (s < 0 || s >= (int)sizeof(full_path)) {
continue;
}
int is_global = 0;
for (size_t i = 0; i < global_cnt; i++) {
if (strcmp(full_path, globals[i]) == 0) {
is_global = 1;
break;
}
}
int is_override = 0;
for (size_t i = 0; i < override_cnt; i++) {
if (overrides[i].type == TYPE_INJECT) {
continue;
}
if (strcmp(full_path, overrides[i].path) == 0) {
is_override = 1;
break;
}
}
if (!is_global && !is_override && strcmp(buf, CFG_NAME) != 0) {
filler(buf, dir->d_name, NULL, 0, 0);
}
}
closedir(d);
}
if (fd >= 0) {
close(fd);
}
if (dir_exists) {
rv = 0;
if (path[1] == '\0') {
filler(buf, CFG_NAME, NULL, 0, 0);
}
} else {
rv = -1;
errno = ENOENT;
}
FS_IMP_RETURN(rv);
}
/*
* Ideally we should track the DIR in m_opendir() so we can closedir() here.
* However, libfuse's expectation to do so require ugly casting. For the time
* being, just close the DIR in m_opendir() and assume this succeeds.
*/
static int m_releasedir(const char *path, struct fuse_file_info *fi)
{
(void)fi;
(void)path;
DEBUG("m_releasedir", path);
return 0;
}
static int m_mknod(const char *path, mode_t mode, dev_t rdev)
{
DEBUG("m_mknod", path);
FS_IMP_SETUP(path);
DISALLOW_ON_CFG(rpath);
rv = mknodat(ref_fd, rpath, mode, rdev);
FS_IMP_RETURN(rv);
}
static int m_mkdir(const char *path, mode_t mode)
{
DEBUG("m_mkdir", path);
FS_IMP_SETUP(path);
DISALLOW_ON_CFG(rpath);
rv = mkdirat(ref_fd, rpath, mode);
FS_IMP_RETURN(rv);
}
static int m_symlink(const char *symlink_string, const char *path)
{
DEBUG("m_symlink", path);
FS_IMP_SETUP(path);
DISALLOW_ON_CFG(rpath);
rv = symlinkat(symlink_string, ref_fd, rpath);
FS_IMP_RETURN(rv);
}
static int m_unlink(const char *path)
{
DEBUG("m_unlink", path);
FS_IMP_SETUP(path);
DISALLOW_ON_CFG(rpath);
rv = unlinkat(ref_fd, rpath, 0);
FS_IMP_RETURN(rv);
}
static int m_rmdir(const char *path)
{
DEBUG("m_rmdir", path);
FS_IMP_SETUP(path);
DISALLOW_ON_CFG(rpath);
rv = unlinkat(ref_fd, rpath, AT_REMOVEDIR);
FS_IMP_RETURN(rv);
}
/*
* TODO: This creates a temporary, virtual-filesystem-visible file. This is
* bothersome aesthetically, introduces a minor risk of some other process
* manipulating the file, and introduces a risk of leaving a file on-disk if
* interrupted.
*
* The ideal alternative would be to use openat() with O_TMPFILE and linkat()
* with AT_REPLACE. However, at the time of writing O_TMPFILE is only
* supported on a subset of filesystems and AT_REPLACE is not upstreamed into
* Linux.
*
* Once available, we should investigate conditionally utilizing
* O_TMPFILE/AT_REPLACE if the option is available on the given system. If it
* is not, we can fall back to the current method below.
*/
static int m_rename(const char *from, const char *to, unsigned int flags)
{
DEBUG("m_rename:from", from);
DEBUG("m_rename:to", to);
FS_IMP_SETUP(from);
from = rpath;
DISALLOW_ON_CFG(from);
int to_ref_fd = get_ref_fd(to);
to = (to && to[1]) ? to + 1 : ".";
DISALLOW_ON_CFG(to);
char buf[PATH_MAX];
char tmp_path[PATH_MAX];
ssize_t bytes_read;
ssize_t bytes_written;
int from_fd = -1;
int to_fd = -1;
if (flags) {
/*
* TODO: implement flags.
*
* Implementing this would require renameat2() which is
* upstreamed into Linux but, at the time of writing, not yet
* provided by musl.
*/
rv = -1;
errno = EOPNOTSUPP;
goto clean_up_and_return;
}
/*
* Try to renameat() first. If it fails due to EXDEV fall back to
* manually handling the request.
*/
rv = renameat(ref_fd, from, to_ref_fd, to);
if (rv >= 0 || (rv < 0 && errno != EXDEV)) {
goto clean_up_and_return;
}
DEBUG("m_rename:exdev", from);
struct stat stbuf;
if ((rv = fstatat(ref_fd, from, &stbuf, AT_SYMLINK_NOFOLLOW)) < 0) {
goto clean_up_and_return;
}
switch (stbuf.st_mode & S_IFMT) {
case S_IFBLK:
case S_IFCHR:
case S_IFIFO:
case S_IFSOCK:
if ((rv = mknodat(to_ref_fd, to, stbuf.st_mode, stbuf.st_rdev)) < 0) {
goto clean_up_and_return;
}
break;
case S_IFLNK:
bytes_read = readlinkat(ref_fd, from, buf, sizeof(buf));
if (bytes_read < 0) {
rv = bytes_read;
goto clean_up_and_return;
}
if ((size_t)bytes_read >= sizeof(buf)) {
rv = -1;
errno = ENAMETOOLONG;
goto clean_up_and_return;
}
buf[bytes_read] = '\0';
if ((rv = symlinkat(buf, to_ref_fd, to)) < 0) {
goto clean_up_and_return;
}
break;
case S_IFDIR:
if ((rv = mkdirat(to_ref_fd, to, stbuf.st_mode)) < 0) {
goto clean_up_and_return;
}
break;
case S_IFREG:
/*
* Get temporary file path to populate to do a
* mount-point-local rename().
*/
rv = snprintf(tmp_path, sizeof(tmp_path), "./.bedrock-tmpfile-%lu", GETTID());
if (rv < 0 || rv >= (int)sizeof(tmp_path)) {
rv = -1;
errno = ENAMETOOLONG;
goto clean_up_and_return;
}
DEBUG("m_rename:backup", tmp_path);
/*
* Copy into temporary file.
*/
unlinkat(to_ref_fd, tmp_path, 0);
if ((to_fd = openat(to_ref_fd, tmp_path, O_NONBLOCK | O_CREAT | O_RDWR | O_NOFOLLOW, stbuf.st_mode)) < 0) {
rv = -1;
goto clean_up_and_return;
}
if ((from_fd = openat(ref_fd, from, O_NONBLOCK | O_RDONLY | O_NOFOLLOW)) < 0) {
rv = -1;
goto clean_up_and_return;
}
if ((rv = ftruncate(to_fd, stbuf.st_size)) < 0) {
goto clean_up_and_return;
}
while ((bytes_read = read(from_fd, buf, sizeof(buf))) > 0) {
if ((bytes_written = write(to_fd, buf, bytes_read)) < 0) {
DEBUG("m_rename:tmp-write-error", tmp_path);
rv = -1;
goto clean_up_and_return;
}
}
if (bytes_read < 0) {
DEBUG("m_rename:source-read-error", from);
rv = -1;
goto clean_up_and_return;
}
close(to_fd);
/*
* rename() the temporary file to the target.
*/
DEBUG("m_rename:renameat", tmp_path);
if ((rv = renameat(to_ref_fd, tmp_path, to_ref_fd, to)) < 0) {
DEBUG("m_rename:renameat-error", tmp_path);
goto clean_up_and_return;
}
break;
}
/*
* Copy metadata
*/
if ((rv = fchownat(to_ref_fd, to, stbuf.st_uid, stbuf.st_gid, AT_SYMLINK_NOFOLLOW)) < 0) {
goto clean_up_and_return;
}
if ((rv = fchmodat(to_ref_fd, to, stbuf.st_mode, AT_SYMLINK_NOFOLLOW)) < 0) {
goto clean_up_and_return;
}
/*
* Everything should be in place. Remove original.
*/
int unlinkflag = 0;
if (S_ISDIR(stbuf.st_mode)) {
unlinkflag = AT_REMOVEDIR;
}
if ((rv = unlinkat(ref_fd, from, unlinkflag)) < 0) {
goto clean_up_and_return;
}
rv = 0;
clean_up_and_return:
if (from_fd >= 0) {
close(from_fd);
}
if (to_fd >= 0) {
close(to_fd);
}
FS_IMP_RETURN(rv);
}
static int m_link(const char *from, const char *to)
{
DEBUG("m_link:from", from);
DEBUG("m_link:to", to);
FS_IMP_SETUP(from);
from = rpath;
DISALLOW_ON_CFG(from);
int to_ref_fd = get_ref_fd(to);
to = (to && to[1]) ? to + 1 : ".";
DISALLOW_ON_CFG(to);
rv = linkat(ref_fd, from, to_ref_fd, to, 0);
FS_IMP_RETURN(rv);
}
static int m_chmod(const char *path, mode_t mode, struct fuse_file_info *fi)
{
(void)fi;
DEBUG("m_chmod", path);
FS_IMP_SETUP(path);
DISALLOW_ON_CFG(rpath);
rv = fchmodat(ref_fd, rpath, mode, AT_SYMLINK_NOFOLLOW);
FS_IMP_RETURN(rv);
}
static int m_chown(const char *path, uid_t uid, gid_t gid, struct fuse_file_info *fi)
{
(void)fi;
DEBUG("m_chown", path);
FS_IMP_SETUP(path);
DISALLOW_ON_CFG(rpath);
rv = fchownat(ref_fd, rpath, uid, gid, AT_SYMLINK_NOFOLLOW);
FS_IMP_RETURN(rv);
}
static int m_truncate(const char *path, off_t size, struct fuse_file_info *fi)
{
(void)fi;
DEBUG("m_truncate", path);
FS_IMP_SETUP(path);
DISALLOW_ON_CFG(rpath);
int fd = openat(ref_fd, rpath, O_NONBLOCK | O_RDWR | O_NOFOLLOW);
if (fd < 0) {
rv = fd;
} else {
rv = ftruncate(fd, size);
close(fd);
}
FS_IMP_RETURN(rv);
}
static int m_utimens(const char *path, const struct timespec ts[2], struct fuse_file_info *fi)
{
(void)fi;
DEBUG("m_utimens", path);
FS_IMP_SETUP(path);
DISALLOW_ON_CFG(rpath);
rv = utimensat(ref_fd, rpath, ts, AT_SYMLINK_NOFOLLOW);
FS_IMP_RETURN(rv);
}
static int m_create(const char *path, mode_t mode, struct fuse_file_info *fi)
{
DEBUG("m_create", path);
FS_IMP_SETUP(path);
DISALLOW_ON_CFG(rpath);
int fd = openat(ref_fd, rpath, O_NONBLOCK | fi->flags, mode);
if (fd < 0) {
rv = -1;
} else {
rv = 0;
fi->fh = fd;
}
FS_IMP_RETURN(rv);
}
static int m_open(const char *path, struct fuse_file_info *fi)
{
DEBUG("m_open", path);
FS_IMP_SETUP(path);
if (strcmp(rpath, CFG_NAME) == 0) {
struct fuse_context *context = fuse_get_context();
fi->fh = -1;
if (context->uid != 0) {
rv = -1;
errno = EACCES;
} else {
rv = 0;
}
} else {
int fd = openat(ref_fd, rpath, O_NONBLOCK | fi->flags);
if (fd < 0) {
rv = -1;
fi->fh = -1;
} else {
rv = 0;
fi->fh = fd;
}
}
FS_IMP_RETURN(rv);
}
static int m_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi)
{
(void)fi;
DEBUG("m_read", path);
FS_IMP_SETUP(path);
if (strcmp(rpath, CFG_NAME) == 0) {
struct fuse_context *context = fuse_get_context();
if (context->uid == 0) {
rv = cfg_read(buf, size, offset);
} else {
rv = -1;
errno = EACCES;
}
} else {
int fd = openat(ref_fd, rpath, O_NONBLOCK | O_RDONLY | O_NOFOLLOW);
if (fd >= 0) {
rv = pread(fd, buf, size, offset);
close(fd);
} else {
rv = -1;
}
}
FS_IMP_RETURN(rv);
}
static int m_write(const char *path, const char *buf, size_t size, off_t offset, struct fuse_file_info *fi)
{
(void)offset;
(void)fi;
DEBUG("m_write", path);
FS_IMP_SETUP(path);
if (strcmp(rpath, CFG_NAME) == 0) {
pthread_rwlock_unlock(&cfg_lock);
pthread_rwlock_wrlock(&cfg_lock);
struct fuse_context *context = fuse_get_context();
if (context->uid != 0) {
rv = -1;
errno = EACCES;
} else if (strncmp(buf, CMD_ADD_GLOBAL, CMD_ADD_GLOBAL_LEN) == 0) {
rv = cfg_add_global(buf, size);
} else if (strncmp(buf, CMD_RM_GLOBAL, CMD_RM_GLOBAL_LEN) == 0) {
rv = cfg_rm_global(buf, size);
} else if (strncmp(buf, CMD_ADD_OVERRIDE, CMD_ADD_OVERRIDE_LEN) == 0) {
rv = cfg_add_override(buf, size);
} else if (strncmp(buf, CMD_RM_OVERRIDE, CMD_RM_OVERRIDE_LEN) == 0) {
rv = cfg_rm_override(buf, size);
} else {
rv = -1;
errno = EINVAL;
}
pthread_rwlock_unlock(&cfg_lock);
pthread_rwlock_rdlock(&cfg_lock);
} else {
int fd = openat(ref_fd, rpath, O_NONBLOCK | O_WRONLY | O_NOFOLLOW);
if (fd >= 0) {
rv = pwrite(fd, buf, size, offset);
close(fd);
} else {
rv = -1;
}
}
FS_IMP_RETURN(rv);
}
static int m_statfs(const char *path, struct statvfs *stbuf)
{
DEBUG("m_statfs", path);
FS_IMP_SETUP(path);
DISALLOW_ON_CFG(rpath);
int fd = openat(ref_fd, rpath, O_NONBLOCK | O_RDONLY | O_NOFOLLOW | O_DIRECTORY);
if (fd >= 0) {
rv = fstatvfs(fd, stbuf);
close(fd);
} else {
rv = -1;
}
FS_IMP_RETURN(rv);
}
/*
* flush() is asking us to close the file descriptor to flush everything to
* disk, *but* this may be called multiple times (multiple file descriptors to
* same file?), and so we can't actually close the file. We can use dup() to
* get the desired effect.
*/
static int m_flush(const char *path, struct fuse_file_info *fi)
{
DEBUG("m_flush", path);
FS_IMP_SETUP_FD(fi->fh, path, 0);
rv = close(dup(fi->fh));
FS_IMP_RETURN(rv);
}
/*
* Final close() call on the file.
*/
static int m_release(const char *path, struct fuse_file_info *fi)
{
DEBUG("m_release", path);
FS_IMP_SETUP_FD(fi->fh, path, 0);
rv = close(fi->fh);
FS_IMP_RETURN(rv);
}
/*
* The FUSE API talks about a 'datasync parameter' being non-zero - presumably
* that's the second parameter, since its the only number (int).
*/
static int m_fsync(const char *path, int datasync, struct fuse_file_info *fi)
{
DEBUG("m_fsync", path);
FS_IMP_SETUP_FD(fi->fh, path, 0);
if (datasync) {
rv = fdatasync(fi->fh);
} else {
rv = fsync(fi->fh);
}
FS_IMP_RETURN(rv);
}
static int m_fallocate(const char *path, int mode, off_t offset, off_t length, struct fuse_file_info *fi)
{
(void)fi;
DEBUG("m_fallocate", path);
FS_IMP_SETUP(path);
DISALLOW_ON_CFG(rpath);
int fd = openat(ref_fd, rpath, O_NONBLOCK | O_RDWR | O_NOFOLLOW);
if (fd >= 0) {
rv = fallocate(fd, mode, offset, length);
close(fd);
} else {
rv = -1;
}
FS_IMP_RETURN(rv);
}
static int m_setxattr(const char *path, const char *name, const char *value, size_t size, int flags)
{
DEBUG("m_setxattr", path);
FS_IMP_SETUP(path);
DISALLOW_ON_CFG(rpath);
int fd = openat(ref_fd, rpath, O_NONBLOCK | O_RDONLY | O_NOFOLLOW);
if (fd >= 0) {
char buf[PATH_MAX];
if (procpath(fd, buf, sizeof(buf)) < 0) {
rv = -1;
errno = ENAMETOOLONG;
} else {
rv = lsetxattr(buf, name, value, size, flags);
}
close(fd);
} else {
rv = -1;
}
FS_IMP_RETURN(rv);
}
static int m_getxattr(const char *path, const char *name, char *value, size_t size)
{
DEBUG("m_getxattr", path);
FS_IMP_SETUP(path);
int fd = -1;
if (strcmp(rpath, CFG_NAME) == 0 && strcmp(STRATUM_XATTR, name) == 0) {
if (size <= 0) {
rv = strlen(GLOBAL_STRATUM);
} else if (size < strlen(GLOBAL_STRATUM)) {
rv = -1;
errno = ERANGE;
} else {
rv = strlen(GLOBAL_STRATUM);
strcpy(value, GLOBAL_STRATUM);
}
} else if (strcmp(rpath, CFG_NAME) == 0 && strcmp(LPATH_XATTR, name) == 0) {
if (size <= 0) {
rv = strlen(ROOTDIR);
} else if (size < strlen(ROOTDIR)) {
rv = -1;
errno = ERANGE;
} else {
rv = strlen(ROOTDIR);
strcpy(value, ROOTDIR);
}
} else if (strcmp(rpath, CFG_NAME) == 0) {
rv = -1;
errno = ENODATA;
} else if ((fd = openat(ref_fd, rpath, O_NONBLOCK | O_RDONLY | O_NOFOLLOW)) < 0) {
/*
* Linux fails to provide a lgetxattr() equivalent which apply
* to file descriptors and directly (without following) on
* symlinks. For example, neither lgetxattr() nor fgetxattr()
* have both of these attributes. We cannot use lgetxattr() as
* that would loop back to us. This filesystem's entire design
* revolves around using file descriptors as references.
*
* If we can open the file we can use /proc to get an absolute
* file path to the file. This is what the procpath() call
* does below.
*
* There are two scenarios where we cannot open then file:
*
* - If the file's permissions disallow it. For example,
* /etc/passwd. This returns EACCES.
* - If it's a bad symlink, such as one that is EINVAL, ELOOP,
* or ENAMETOOLONG.
*
* Most users do not use xattrs, and thus our best guess in
* these scenarios is that the proper return value is probably
* ENODATA. Until we have a better solution, use ENODATA.
*/
if (errno == EACCES || errno == EINVAL || errno == ELOOP || errno == ENAMETOOLONG) {
errno = ENODATA;
}
rv = -1;
} else if (strcmp(STRATUM_XATTR, name) == 0 && ref_fd == global_ref_fd) {
if (size <= 0) {
rv = strlen(GLOBAL_STRATUM);
} else if (size < strlen(GLOBAL_STRATUM)) {
rv = -1;
errno = ERANGE;
} else {
rv = strlen(GLOBAL_STRATUM);
strcpy(value, GLOBAL_STRATUM);
}
} else if (strcmp(STRATUM_XATTR, name) == 0) {
if (size <= 0) {
rv = strlen(local_name);
} else if (size < strlen(local_name)) {
rv = -1;
errno = ERANGE;
} else {
rv = strlen(local_name);
strcpy(value, local_name);
}
} else if (strcmp(LPATH_XATTR, name) == 0) {
char lpath[PATH_MAX];
int s = snprintf(lpath, sizeof(lpath), "%s%s", mntpt, path);
if (s < 0 || s >= (int)sizeof(lpath)) {
rv = -1;
errno = E2BIG;
} else if (size <= 0) {
rv = strlen(lpath);
} else if (size < strlen(lpath)) {
rv = -1;
errno = ERANGE;
} else {
rv = strlen(lpath);
strcpy(value, lpath);
}
} else {
char buf[PATH_MAX];
if (procpath(fd, buf, sizeof(buf)) < 0) {
rv = -1;
errno = ENAMETOOLONG;
} else {
rv = lgetxattr(buf, name, value, size);
}
}
if (fd >= 0) {
close(fd);
}
FS_IMP_RETURN(rv);
}
static int m_listxattr(const char *path, char *list, size_t size)
{
DEBUG("m_listxattr", path);
FS_IMP_SETUP(path);
DISALLOW_ON_CFG(rpath);
int fd = openat(ref_fd, rpath, O_NONBLOCK | O_RDWR | O_NOFOLLOW);
if (fd >= 0) {
char buf[PATH_MAX];
if (procpath(fd, buf, sizeof(buf)) < 0) {
rv = -1;
errno = ENAMETOOLONG;
} else {
rv = llistxattr(buf, list, size);
}
close(fd);
} else {
rv = -1;
}
FS_IMP_RETURN(rv);
}
static int m_removexattr(const char *path, const char *name)
{
DEBUG("m_removexattr", path);
FS_IMP_SETUP(path);
DISALLOW_ON_CFG(rpath);
int fd = openat(ref_fd, rpath, O_NONBLOCK | O_RDWR | O_NOFOLLOW);
if (fd >= 0) {
char buf[PATH_MAX];
if (procpath(fd, buf, sizeof(buf)) < 0) {
rv = -1;
errno = ENAMETOOLONG;
} else {
rv = lremovexattr(buf, name);
}
close(fd);
} else {
rv = -1;
}
FS_IMP_RETURN(rv);
}
static int m_flock(const char *path, struct fuse_file_info *fi, int op)
{
(void)path;
DEBUG("m_flock", path);
FS_IMP_SETUP_FD(fi->fh, path, -EINVAL);
rv = flock(fi->fh, op);
FS_IMP_RETURN(rv);
}
/*
* Implemented filesystem calls
*
* Do not implement ioctl. FUSE's ioctl acts differently from that of other
* filesystems in a way which causes significant problems, and not implementing
* ioctl appears to be the cleanest solution. See the following links for more
* information about FS_IOC_GETFLAGS's issue:
*
* http://linux-fsdevel.vger.kernel.narkive.com/eUZdzNjw/argument-type-for-fs-ioc-getflags-fs-ioc-setflags-ioctls
* http://sourceforge.net/p/fuse/mailman/message/31773852/
*
*/
static struct fuse_operations m_oper = {
.init = m_init,
.getattr = m_getattr,
.access = m_access,
.readlink = m_readlink,
.opendir = m_opendir,
.readdir = m_readdir,
.releasedir = m_releasedir,
.mknod = m_mknod,
.mkdir = m_mkdir,
.symlink = m_symlink,
.unlink = m_unlink,
.rmdir = m_rmdir,
.rename = m_rename,
.link = m_link,
.chmod = m_chmod,
.chown = m_chown,
.truncate = m_truncate,
.utimens = m_utimens,
.create = m_create,
.open = m_open,
.read = m_read,
/* .read_buf = m_read_buf, */
.write = m_write,
/* .write_buf = m_write_buf, */
.statfs = m_statfs,
.flush = m_flush,
.release = m_release,
.fsync = m_fsync,
.fallocate = m_fallocate,
.setxattr = m_setxattr,
.getxattr = m_getxattr,
.listxattr = m_listxattr,
.removexattr = m_removexattr,
/* .lock = m_lock, TODO */
.flock = m_flock,
};
int main(int argc, char *argv[])
{
/*
* Ensure we are running as root. This is needed to mimic caller
* process permissions.
*/
if (getuid() != 0) {
fprintf(stderr, "error: not running as root.\n");
return 1;
}
/*
* Extract mount point from arguments
*/
struct fuse_args args = FUSE_ARGS_INIT(argc, argv);
if (fuse_opt_parse(&args, NULL, NULL, NULL) < 0) {
fprintf(stderr, "error: unable to parse arguments.\n");
return 1;
}
struct fuse_cmdline_opts opts;
if (fuse_parse_cmdline(&args, &opts) < 0) {
fprintf(stderr, "error: unable to parse arguments.\n");
return 1;
}
if (opts.mountpoint == NULL) {
fprintf(stderr, "error: no mount point provided.\n");
return 1;
}
debug = opts.debug;
/*
* Get local mount point reference before mounting over.
*/
mntpt = opts.mountpoint;
if ((local_ref_fd = open(mntpt, O_DIRECTORY)) < 0) {
fprintf(stderr, "error: unable to open local mount point\n");
return 1;
}
/*
* Get global mount point reference
*/
int global_root_fd = open(global_root, O_DIRECTORY);
const char *const rmntpt = (mntpt && mntpt[1]) ? mntpt + 1 : ".";
if ((global_ref_fd = openat(global_root_fd, rmntpt, O_NONBLOCK | O_DIRECTORY)) < 0) {
fprintf(stderr, "error: unable to open global mount point\n");
return 1;
}
close(global_root_fd);
/*
* Get local stratum name
*/
if (lgetxattr("/", STRATUM_XATTR, local_name, sizeof(local_name)) < 0) {
fprintf(stderr, "error: unable to determine local stratum\n");
return 1;
}
/*
* Initialize mutex
*/
if (pthread_rwlock_init(&cfg_lock, NULL) < 0) {
fprintf(stderr, "error: initializing mutexes\n");
return 1;
}
/*
* Initialize config stat
*/
memset(&cfg_stat, 0, sizeof(struct stat));
cfg_stat.st_ctime = time(NULL);
cfg_stat.st_mtime = cfg_stat.st_ctime;
cfg_stat.st_atime = cfg_stat.st_ctime;
cfg_stat.st_mode = S_IFREG | 0600;
cfg_stat.st_size = 0;
/*
* Clear umask
*/
umask(0);
/*
* Mount filesystem.
*
* Incoming filesystem calls will be fulfilled by the functions listed
* in m_oper above.
*/
return fuse_main(argc, argv, &m_oper, NULL);
}