From b57882b2ffd1000ce4a4628dff4ec6a651edf4da Mon Sep 17 00:00:00 2001 From: Nekun Date: Tue, 22 Feb 2022 05:49:19 +0000 Subject: [PATCH 1/8] Initial implementation of file filter: hiding specified files --- src/Makefile.am | 4 +- src/bindfs.c | 253 +++++++++++++++++++++++++++++++++++++++++++++++- src/filter.c | 126 ++++++++++++++++++++++++ src/filter.h | 42 ++++++++ 4 files changed, 419 insertions(+), 6 deletions(-) create mode 100644 src/filter.c create mode 100644 src/filter.h diff --git a/src/Makefile.am b/src/Makefile.am index 7e25193..5e00a8c 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -2,8 +2,8 @@ bin_PROGRAMS = bindfs -noinst_HEADERS = debug.h permchain.h userinfo.h arena.h misc.h usermap.h rate_limiter.h -bindfs_SOURCES = bindfs.c debug.c permchain.c userinfo.c arena.c misc.c usermap.c rate_limiter.c +noinst_HEADERS = debug.h permchain.h userinfo.h arena.h misc.h usermap.h rate_limiter.h filter.h +bindfs_SOURCES = bindfs.c debug.c permchain.c userinfo.c arena.c misc.c usermap.c rate_limiter.c filter.c AM_CPPFLAGS = ${my_CPPFLAGS} ${fuse_CFLAGS} ${fuse3_CFLAGS} AM_CFLAGS = ${my_CFLAGS} diff --git a/src/bindfs.c b/src/bindfs.c index bc24154..90b3f75 100644 --- a/src/bindfs.c +++ b/src/bindfs.c @@ -64,11 +64,7 @@ #include #endif -#ifdef HAVE_FUSE_3 -#ifndef __NR_renameat2 #include // For dirname(), basename() -#endif -#endif #ifdef __linux__ #include @@ -90,6 +86,7 @@ #include "rate_limiter.h" #include "userinfo.h" #include "usermap.h" +#include "filter.h" /* Apple Structs */ #ifdef __APPLE__ @@ -136,6 +133,8 @@ static struct Settings { RateLimiter *read_limiter; RateLimiter *write_limiter; + FileFilter *filefilter; + enum CreatePolicy { CREATE_AS_USER, CREATE_AS_MOUNTER @@ -370,6 +369,58 @@ static int is_mirrored_user(uid_t uid) return 0; } +/* For "read" ops, mode should not be defined, it will be retrieved from + * existing file. For "write" ops, desired mode is specified, but if file + * exists, patterns will be matched against mode of existing file. + * + * Also, for handling "two-way" ops, mode of existing file can be returned. */ +static int filefilter_check(const char *path, mode_t mode, mode_t *f_mode) +{ + char *path_copy; + char *fn; + struct stat st; + enum { NEW, EXISTING, OVERWRITE } which; + + if (!settings.filefilter) + return 0; + + path_copy = strdup(path); + fn = basename(path_copy); + + if (lstat(path,&st) == -1) { + if (errno == ENOENT) { + which = NEW; + } else { /* is it possible? */ + errno = EIO; + goto out; + }; + } else { + /* To protect hidden files from overwriting (say, we have + * 'p:testfifo' policy and do 'mv regular_file testfifo'), + * we should check mode of existing files on write operations, + * not desired mode for new file */ + which = mode ? OVERWRITE : EXISTING; + mode = st.st_mode; + }; + + mode &= S_IFMT; + + /* Return retrieved mode of probed file for some two-way + * ops, like link and rename */ + if (f_mode) + memcpy(f_mode,&mode,sizeof(mode_t)); + + if (filefilter_find_match(settings.filefilter,fn,mode) == filefilter_status_found) { + errno = (which == NEW || which == OVERWRITE) ? EPERM : ENOENT; + goto out; + }; + + errno = 0; +out: + free(path_copy); + return errno ? -1 : 0; +} + static char *process_path(const char *path, bool resolve_symlinks) { if (path == NULL) { /* possible? */ @@ -564,6 +615,9 @@ static int delete_file(const char *path, int (*target_delete_func)(const char *) if (real_path == NULL) return -errno; + if (filefilter_check(real_path,0,NULL) == -1) + return -errno; + if (settings.resolve_symlinks) { if (lstat(real_path, &st) == -1) { free(real_path); @@ -728,6 +782,9 @@ static int bindfs_getattr(const char *path, struct stat *stbuf) if (real_path == NULL) return -errno; + if (filefilter_check(real_path,0,NULL) == -1) + return -errno; + if (lstat(real_path, stbuf) == -1) { free(real_path); return -errno; @@ -749,6 +806,9 @@ static int bindfs_fgetattr(const char *path, struct stat *stbuf, if (real_path == NULL) return -errno; + if (filefilter_check(real_path,0,NULL) == -1) + return -errno; + if (fstat(fi->fh, stbuf) == -1) { free(real_path); return -errno; @@ -768,6 +828,9 @@ static int bindfs_readlink(const char *path, char *buf, size_t size) if (real_path == NULL) return -errno; + if (filefilter_check(real_path,0,NULL) == -1) + return -errno; + /* No need to check for access to the link itself, since symlink permissions don't matter. Access to the path components of the symlink are automatically queried by FUSE. */ @@ -837,6 +900,11 @@ static int bindfs_readdir(const char *path, void *buf, fuse_fill_dir_t filler, st.st_ino = de->d_ino; st.st_mode = de->d_type << 12; + /* skip filling of matched names */ + if (settings.filefilter && + (filefilter_find_match(settings.filefilter,de->d_name,st.st_mode) == filefilter_status_found)) + continue; + if (settings.resolve_symlinks && (st.st_mode & S_IFLNK) == S_IFLNK) { int file_len = strlen(de->d_name) + 1; // (include null terminator) append_to_memory_block(&resolve_buf, de->d_name, file_len); @@ -844,6 +912,8 @@ static int bindfs_readdir(const char *path, void *buf, fuse_fill_dir_t filler, resolve_buf.size -= file_len; if (resolved) { + if (filefilter_check(resolved,0,NULL) == -1) + continue; if (lstat(resolved, &st) == -1) { result = -errno; break; @@ -889,6 +959,9 @@ static int bindfs_mknod(const char *path, mode_t mode, dev_t rdev) if (real_path == NULL) return -errno; + if (filefilter_check(real_path,mode&S_IFMT,NULL) == -1) + return -errno; + mode = permchain_apply(settings.create_permchain, mode); if (S_ISFIFO(mode)) @@ -917,6 +990,9 @@ static int bindfs_mkdir(const char *path, mode_t mode) if (real_path == NULL) return -errno; + if (filefilter_check(real_path,S_IFDIR,NULL) == -1) + return -errno; + mode |= S_IFDIR; /* tell permchain_apply this is a directory */ mode = permchain_apply(settings.create_permchain, mode); @@ -956,6 +1032,11 @@ static int bindfs_symlink(const char *from, const char *to) if (real_to == NULL) return -errno; + if (filefilter_check(from,0,NULL) == -1) + return -errno; + if (filefilter_check(real_to,S_IFLNK,NULL) == -1) + return -errno; + res = symlink(from, real_to); if (res == -1) { free(real_to); @@ -991,6 +1072,12 @@ static int bindfs_rename(const char *from, const char *to) return -errno; } + mode_t mode_from; + if (filefilter_check(real_from,0,&mode_from) == -1) + return -errno; + if (filefilter_check(real_to,mode_from,NULL) == -1) + return -errno; + #ifdef HAVE_FUSE_3 if (flags == 0) { @@ -1033,6 +1120,12 @@ static int bindfs_link(const char *from, const char *to) return -errno; } + mode_t mode_from; + if (filefilter_check(real_from,0,&mode_from) == -1) + return -errno; + if (filefilter_check(real_to,mode_from,NULL) == -1) + return -errno; + res = link(real_from, real_to); free(real_from); free(real_to); @@ -1057,6 +1150,9 @@ static int bindfs_chmod(const char *path, mode_t mode) if (real_path == NULL) return -errno; + if (filefilter_check(real_path,0,NULL) == -1) + return -errno; + if (settings.chmod_allow_x) { /* Get the old permission bits and see which bits would change. */ if (lstat(real_path, &st) == -1) { @@ -1155,6 +1251,9 @@ static int bindfs_chown(const char *path, uid_t uid, gid_t gid) if (real_path == NULL) return -errno; + if (filefilter_check(real_path,0,NULL) == -1) + return -errno; + res = lchown(real_path, uid, gid); free(real_path); if (res == -1) @@ -1178,6 +1277,9 @@ static int bindfs_truncate(const char *path, off_t size) if (real_path == NULL) return -errno; + if (filefilter_check(real_path,0,NULL) == -1) + return -errno; + res = truncate(real_path, size); free(real_path); if (res == -1) @@ -1214,6 +1316,9 @@ static int bindfs_utimens(const char *path, const struct timespec ts[2]) if (real_path == NULL) return -errno; + if (filefilter_check(real_path,0,NULL) == -1) + return -errno; + #ifdef HAVE_UTIMENSAT res = utimensat(settings.mntsrc_fd, real_path, ts, AT_SYMLINK_NOFOLLOW); #elif HAVE_LUTIMES @@ -1244,6 +1349,9 @@ static int bindfs_create(const char *path, mode_t mode, struct fuse_file_info *f if (real_path == NULL) return -errno; + if (filefilter_check(real_path,S_IFREG,NULL) == -1) + return -errno; + mode |= S_IFREG; /* tell permchain_apply this is a regular file */ mode = permchain_apply(settings.create_permchain, mode); @@ -1278,6 +1386,13 @@ static int bindfs_open(const char *path, struct fuse_file_info *fi) return -errno; int flags = fi->flags; + mode_t mode = 0; + + if (flags&O_CREAT) + mode = S_IFREG; + if (filefilter_check(real_path,mode,NULL) == -1) + return -errno; + #ifdef __linux__ if (!settings.forward_odirect) { flags &= ~O_DIRECT; @@ -1413,6 +1528,9 @@ static int bindfs_statfs(const char *path, struct statvfs *stbuf) if (real_path == NULL) return -errno; + if (filefilter_check(real_path,0,NULL) == -1) + return -errno; + res = statvfs(real_path, stbuf); free(real_path); if (res == -1) @@ -1475,6 +1593,9 @@ static int bindfs_setxattr(const char *path, const char *name, const char *value if (real_path == NULL) return -errno; + if (filefilter_check(real_path,0,NULL) == -1) + return -errno; + #if defined(__APPLE__) if (!strncmp(name, XATTR_APPLE_PREFIX, sizeof(XATTR_APPLE_PREFIX) - 1)) { flags &= ~(XATTR_NOSECURITY); @@ -1517,6 +1638,9 @@ static int bindfs_getxattr(const char *path, const char *name, char *value, if (real_path == NULL) return -errno; + if (filefilter_check(real_path,0,NULL) == -1) + return -errno; + #if defined(__APPLE__) if (strcmp(name, A_KAUTH_FILESEC_XATTR) == 0) { char new_name[MAXPATHLEN]; @@ -1547,6 +1671,10 @@ static int bindfs_listxattr(const char *path, char* list, size_t size) if (real_path == NULL) return -errno; + if (filefilter_check(real_path,0,NULL) == -1) { + return -errno; + } + #if defined(__APPLE__) ssize_t res = listxattr(real_path, list, size, XATTR_NOFOLLOW); if (res > 0) { @@ -1600,6 +1728,9 @@ static int bindfs_removexattr(const char *path, const char *name) if (real_path == NULL) return -errno; + if (filefilter_check(real_path,0,NULL) == -1) + return -errno; + #if defined(__APPLE__) if (strcmp(name, A_KAUTH_FILESEC_XATTR) == 0) { char new_name[MAXPATHLEN]; @@ -1703,6 +1834,9 @@ static void print_usage(const char *progname) " --create-for-group=... New files owned by specified group. *\n" " --create-with-perms=... Alter permissions of new files.\n" "\n" + "File filtering policy:\n" + " --file-filter=*.png/... Hide files in underlying dir.\n" + "\n" "Chown policy:\n" " --chown-normal Try to chown the original files (the default).\n" " --chown-ignore Have all chowns fail silently.\n" @@ -2174,6 +2308,105 @@ static int parse_user_map(UserMap *map, UserMap *reverse_map, char *spec) return 0; } +static int parse_file_filter(FileFilter *filter, char *spec) +{ + char *p; + enum { FN,SLASH,MODE } cpos; + FFType type; + FFStatus ret; + char *fn = NULL; + char *sl_p,*col_p; + int fn_pos = 0; + char next; + + if (strlen(spec) < 1) { + fprintf(stderr,"Pattern string not specified\n"); + return 0; + }; + + fn = malloc(PATH_MAX); + fn_pos = 0; + + for (p = spec, cpos = SLASH; *p; p++) { + next = *(p+1); + + if (cpos == SLASH) { + if (p == spec && *p != '/') + p--; + + sl_p = strchr(p+1,'/'); + col_p = strchr(p+1,':'); + + if (col_p && ( (!sl_p && col_p) || (sl_p > col_p) )) { + type = 0; + cpos = MODE; + continue; + } else { + type = FFT_ANY; + cpos = FN; + continue; + }; + }; + + if (cpos == MODE) { + switch(*p) { + case 'a': type = FFT_ANY; break; + case 's': type |= FFT_SCK; break; + case 'l': type |= FFT_LNK; break; + case 'r': type |= FFT_REG; break; + case 'b': type |= FFT_BLK; break; + case 'd': type |= FFT_DIR; break; + case 'c': type |= FFT_CHR; break; + case 'p': type |= FFT_PIP; break; + + case ':': + if (next == '/' || next == '\0') { + fprintf(stderr,"Invalid syntax: matching pattern not specified after mode specifier\n"); + goto fail; + }; + cpos = FN; + continue; + break; + default: + fprintf(stderr,"Invalid syntax: '%c' is not a valid mode token\n",*p); + goto fail; + break; + }; + }; + + if (cpos == FN) { + if (fn_pos >= PATH_MAX-1) { + fprintf(stderr,"Filename pattern too long\n"); + goto fail; + }; + if (*p == '/' || *p == '\0') { + fprintf(stderr,"Empty filename pattern\n"); + goto fail; + }; + + fn[fn_pos++] = *p; + if (next == '/' || next == '\0') { + fn[fn_pos] = '\0'; + if ((ret = filefilter_add(settings.filefilter,fn,type)) != filefilter_status_ok) { + fprintf(stderr,"Inserting filter spec '%s' failed: %s\n",fn,ffstatus_str(ret)); + goto fail; + }; + fn_pos = 0; + free(fn); + fn = malloc(PATH_MAX); + cpos = SLASH; + continue; + }; + }; + } + + free(fn); + return 1; +fail: + free(fn); + return 0; +} + static void maybe_stdout_stderr_to_file() { /* TODO: make this a command line option. */ @@ -2243,6 +2476,8 @@ static void atexit_func() settings.usermap = NULL; usermap_destroy(settings.usermap_reverse); settings.usermap_reverse = NULL; + filefilter_destroy(settings.filefilter); + settings.filefilter = NULL; permchain_destroy(settings.permchain); settings.permchain = NULL; permchain_destroy(settings.create_permchain); @@ -2313,6 +2548,7 @@ int main(int argc, char *argv[]) char *map; char *map_passwd; char *map_group; + char *file_filter; char *read_rate; char *write_rate; char *create_for_user; @@ -2357,6 +2593,7 @@ int main(int argc, char *argv[]) OPT_OFFSET2("--map=%s", "map=%s", map, -1), OPT_OFFSET2("--map-passwd=%s", "map-passwd=%s", map_passwd, -1), OPT_OFFSET2("--map-group=%s", "map-group=%s", map_group, -1), + OPT_OFFSET2("--file-filter=%s", "file-filter=%s", file_filter, -1), OPT_OFFSET3("-n", "--no-allow-other", "no-allow-other", no_allow_other, -1), OPT_OFFSET2("--read-rate=%s", "read-rate=%s", read_rate, -1), @@ -2417,6 +2654,7 @@ int main(int argc, char *argv[]) settings.permchain = permchain_create(); settings.usermap = usermap_create(); settings.usermap_reverse = usermap_create(); + settings.filefilter = filefilter_create(); settings.read_limiter = NULL; settings.write_limiter = NULL; settings.new_uid = -1; @@ -2591,6 +2829,13 @@ int main(int argc, char *argv[]) } } + if (od.file_filter) { + if (!parse_file_filter(settings.filefilter,od.file_filter)) { + /* parse_file_filter() returned an error */ + return 1; + }; + } + if (od.forward_odirect) { #ifdef __linux__ settings.forward_odirect = 1; diff --git a/src/filter.c b/src/filter.c new file mode 100644 index 0000000..fe3cf91 --- /dev/null +++ b/src/filter.c @@ -0,0 +1,126 @@ +#include +#include +#include + +#include "filter.h" + +char *ffstatus_str_arr[] = { + [1+filefilter_status_found] = "Matching pattern found", + [1+filefilter_status_ok] = "Success", + [1+filefilter_status_notfound] = "Matching pattern not found", + [1+filefilter_status_incorrect_name] = "Incorrect matching pattern", + [1+filefilter_status_incorrect_mode] = "Incorrect file type", + [1+filefilter_status_addfail] = "Inserting pattern failed", + [1+filefilter_status_dupfound] = "Duplicate found" +}; + +struct FileFilter { + char **name; + FFType *type; + char *has_wildcard; +}; + +FileFilter *filefilter_create() +{ + FileFilter* f = (FileFilter*)malloc(sizeof(FileFilter)); + f->name = (char**)malloc(sizeof(char*)); + f->type = (FFType*)malloc(sizeof(FFType)); + f->has_wildcard = (char*)malloc(sizeof(char)); + f->name[0] = NULL; + f->type[0] = 0; + f->has_wildcard[0] = 0; + return f; +} + +void filefilter_destroy(FileFilter *f) +{ + int i = 0; + + while(f->name[i++] != NULL) + free(f->name[i]); + free(f->name); + free(f->type); + free(f->has_wildcard); + free(f); +} + +FFStatus filefilter_add(FileFilter *f, char *spec, FFType type) +{ + int pos = -1; + char *newname = NULL; + + if (strlen(spec) == 0) + return filefilter_status_incorrect_name; + + if (!(type&FFT_ANY)) + return filefilter_status_incorrect_mode; + + if (strchr(spec,'/')) + return filefilter_status_incorrect_name; + + newname = strdup(spec); + + while(f->name[++pos] != NULL) { + if (strcmp(f->name[pos],newname) == 0) + return filefilter_status_dupfound; + }; + f->name = (char**)realloc(f->name, sizeof(char*)*(pos+2)); + f->type = (FFType*)realloc(f->type, sizeof(FFType)*(pos+2)); + f->has_wildcard = (char*)realloc(f->has_wildcard, sizeof(char)*(pos+2)); + + f->name[pos+1] = NULL; + f->type[pos+1] = 0; + f->has_wildcard[pos+1] = 0; + + f->name[pos] = newname; + f->type[pos] = type; + + /* + * Here we try to opportunistically determine whether we have + * wildcard patterns specified in glob(7), to improve performance + * of simple specs contains exact names. + * + * Due to complexity of ranges syntax enclosed in square brackets, + * we don't fully check the spec for its presence, just consider this + * is a glob pattern if one of globbing (escaped or not) characters is found. + * + * This just leaves false-positives unoptimized, but fnmatch() should + * handle them correctly. + * + * TODO? + * */ + if (strpbrk(newname,"*?[]")) { + f->has_wildcard[pos] = 1; + } else { + f->has_wildcard[pos] = 0; + }; + + return filefilter_status_ok; +} + +FFStatus filefilter_find_match(FileFilter *f, char *fn, mode_t type) +{ + int pos = -1; + FFType type_b = MODET_TO_BITMASK(type); + + if (strlen(fn) == 0) { + return filefilter_status_incorrect_name; + }; + if (!(type_b&FFT_ANY)) { + return filefilter_status_incorrect_mode; + }; + + while(f->name[++pos] != NULL) { + if (!(f->type[pos]&type_b)) + continue; + if (f->has_wildcard[pos]) { + if (fnmatch(f->name[pos],fn,0) == 0) + return filefilter_status_found; + } else { + if (strcmp(f->name[pos],fn) == 0) + return filefilter_status_found; + }; + }; + + return filefilter_status_notfound; +} diff --git a/src/filter.h b/src/filter.h new file mode 100644 index 0000000..de05e18 --- /dev/null +++ b/src/filter.h @@ -0,0 +1,42 @@ +#ifndef INC_BINDFS_FILTER_H +#define INC_BINDFS_FILTER_H + +#include +#include + +#define MODET_TO_BITMASK(m) ( 1 << (m>>12) ) + +struct FileFilter; +typedef struct FileFilter FileFilter; + +typedef enum FileFilterStatus { + filefilter_status_found = -1, + filefilter_status_ok = 0, + filefilter_status_notfound = 1, + filefilter_status_incorrect_name = 2, + filefilter_status_incorrect_mode = 3, + filefilter_status_addfail = 4, + filefilter_status_dupfound = 5 +} FFStatus; + +extern char *ffstatus_str_arr[]; +#define ffstatus_str(s) ffstatus_str_arr[s+1] + +typedef enum FFType { + FFT_SCK = 1 << (S_IFSOCK>>12), + FFT_LNK = 1 << (S_IFLNK>>12), + FFT_REG = 1 << (S_IFREG>>12), + FFT_BLK = 1 << (S_IFBLK>>12), + FFT_DIR = 1 << (S_IFDIR>>12), + FFT_CHR = 1 << (S_IFCHR>>12), + FFT_PIP = 1 << (S_IFIFO>>12), + FFT_ANY = (FFT_SCK|FFT_LNK|FFT_REG|FFT_BLK|FFT_DIR|FFT_CHR|FFT_PIP) +} FFType; + +FileFilter *filefilter_create(); +void filefilter_destroy(FileFilter *f); + +FFStatus filefilter_add(FileFilter *f, char *spec, FFType type); +FFStatus filefilter_find_match(FileFilter *f, char *fn, mode_t type); + +#endif From 9970d77561cfd47ab8290957e6b5dd470dcf48c8 Mon Sep 17 00:00:00 2001 From: Nekun Date: Tue, 22 Feb 2022 06:05:47 +0000 Subject: [PATCH 2/8] Add manpage section for file filter --- src/bindfs.1 | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/bindfs.1 b/src/bindfs.1 index b681e44..4c74b72 100644 --- a/src/bindfs.1 +++ b/src/bindfs.1 @@ -142,6 +142,39 @@ them completely. See \fB\%PERMISSION \%SPECIFICATION\fP below for details. +.SH FILE FILTERING POLICY + +.TP +.B \-\-file-filter=\fIr:*.png/hiddenfile/d:dir2/...\fP, -o file-filter=... +Takes a slash-separated list of file names which will be hidden +in target filesystem. Files will not appear in directories, any +access operation will fail with \fIENOENT\fP, any write operation +(create, link, rename, ...) will fail with \fIEPERM\fP, even if +specified file not exist. If \fI\-\-resolve\-symlinks\fP specified, +symlinks referenced to a hidden files will also be hidden. + +Files can be specified as a POSIX wildcards (see \fBglob\fP(7) for +details). Optionally, only certain file types might be matched, as +specified with the following tokens which preceded filename and +terminated with a colon: + + \fBa\fP any file (this is a default) + \fBs\fP socket + \fBl\fP symbolic link + \fBr\fP regular file + \fBb\fP block device + \fBc\fP character device + \fBd\fP directory + \fBp\fP named pipe (FIFO) + e.g. \fIr:file/d:dir/slrbcp:notdir/...\fP + +If a type tokens not specified, any file types will be matched. +If a filename contains a colon, type token part must be specified +to distinguish specifier parts, e.g. \fIa:pci:0000:00:00/...\fP, +where \fIa\fP is an "any type" specifier and \fIpci:0000:00:00\fP is +a file name. + + .SH CHOWN/CHGRP POLICY The behaviour on chown/chgrp calls can be changed. By default they are passed through to the source directory even if bindfs is set to show From 390bdcbe163800e5381472a4de508d7c1501720b Mon Sep 17 00:00:00 2001 From: Nekun Date: Thu, 24 Feb 2022 01:59:20 +0000 Subject: [PATCH 3/8] bindfs.1: fix style in filtering section --- src/bindfs.1 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/bindfs.1 b/src/bindfs.1 index 4c74b72..1fa7e8c 100644 --- a/src/bindfs.1 +++ b/src/bindfs.1 @@ -148,9 +148,9 @@ See \fB\%PERMISSION \%SPECIFICATION\fP below for details. .B \-\-file-filter=\fIr:*.png/hiddenfile/d:dir2/...\fP, -o file-filter=... Takes a slash-separated list of file names which will be hidden in target filesystem. Files will not appear in directories, any -access operation will fail with \fIENOENT\fP, any write operation -(create, link, rename, ...) will fail with \fIEPERM\fP, even if -specified file not exist. If \fI\-\-resolve\-symlinks\fP specified, +access operation will fail with \fBENOENT\fP, any write operation +(create, link, rename, ...) will fail with \fBEPERM\fP, even if +specified file not exist. If \fB\-\-resolve\-symlinks\fP specified, symlinks referenced to a hidden files will also be hidden. Files can be specified as a POSIX wildcards (see \fBglob\fP(7) for @@ -168,8 +168,8 @@ terminated with a colon: \fBp\fP named pipe (FIFO) e.g. \fIr:file/d:dir/slrbcp:notdir/...\fP -If a type tokens not specified, any file types will be matched. -If a filename contains a colon, type token part must be specified +If a type tokens not specified, any file types will be matched. +If a filename contains a colon, type token part must be specified to distinguish specifier parts, e.g. \fIa:pci:0000:00:00/...\fP, where \fIa\fP is an "any type" specifier and \fIpci:0000:00:00\fP is a file name. From 6b922697c6b15f5db5dd8c744a964d4a2ee4412a Mon Sep 17 00:00:00 2001 From: Nekun Date: Thu, 24 Feb 2022 03:13:39 +0000 Subject: [PATCH 4/8] file-filter: fix pattern length limit --- src/bindfs.c | 10 +++++++--- src/filter.h | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/bindfs.c b/src/bindfs.c index 90b3f75..c5dce96 100644 --- a/src/bindfs.c +++ b/src/bindfs.c @@ -2324,7 +2324,11 @@ static int parse_file_filter(FileFilter *filter, char *spec) return 0; }; - fn = malloc(PATH_MAX); + /* Glob pattern might be longer than NAME_MAX, so don't use it + * as a limit. But in the other side, ARG_MAX on Linux is 2MB, + * which might be expensive to alloc on, e.g., embedded systems, + * so let's define some "reasonable" value */ + fn = malloc(FF_FN_LIMIT); fn_pos = 0; for (p = spec, cpos = SLASH; *p; p++) { @@ -2375,7 +2379,7 @@ static int parse_file_filter(FileFilter *filter, char *spec) }; if (cpos == FN) { - if (fn_pos >= PATH_MAX-1) { + if (fn_pos >= FF_FN_LIMIT-1) { fprintf(stderr,"Filename pattern too long\n"); goto fail; }; @@ -2393,7 +2397,7 @@ static int parse_file_filter(FileFilter *filter, char *spec) }; fn_pos = 0; free(fn); - fn = malloc(PATH_MAX); + fn = malloc(FF_FN_LIMIT); cpos = SLASH; continue; }; diff --git a/src/filter.h b/src/filter.h index de05e18..229bafa 100644 --- a/src/filter.h +++ b/src/filter.h @@ -4,6 +4,7 @@ #include #include +#define FF_FN_LIMIT 8192 #define MODET_TO_BITMASK(m) ( 1 << (m>>12) ) struct FileFilter; From eff68fdc7031f546102818fa7498ae5e5af77a64 Mon Sep 17 00:00:00 2001 From: Nekun Date: Thu, 24 Feb 2022 08:56:59 +0000 Subject: [PATCH 5/8] Initial implementation of --delete-filtered option --- src/bindfs.c | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/src/bindfs.c b/src/bindfs.c index c5dce96..9bcb0b9 100644 --- a/src/bindfs.c +++ b/src/bindfs.c @@ -65,6 +65,7 @@ #endif #include // For dirname(), basename() +#include // For nftw() in --delete-filtered impl #ifdef __linux__ #include @@ -134,6 +135,7 @@ static struct Settings { RateLimiter *write_limiter; FileFilter *filefilter; + int delete_filtered; enum CreatePolicy { CREATE_AS_USER, @@ -600,6 +602,14 @@ static int chown_new_file(const char *path, struct fuse_context *fc, int (*chown return 0; } +/* nftw() callback */ +int delete_recurse(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) +{ + int (*delfunc)(const char*) = S_ISDIR(sb->st_mode) ? rmdir : unlink; + + return delfunc(fpath) ? errno : 0; +} + static int delete_file(const char *path, int (*target_delete_func)(const char *)) { int res; char *real_path; @@ -618,6 +628,65 @@ static int delete_file(const char *path, int (*target_delete_func)(const char *) if (filefilter_check(real_path,0,NULL) == -1) return -errno; + /* Remove hidden with --file-filter files before rmdir(), to + * avoid ENOTEMPTY */ + if (settings.delete_filtered && main_delete_func == rmdir) { + DIR *dp = NULL; + struct dirent *de = malloc(sizeof(struct dirent)); + int e_cnt = 0, nf_cnt = 0; + char *e_path = strdup(real_path); + int e_path_len = strlen(e_path); + + if (lstat(real_path,&st) == -1) + goto delfil_out; + if (!S_ISDIR(st.st_mode)) + goto delfil_out; + + if ((dp = opendir(real_path))) { + while ((de = readdir(dp))) { + if (strcmp(de->d_name,".") == 0 || strcmp(de->d_name,"..") == 0) + continue; + e_cnt++; + if (filefilter_find_match(settings.filefilter,de->d_name,de->d_type << 12) == filefilter_status_notfound) + nf_cnt++; + } + + /* empty directory or non-filtered entries exists */ + if(!e_cnt || nf_cnt) + goto delfil_out; + + rewinddir(dp); + while ((de = readdir(dp))) { + /* should contain only filtered entries here, don't recheck */ + if (strcmp(de->d_name,".") == 0 || strcmp(de->d_name,"..") == 0) + continue; + + e_path = realloc(e_path,e_path_len+strlen(de->d_name)+2); + if (e_path[e_path_len-1] != '/') + e_path[e_path_len] = '/'; + strcpy(e_path+e_path_len+1,de->d_name); + if (de->d_type == DT_DIR) { + /* delete it recursively, don't follow symlinks */ + int ret = nftw(e_path,delete_recurse,8,FTW_DEPTH|FTW_MOUNT|FTW_PHYS); + if (ret < 0) + return -ENOTEMPTY; + else if (ret > 0) + return -ret; + } else { + if (unlink(e_path) != 0) + return -errno; + } + } + } else { + return -errno; + } +delfil_out: + if (dp) + closedir(dp); + free(de); + free(e_path); + } + if (settings.resolve_symlinks) { if (lstat(real_path, &st) == -1) { free(real_path); @@ -1835,7 +1904,8 @@ static void print_usage(const char *progname) " --create-with-perms=... Alter permissions of new files.\n" "\n" "File filtering policy:\n" - " --file-filter=*.png/... Hide files in underlying dir.\n" + " --file-filter=*.png/... Hide files in target filesystem.\n" + " --delete-filtered Remove hidden files in removing directory\n" "\n" "Chown policy:\n" " --chown-normal Try to chown the original files (the default).\n" @@ -1902,6 +1972,7 @@ enum OptionKey { OPTKEY_FUSE_VERSION, OPTKEY_CREATE_AS_USER, OPTKEY_CREATE_AS_MOUNTER, + OPTKEY_DELETE_FILTERED, OPTKEY_CHOWN_NORMAL, OPTKEY_CHOWN_IGNORE, OPTKEY_CHOWN_DENY, @@ -1991,6 +2062,10 @@ static int process_option(void *data, const char *arg, int key, settings.chmod_allow_x = 1; return 0; + case OPTKEY_DELETE_FILTERED: + settings.delete_filtered = 1; + return 0; + case OPTKEY_XATTR_NONE: settings.xattr_policy = XATTR_UNIMPLEMENTED; return 0; @@ -2617,6 +2692,8 @@ int main(int argc, char *argv[]) OPT2("--chgrp-ignore", "chgrp-ignore", OPTKEY_CHGRP_IGNORE), OPT2("--chgrp-deny", "chgrp-deny", OPTKEY_CHGRP_DENY), + OPT2("--delete-filtered", "delete-filtered", OPTKEY_DELETE_FILTERED), + OPT2("--chmod-normal", "chmod-normal", OPTKEY_CHMOD_NORMAL), OPT2("--chmod-ignore", "chmod-ignore", OPTKEY_CHMOD_IGNORE), OPT2("--chmod-deny", "chmod-deny", OPTKEY_CHMOD_DENY), @@ -2659,6 +2736,7 @@ int main(int argc, char *argv[]) settings.usermap = usermap_create(); settings.usermap_reverse = usermap_create(); settings.filefilter = filefilter_create(); + settings.delete_filtered = 0; settings.read_limiter = NULL; settings.write_limiter = NULL; settings.new_uid = -1; @@ -2833,11 +2911,16 @@ int main(int argc, char *argv[]) } } + /* Parse file filtering policy */ if (od.file_filter) { if (!parse_file_filter(settings.filefilter,od.file_filter)) { /* parse_file_filter() returned an error */ return 1; - }; + } + } + if (settings.delete_filtered && !od.file_filter) { + fprintf(stderr,"Error: --delete-filter must be used only with --file-filter specified\n"); + return 1; } if (od.forward_odirect) { From fd38dbe4d6a06d7dd0475b4fa095a91f79c9430c Mon Sep 17 00:00:00 2001 From: Nekun Date: Thu, 24 Feb 2022 09:02:42 +0000 Subject: [PATCH 6/8] remove extra semicolons --- src/bindfs.c | 28 ++++++++++++++-------------- src/filter.c | 12 ++++++------ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/bindfs.c b/src/bindfs.c index 9bcb0b9..c62b6fa 100644 --- a/src/bindfs.c +++ b/src/bindfs.c @@ -395,7 +395,7 @@ static int filefilter_check(const char *path, mode_t mode, mode_t *f_mode) } else { /* is it possible? */ errno = EIO; goto out; - }; + } } else { /* To protect hidden files from overwriting (say, we have * 'p:testfifo' policy and do 'mv regular_file testfifo'), @@ -403,7 +403,7 @@ static int filefilter_check(const char *path, mode_t mode, mode_t *f_mode) * not desired mode for new file */ which = mode ? OVERWRITE : EXISTING; mode = st.st_mode; - }; + } mode &= S_IFMT; @@ -415,7 +415,7 @@ static int filefilter_check(const char *path, mode_t mode, mode_t *f_mode) if (filefilter_find_match(settings.filefilter,fn,mode) == filefilter_status_found) { errno = (which == NEW || which == OVERWRITE) ? EPERM : ENOENT; goto out; - }; + } errno = 0; out: @@ -2397,7 +2397,7 @@ static int parse_file_filter(FileFilter *filter, char *spec) if (strlen(spec) < 1) { fprintf(stderr,"Pattern string not specified\n"); return 0; - }; + } /* Glob pattern might be longer than NAME_MAX, so don't use it * as a limit. But in the other side, ARG_MAX on Linux is 2MB, @@ -2424,8 +2424,8 @@ static int parse_file_filter(FileFilter *filter, char *spec) type = FFT_ANY; cpos = FN; continue; - }; - }; + } + } if (cpos == MODE) { switch(*p) { @@ -2442,7 +2442,7 @@ static int parse_file_filter(FileFilter *filter, char *spec) if (next == '/' || next == '\0') { fprintf(stderr,"Invalid syntax: matching pattern not specified after mode specifier\n"); goto fail; - }; + } cpos = FN; continue; break; @@ -2450,18 +2450,18 @@ static int parse_file_filter(FileFilter *filter, char *spec) fprintf(stderr,"Invalid syntax: '%c' is not a valid mode token\n",*p); goto fail; break; - }; - }; + } + } if (cpos == FN) { if (fn_pos >= FF_FN_LIMIT-1) { fprintf(stderr,"Filename pattern too long\n"); goto fail; - }; + } if (*p == '/' || *p == '\0') { fprintf(stderr,"Empty filename pattern\n"); goto fail; - }; + } fn[fn_pos++] = *p; if (next == '/' || next == '\0') { @@ -2469,14 +2469,14 @@ static int parse_file_filter(FileFilter *filter, char *spec) if ((ret = filefilter_add(settings.filefilter,fn,type)) != filefilter_status_ok) { fprintf(stderr,"Inserting filter spec '%s' failed: %s\n",fn,ffstatus_str(ret)); goto fail; - }; + } fn_pos = 0; free(fn); fn = malloc(FF_FN_LIMIT); cpos = SLASH; continue; - }; - }; + } + } } free(fn); diff --git a/src/filter.c b/src/filter.c index fe3cf91..bc1e9e5 100644 --- a/src/filter.c +++ b/src/filter.c @@ -63,7 +63,7 @@ FFStatus filefilter_add(FileFilter *f, char *spec, FFType type) while(f->name[++pos] != NULL) { if (strcmp(f->name[pos],newname) == 0) return filefilter_status_dupfound; - }; + } f->name = (char**)realloc(f->name, sizeof(char*)*(pos+2)); f->type = (FFType*)realloc(f->type, sizeof(FFType)*(pos+2)); f->has_wildcard = (char*)realloc(f->has_wildcard, sizeof(char)*(pos+2)); @@ -93,7 +93,7 @@ FFStatus filefilter_add(FileFilter *f, char *spec, FFType type) f->has_wildcard[pos] = 1; } else { f->has_wildcard[pos] = 0; - }; + } return filefilter_status_ok; } @@ -105,10 +105,10 @@ FFStatus filefilter_find_match(FileFilter *f, char *fn, mode_t type) if (strlen(fn) == 0) { return filefilter_status_incorrect_name; - }; + } if (!(type_b&FFT_ANY)) { return filefilter_status_incorrect_mode; - }; + } while(f->name[++pos] != NULL) { if (!(f->type[pos]&type_b)) @@ -119,8 +119,8 @@ FFStatus filefilter_find_match(FileFilter *f, char *fn, mode_t type) } else { if (strcmp(f->name[pos],fn) == 0) return filefilter_status_found; - }; - }; + } + } return filefilter_status_notfound; } From b6d7614cbd5da44736c8c52bfebb617e2e943bbc Mon Sep 17 00:00:00 2001 From: Nekun Date: Thu, 24 Feb 2022 09:17:01 +0000 Subject: [PATCH 7/8] bindfs.1: add section for --delete-filtered --- src/bindfs.1 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/bindfs.1 b/src/bindfs.1 index 1fa7e8c..a2495e7 100644 --- a/src/bindfs.1 +++ b/src/bindfs.1 @@ -174,6 +174,14 @@ to distinguish specifier parts, e.g. \fIa:pci:0000:00:00/...\fP, where \fIa\fP is an "any type" specifier and \fIpci:0000:00:00\fP is a file name. +.TP +.B \-\-delete\-filtered +When removing a directory, also transparently remove hidden with +\fB\-\-file\-filter\fP files in it. If this option not active, +such directories won't remove with \fBENOTEMPTY\fP error. If a directory +contains hidden directories, they will be deleted recursively, as with +\fBrm -rf\fP command. If \fB\-\-resolve\-symlinks\fP is active, hidden +files in resolved directories will not be removed. .SH CHOWN/CHGRP POLICY The behaviour on chown/chgrp calls can be changed. By default they are passed From 99d50ad39d014faa3683245f80317f54e4daadca Mon Sep 17 00:00:00 2001 From: Nekun Date: Thu, 24 Feb 2022 11:33:04 +0000 Subject: [PATCH 8/8] Forbid using --delete-filter with --delete-deny --- src/bindfs.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/bindfs.c b/src/bindfs.c index c62b6fa..21c96ce 100644 --- a/src/bindfs.c +++ b/src/bindfs.c @@ -2919,7 +2919,11 @@ int main(int argc, char *argv[]) } } if (settings.delete_filtered && !od.file_filter) { - fprintf(stderr,"Error: --delete-filter must be used only with --file-filter specified\n"); + fprintf(stderr, "Error: --delete-filtered must be used only with --file-filter specified\n"); + return 1; + } + if (settings.delete_filtered && settings.delete_deny) { + fprintf(stderr, "Error: --delete-filtered is incompatible with --delete-deny\n"); return 1; }