diff options
Diffstat (limited to 'src/fsade.c')
-rw-r--r-- | src/fsade.c | 508 |
1 files changed, 508 insertions, 0 deletions
diff --git a/src/fsade.c b/src/fsade.c new file mode 100644 index 0000000..d56fb07 --- /dev/null +++ b/src/fsade.c @@ -0,0 +1,508 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "prelude.h" +#include "fsade.h" +#include "atomic.h" +#include "bfstd.h" +#include "bftw.h" +#include "dir.h" +#include "dstring.h" +#include "sanity.h" +#include <errno.h> +#include <fcntl.h> +#include <stddef.h> +#include <unistd.h> + +#if BFS_CAN_CHECK_ACL +# include <sys/acl.h> +#endif + +#if BFS_CAN_CHECK_CAPABILITIES +# include <sys/capability.h> +#endif + +#if BFS_CAN_CHECK_CONTEXT +# include <selinux/selinux.h> +#endif + +#if BFS_USE_SYS_EXTATTR_H +# include <sys/extattr.h> +#elif BFS_USE_SYS_XATTR_H +# include <sys/xattr.h> +#endif + +/** + * Many of the APIs used here don't have *at() variants, but we can try to + * emulate something similar if /proc/self/fd is available. + */ +attr(maybe_unused) +static const char *fake_at(const struct BFTW *ftwbuf) { + static atomic int proc_works = -1; + + dchar *path = NULL; + if (ftwbuf->at_fd == AT_FDCWD || load(&proc_works, relaxed) == 0) { + goto fail; + } + + path = dstrprintf("/proc/self/fd/%d/", ftwbuf->at_fd); + if (!path) { + goto fail; + } + + if (load(&proc_works, relaxed) < 0) { + if (xfaccessat(AT_FDCWD, path, F_OK) != 0) { + store(&proc_works, 0, relaxed); + goto fail; + } else { + store(&proc_works, 1, relaxed); + } + } + + if (dstrcat(&path, ftwbuf->at_path) != 0) { + goto fail; + } + + return path; + +fail: + dstrfree(path); + return ftwbuf->path; +} + +attr(maybe_unused) +static void free_fake_at(const struct BFTW *ftwbuf, const char *path) { + if (path != ftwbuf->path) { + dstrfree((dchar *)path); + } +} + +/** + * Check if an error was caused by the absence of support or data for a feature. + */ +attr(maybe_unused) +static bool is_absence_error(int error) { + // If the OS doesn't support the feature, it's obviously not enabled for + // any files + if (error == ENOTSUP) { + return true; + } + + // On Linux, ACLs and capabilities are implemented in terms of extended + // attributes, which report ENODATA/ENOATTR when missing + +#ifdef ENODATA + if (error == ENODATA) { + return true; + } +#endif + +#if defined(ENOATTR) && ENOATTR != ENODATA + if (error == ENOATTR) { + return true; + } +#endif + + // On at least FreeBSD and macOS, EINVAL is returned when the requested + // ACL type is not supported for that file + if (error == EINVAL) { + return true; + } + +#if __APPLE__ + // On macOS, ENOENT can also signal that a file has no ACLs + if (error == ENOENT) { + return true; + } +#endif + + return false; +} + +#if BFS_CAN_CHECK_ACL + +#if BFS_HAS_ACL_GET_FILE + +/** Unified interface for incompatible acl_get_entry() implementations. */ +static int bfs_acl_entry(acl_t acl, int which, acl_entry_t *entry) { +#if BFS_HAS_ACL_GET_ENTRY + int ret = acl_get_entry(acl, which, entry); +# if __APPLE__ + // POSIX.1e specifies a return value of 1 for success, but macOS returns 0 instead + return !ret; +# else + return ret; +# endif +#elif __DragonFly__ +# if !defined(ACL_FIRST_ENTRY) && !defined(ACL_NEXT_ENTRY) +# define ACL_FIRST_ENTRY 0 +# define ACL_NEXT_ENTRY 1 +# endif + + switch (which) { + case ACL_FIRST_ENTRY: + *entry = &acl->acl_entry[0]; + break; + case ACL_NEXT_ENTRY: + ++*entry; + break; + default: + errno = EINVAL; + return -1; + } + + acl_entry_t last = &acl->acl_entry[acl->acl_cnt]; + return *entry == last; +#else + errno = ENOTSUP; + return -1; +#endif +} + +/** Unified interface for acl_get_tag_type(). */ +attr(maybe_unused) +static int bfs_acl_tag_type(acl_entry_t entry, acl_tag_t *tag) { +#if BFS_HAS_ACL_GET_TAG_TYPE + return acl_get_tag_type(entry, tag); +#elif __DragonFly__ + *tag = entry->ae_tag; + return 0; +#else + errno = ENOTSUP; + return -1; +#endif +} + +/** Check if a POSIX.1e ACL is non-trivial. */ +static int bfs_check_posix1e_acl(acl_t acl, bool ignore_required) { + int ret = 0; + + acl_entry_t entry; + for (int status = bfs_acl_entry(acl, ACL_FIRST_ENTRY, &entry); + status > 0; + status = bfs_acl_entry(acl, ACL_NEXT_ENTRY, &entry)) + { +#if defined(ACL_USER_OBJ) && defined(ACL_GROUP_OBJ) && defined(ACL_OTHER) + if (ignore_required) { + acl_tag_t tag; + if (bfs_acl_tag_type(entry, &tag) != 0) { + ret = -1; + continue; + } + if (tag == ACL_USER_OBJ || tag == ACL_GROUP_OBJ || tag == ACL_OTHER) { + continue; + } + } +#endif + + ret = 1; + break; + } + + return ret; +} + +/** Check if an ACL of the given type is non-trivial. */ +static int bfs_check_acl_type(acl_t acl, acl_type_t type) { + if (type == ACL_TYPE_DEFAULT) { + // For directory default ACLs, any entries make them non-trivial + return bfs_check_posix1e_acl(acl, false); + } + +#if BFS_HAS_ACL_IS_TRIVIAL_NP + int trivial; + int ret = acl_is_trivial_np(acl, &trivial); + + // msan seems to be missing an interceptor for acl_is_trivial_np() + sanitize_init(&trivial); + + if (ret < 0) { + return -1; + } else if (trivial) { + return 0; + } else { + return 1; + } +#else + return bfs_check_posix1e_acl(acl, true); +#endif +} + +#endif // BFS_HAS_ACL_GET_FILE + +int bfs_check_acl(const struct BFTW *ftwbuf) { + if (ftwbuf->type == BFS_LNK) { + return 0; + } + + const char *path = fake_at(ftwbuf); + +#if BFS_HAS_ACL_TRIVIAL + int ret = acl_trivial(path); + int error = errno; +#elif BFS_HAS_ACL_GET_FILE + static const acl_type_t acl_types[] = { +# if __APPLE__ + // macOS gives EINVAL for either of the two standard ACL types, + // supporting only ACL_TYPE_EXTENDED + ACL_TYPE_EXTENDED, +# else + // The two standard POSIX.1e ACL types + ACL_TYPE_ACCESS, + ACL_TYPE_DEFAULT, +# endif + +# ifdef ACL_TYPE_NFS4 + ACL_TYPE_NFS4, +# endif + }; + + int ret = -1, error = 0; + for (size_t i = 0; i < countof(acl_types) && ret <= 0; ++i) { + acl_type_t type = acl_types[i]; + + if (type == ACL_TYPE_DEFAULT && ftwbuf->type != BFS_DIR) { + // ACL_TYPE_DEFAULT is supported only for directories, + // otherwise acl_get_file() gives EACCESS + continue; + } + + acl_t acl = acl_get_file(path, type); + if (!acl) { + error = errno; + if (is_absence_error(error)) { + ret = 0; + } + continue; + } + + ret = bfs_check_acl_type(acl, type); + error = errno; + acl_free(acl); + } +#endif + + free_fake_at(ftwbuf, path); + errno = error; + return ret; +} + +#else // !BFS_CAN_CHECK_ACL + +int bfs_check_acl(const struct BFTW *ftwbuf) { + errno = ENOTSUP; + return -1; +} + +#endif + +#if BFS_CAN_CHECK_CAPABILITIES + +int bfs_check_capabilities(const struct BFTW *ftwbuf) { + if (ftwbuf->type == BFS_LNK) { + return 0; + } + + int ret = -1, error; + const char *path = fake_at(ftwbuf); + + cap_t caps = cap_get_file(path); + if (!caps) { + error = errno; + if (is_absence_error(error)) { + ret = 0; + } + goto out_path; + } + + // TODO: Any better way to check for a non-empty capability set? + char *text = cap_to_text(caps, NULL); + if (!text) { + error = errno; + goto out_caps; + } + ret = text[0] ? 1 : 0; + + error = errno; + cap_free(text); +out_caps: + cap_free(caps); +out_path: + free_fake_at(ftwbuf, path); + errno = error; + return ret; +} + +#else // !BFS_CAN_CHECK_CAPABILITIES + +int bfs_check_capabilities(const struct BFTW *ftwbuf) { + errno = ENOTSUP; + return -1; +} + +#endif + +#if BFS_CAN_CHECK_XATTRS + +#if BFS_USE_SYS_EXTATTR_H + +/** Wrapper for extattr_list_{file,link}. */ +static ssize_t bfs_extattr_list(const char *path, enum bfs_type type, int namespace) { + if (type == BFS_LNK) { +#if BFS_HAS_EXTATTR_LIST_LINK + return extattr_list_link(path, namespace, NULL, 0); +#elif BFS_HAS_EXTATTR_GET_LINK + return extattr_get_link(path, namespace, "", NULL, 0); +#else + return 0; +#endif + } + +#if BFS_HAS_EXTATTR_LIST_FILE + return extattr_list_file(path, namespace, NULL, 0); +#elif BFS_HAS_EXTATTR_GET_FILE + // From man extattr(2): + // + // In earlier versions of this API, passing an empty string for the + // attribute name to extattr_get_file() would return the list of attributes + // defined for the target object. This interface has been deprecated in + // preference to using the explicit list API, and should not be used. + return extattr_get_file(path, namespace, "", NULL, 0); +#else + return 0; +#endif +} + +/** Wrapper for extattr_get_{file,link}. */ +static ssize_t bfs_extattr_get(const char *path, enum bfs_type type, int namespace, const char *name) { + if (type == BFS_LNK) { +#if BFS_HAS_EXTATTR_GET_LINK + return extattr_get_link(path, namespace, name, NULL, 0); +#else + return 0; +#endif + } + +#if BFS_HAS_EXTATTR_GET_FILE + return extattr_get_file(path, namespace, name, NULL, 0); +#else + return 0; +#endif +} + +#endif // BFS_USE_SYS_EXTATTR_H + +int bfs_check_xattrs(const struct BFTW *ftwbuf) { + const char *path = fake_at(ftwbuf); + ssize_t len; + +#if BFS_USE_SYS_EXTATTR_H + len = bfs_extattr_list(path, ftwbuf->type, EXTATTR_NAMESPACE_SYSTEM); + if (len <= 0) { + len = bfs_extattr_list(path, ftwbuf->type, EXTATTR_NAMESPACE_USER); + } +#elif __APPLE__ + int options = ftwbuf->type == BFS_LNK ? XATTR_NOFOLLOW : 0; + len = listxattr(path, NULL, 0, options); +#else + if (ftwbuf->type == BFS_LNK) { + len = llistxattr(path, NULL, 0); + } else { + len = listxattr(path, NULL, 0); + } +#endif + + int error = errno; + + free_fake_at(ftwbuf, path); + + if (len > 0) { + return 1; + } else if (len == 0 || is_absence_error(error)) { + return 0; + } else if (error == E2BIG) { + return 1; + } else { + errno = error; + return -1; + } +} + +int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name) { + const char *path = fake_at(ftwbuf); + ssize_t len; + +#if BFS_USE_SYS_EXTATTR_H + len = bfs_extattr_get(path, ftwbuf->type, EXTATTR_NAMESPACE_SYSTEM, name); + if (len < 0) { + len = bfs_extattr_get(path, ftwbuf->type, EXTATTR_NAMESPACE_USER, name); + } +#elif __APPLE__ + int options = ftwbuf->type == BFS_LNK ? XATTR_NOFOLLOW : 0; + len = getxattr(path, name, NULL, 0, 0, options); +#else + if (ftwbuf->type == BFS_LNK) { + len = lgetxattr(path, name, NULL, 0); + } else { + len = getxattr(path, name, NULL, 0); + } +#endif + + int error = errno; + + free_fake_at(ftwbuf, path); + + if (len >= 0) { + return 1; + } else if (is_absence_error(error)) { + return 0; + } else if (error == E2BIG) { + return 1; + } else { + errno = error; + return -1; + } +} + +#else // !BFS_CAN_CHECK_XATTRS + +int bfs_check_xattrs(const struct BFTW *ftwbuf) { + errno = ENOTSUP; + return -1; +} + +int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name) { + errno = ENOTSUP; + return -1; +} + +#endif + +char *bfs_getfilecon(const struct BFTW *ftwbuf) { +#if BFS_CAN_CHECK_CONTEXT + const char *path = fake_at(ftwbuf); + + char *con; + int ret; + if (ftwbuf->type == BFS_LNK) { + ret = lgetfilecon(path, &con); + } else { + ret = getfilecon(path, &con); + } + + if (ret >= 0) { + return con; + } else { + return NULL; + } +#else + errno = ENOTSUP; + return NULL; +#endif +} + +void bfs_freecon(char *con) { +#if BFS_CAN_CHECK_CONTEXT + freecon(con); +#endif +} |