summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTavian Barnes <tavianator@tavianator.com>2019-05-23 17:11:23 -0400
committerTavian Barnes <tavianator@tavianator.com>2019-05-24 09:00:50 -0400
commitd9b3196d6c8f4fa0e7d0a4771040762edaebb1ee (patch)
treef378d6955cf3cd6817a47e3a95284441552b221e
parent28bbaeac8058653a4a46ae439c37d251a550f4f9 (diff)
downloadbfs-d9b3196d6c8f4fa0e7d0a4771040762edaebb1ee.tar.xz
fsade: Refactor the POSIX.1e abstractions
Since we're going to want to abstract more things that aren't part of POSIX.1e (like xattrs) in a similar way, let's give this a more generic name. And while we're at it, give it some more precise error reporting, and add some tests.
-rw-r--r--Makefile2
-rw-r--r--color.c4
-rw-r--r--eval.c18
-rw-r--r--fsade.c232
-rw-r--r--fsade.h (renamed from posix1e.h)32
-rw-r--r--parse.c12
-rw-r--r--posix1e.c192
-rwxr-xr-xtests.sh52
-rw-r--r--tests/test_L_acl.out2
-rw-r--r--tests/test_L_capable.out2
-rw-r--r--tests/test_acl.out1
-rw-r--r--tests/test_capable.out1
12 files changed, 334 insertions, 216 deletions
diff --git a/Makefile b/Makefile
index 65dcf47..868a011 100644
--- a/Makefile
+++ b/Makefile
@@ -73,11 +73,11 @@ bfs: \
dstring.o \
eval.o \
exec.o \
+ fsade.o \
main.o \
mtab.o \
opt.o \
parse.o \
- posix1e.o \
printf.o \
spawn.o \
stat.o \
diff --git a/color.c b/color.c
index fd9949e..aefc3ff 100644
--- a/color.c
+++ b/color.c
@@ -17,7 +17,7 @@
#include "color.h"
#include "bftw.h"
#include "dstring.h"
-#include "posix1e.h"
+#include "fsade.h"
#include "stat.h"
#include "trie.h"
#include "util.h"
@@ -572,7 +572,7 @@ static const char *file_color(const struct colors *colors, const char *filename,
color = colors->setuid;
} else if (colors->setgid && (statbuf->mode & 02000)) {
color = colors->setgid;
- } else if (colors->capable && bfs_check_capabilities(ftwbuf)) {
+ } else if (colors->capable && bfs_check_capabilities(ftwbuf) > 0) {
color = colors->capable;
} else if (colors->executable && (statbuf->mode & 00111)) {
color = colors->executable;
diff --git a/eval.c b/eval.c
index 7a9a1aa..f07b380 100644
--- a/eval.c
+++ b/eval.c
@@ -25,8 +25,8 @@
#include "diag.h"
#include "dstring.h"
#include "exec.h"
+#include "fsade.h"
#include "mtab.h"
-#include "posix1e.h"
#include "printf.h"
#include "stat.h"
#include "trie.h"
@@ -160,14 +160,26 @@ bool eval_access(const struct expr *expr, struct eval_state *state) {
* -acl test.
*/
bool eval_acl(const struct expr *expr, struct eval_state *state) {
- return bfs_check_acl(state->ftwbuf);
+ int ret = bfs_check_acl(state->ftwbuf);
+ if (ret >= 0) {
+ return ret;
+ } else {
+ eval_report_error(state);
+ return false;
+ }
}
/**
* -capable test.
*/
bool eval_capable(const struct expr *expr, struct eval_state *state) {
- return bfs_check_capabilities(state->ftwbuf);
+ int ret = bfs_check_capabilities(state->ftwbuf);
+ if (ret >= 0) {
+ return ret;
+ } else {
+ eval_report_error(state);
+ return false;
+ }
}
/**
diff --git a/fsade.c b/fsade.c
new file mode 100644
index 0000000..7d796e0
--- /dev/null
+++ b/fsade.c
@@ -0,0 +1,232 @@
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> *
+ * *
+ * Permission to use, copy, modify, and/or distribute this software for any *
+ * purpose with or without fee is hereby granted. *
+ * *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
+ ****************************************************************************/
+
+#include "fsade.h"
+#include "bftw.h"
+#include "dstring.h"
+#include "util.h"
+#include <assert.h>
+#include <errno.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_ACL || BFS_CAN_CHECK_CAPABILITIES
+
+/**
+ * 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.
+ */
+static const char *fake_at(const struct BFTW *ftwbuf) {
+ static bool proc_works = true;
+ static bool proc_checked = false;
+
+ char *path = NULL;
+ if (!proc_works || ftwbuf->at_fd == AT_FDCWD) {
+ goto fail;
+ }
+
+ path = dstrprintf("/proc/self/fd/%d/", ftwbuf->at_fd);
+ if (!path) {
+ goto fail;
+ }
+
+ if (!proc_checked) {
+ proc_checked = true;
+ if (xfaccessat(AT_FDCWD, path, F_OK) != 0) {
+ proc_works = false;
+ goto fail;
+ }
+ }
+
+ if (dstrcat(&path, ftwbuf->at_path) != 0) {
+ goto fail;
+ }
+
+ return path;
+
+fail:
+ dstrfree(path);
+ return ftwbuf->path;
+}
+
+static void free_fake_at(const struct BFTW *ftwbuf, const char *path) {
+ if (path != ftwbuf->path) {
+ dstrfree((char *)path);
+ }
+}
+
+/**
+ * Check if an error was caused by the absence of support or data for a feature.
+ */
+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
+
+ return false;
+}
+
+#endif // BFS_CAN_CHECK_ACL || BFS_CAN_CHECK_CAPABILITIES
+
+#if BFS_CAN_CHECK_ACL
+
+/** Check if any ACLs of the given type are non-trivial. */
+static int bfs_check_acl_type(const char *path, acl_type_t type) {
+ acl_t acl = acl_get_file(path, type);
+ if (!acl) {
+ if (is_absence_error(errno)) {
+ return 0;
+ } else {
+ return -1;
+ }
+ }
+
+ int ret = 0;
+ acl_entry_t entry;
+ for (int status = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry);
+ status > 0;
+ status = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
+#if defined(ACL_USER_OBJ) && defined(ACL_GROUP_OBJ) && defined(ACL_OTHER)
+ acl_tag_t tag;
+ if (acl_get_tag_type(entry, &tag) != 0) {
+ continue;
+ }
+ if (tag != ACL_USER_OBJ && tag != ACL_GROUP_OBJ && tag != ACL_OTHER) {
+ ret = 1;
+ break;
+ }
+#else
+ ret = 1;
+ break;
+#endif
+ }
+
+ acl_free(acl);
+ return ret;
+}
+
+int bfs_check_acl(const struct BFTW *ftwbuf) {
+ if (ftwbuf->typeflag == BFTW_LNK) {
+ return 0;
+ }
+
+ const char *path = fake_at(ftwbuf);
+
+ int error = 0;
+ int ret = bfs_check_acl_type(path, ACL_TYPE_ACCESS);
+ if (ret < 0) {
+ error = errno;
+ }
+
+ if (ret <= 0 && ftwbuf->typeflag == BFTW_DIR) {
+ ret = bfs_check_acl_type(path, ACL_TYPE_DEFAULT);
+ if (ret < 0) {
+ error = errno;
+ }
+ }
+
+#ifdef ACL_TYPE_EXTENDED
+ if (ret <= 0) {
+ ret = bfs_check_acl_type(path, ACL_TYPE_EXTENDED);
+ if (ret < 0) {
+ error = errno;
+ }
+ }
+#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->typeflag == BFTW_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
diff --git a/posix1e.h b/fsade.h
index d3d74ff..d4e97c0 100644
--- a/posix1e.h
+++ b/fsade.h
@@ -15,34 +15,44 @@
****************************************************************************/
/**
- * The withdrawn POSIX.1e standard specified a security API for POSIX systems.
- * Although it was never ratified, many of its interfaces are widely deployed
- * in Unix-like systems. These functions wrap the POSIX.1e APIs if present,
- * to support things like Access Control Lists and Capabilities.
+ * A facade over (file)system features that are (un)implemented differently
+ * between platforms.
*/
-#ifndef BFS_POSIX1E_H
-#define BFS_POSIX1E_H
+#ifndef BFS_FSADE_H
+#define BFS_FSADE_H
#include "bftw.h"
#include "util.h"
#include <stdbool.h>
-#if !defined(BFS_HAS_POSIX1E_CAPABILITIES) && BFS_HAS_SYS_CAPABILITY && !__FreeBSD__
+#define BFS_CAN_CHECK_ACL BFS_HAS_SYS_ACL
+
+#if !defined(BFS_CAN_CHECK_CAPABILITIES) && BFS_HAS_SYS_CAPABILITY && !__FreeBSD__
# include <sys/capability.h>
# ifdef CAP_CHOWN
-# define BFS_HAS_POSIX1E_CAPABILITIES true
+# define BFS_CAN_CHECK_CAPABILITIES true
# endif
#endif
/**
* Check if a file has a non-trvial Access Control List.
+ *
+ * @param ftwbuf
+ * The file to check.
+ * @return
+ * 1 if it does, 0 if it doesn't, or -1 if an error occurred.
*/
-bool bfs_check_acl(const struct BFTW *ftwbuf);
+int bfs_check_acl(const struct BFTW *ftwbuf);
/**
* Check if a file has a non-trvial capability set.
+ *
+ * @param ftwbuf
+ * The file to check.
+ * @return
+ * 1 if it does, 0 if it doesn't, or -1 if an error occurred.
*/
-bool bfs_check_capabilities(const struct BFTW *ftwbuf);
+int bfs_check_capabilities(const struct BFTW *ftwbuf);
-#endif // BFS_POSIX1E_H
+#endif // BFS_FSADE_H
diff --git a/parse.c b/parse.c
index 4be493b..dbca7a9 100644
--- a/parse.c
+++ b/parse.c
@@ -28,8 +28,8 @@
#include "eval.h"
#include "exec.h"
#include "expr.h"
+#include "fsade.h"
#include "mtab.h"
-#include "posix1e.h"
#include "printf.h"
#include "stat.h"
#include "typo.h"
@@ -999,12 +999,11 @@ static struct expr *parse_access(struct parser_state *state, int flag, int arg2)
* Parse -acl.
*/
static struct expr *parse_acl(struct parser_state *state, int flag, int arg2) {
-#if BFS_HAS_SYS_ACL
+#if BFS_CAN_CHECK_ACL
struct expr *expr = parse_nullary_test(state, eval_acl);
if (expr) {
expr->cost = STAT_COST;
expr->probability = 0.00002;
- expr->ephemeral_fds = 1;
}
return expr;
#else
@@ -1057,12 +1056,11 @@ static struct expr *parse_time(struct parser_state *state, int field, int unit)
* Parse -capable.
*/
static struct expr *parse_capable(struct parser_state *state, int flag, int arg2) {
-#if BFS_HAS_POSIX1E_CAPABILITIES
+#if BFS_CAN_CHECK_CAPABILITIES
struct expr *expr = parse_nullary_test(state, eval_capable);
if (expr) {
expr->cost = STAT_COST;
expr->probability = 0.000002;
- expr->ephemeral_fds = 1;
}
return expr;
#else
@@ -2427,7 +2425,7 @@ static struct expr *parse_help(struct parser_state *state, int arg1, int arg2) {
cfprintf(cout, "${bld}Tests:${rs}\n\n");
-#if BFS_HAS_SYS_ACL
+#if BFS_CAN_CHECK_ACL
cfprintf(cout, " ${blu}-acl${rs}\n");
cfprintf(cout, " Find files with non-trivial Access Control Lists\n");
#endif
@@ -2438,7 +2436,7 @@ static struct expr *parse_help(struct parser_state *state, int arg1, int arg2) {
"modified\n");
cfprintf(cout, " ${blu}-${rs}[${blu}aBcm${rs}]${blu}time${rs} ${bld}[-+]N${rs}\n");
cfprintf(cout, " Find files ${blu}a${rs}ccessed/${blu}B${rs}irthed/${blu}c${rs}hanged/${blu}m${rs}odified ${bld}N${rs} days ago\n");
-#if BFS_HAS_POSIX1E_CAPABILITIES
+#if BFS_CAN_CHECK_CAPABILITIES
cfprintf(cout, " ${blu}-capable${rs}\n");
cfprintf(cout, " Match files with POSIX.1e capabilities set\n");
#endif
diff --git a/posix1e.c b/posix1e.c
deleted file mode 100644
index a898b39..0000000
--- a/posix1e.c
+++ /dev/null
@@ -1,192 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#include "posix1e.h"
-#include "bftw.h"
-#include "util.h"
-#include <fcntl.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-
-#if BFS_HAS_SYS_ACL
-# include <sys/acl.h>
-#endif
-
-#if BFS_HAS_POSIX1E_CAPABILITIES
-# include <sys/capability.h>
-#endif
-
-#if BFS_HAS_SYS_ACL || BFS_HAS_POSIX1E_CAPABILITIES
-
-static const char *open_path(const struct BFTW *ftwbuf, int *fd) {
-#ifdef O_PATH
- // The POSIX.1e APIS predate the *at() family of functions. We'd still
- // like to do something to avoid path re-traversals and limit races
- // though. Ideally we could just do openat(..., O_PATH) (since we may
- // not have read access) and pass that fd to something like cap_get_fd()
- // but that will fail since fgetxattr() needs read access to the file.
- // The workaround is to use O_PATH to open an fd and then pass
- // /proc/self/fd/<fd> to cap_get_path(). Inspired by
- // https://android.googlesource.com/platform/bionic/+/2825f10b7f61558c264231a536cf3affc0d84204
- int flags = O_PATH;
- if (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW) {
- flags |= O_NOFOLLOW;
- }
-
- *fd = openat(ftwbuf->at_fd, ftwbuf->at_path, flags);
- if (*fd < 0) {
- return NULL;
- }
-
- size_t size = strlen("/proc/self/fd/") + CHAR_BIT*sizeof(int) + 1;
- char *path = malloc(size);
- if (!path) {
- close(*fd);
- *fd = -1;
- return NULL;
- }
-
- snprintf(path, size, "/proc/self/fd/%d", *fd);
- return path;
-#else
- *fd = -1;
- return ftwbuf->path;
-#endif
-}
-
-static void close_path(const struct BFTW *ftwbuf, const char *path, int fd) {
- if (path && path != ftwbuf->path) {
- free((void *)path);
- }
- if (fd >= 0) {
- close(fd);
- }
-}
-
-#endif // BFS_HAS_SYS_ACL || BFS_HAS_POSIX1E_CAPABILITIES
-
-#if BFS_HAS_SYS_ACL
-
-/** Check if any ACLs of the given type are non-trivial. */
-static bool bfs_check_acl_type(const char *path, acl_type_t type) {
- acl_t acl = acl_get_file(path, type);
- if (!acl) {
- return false;
- }
-
- bool ret = false;
- acl_entry_t entry;
- for (int status = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry);
- status > 0;
- status = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
-#if defined(ACL_USER_OBJ) && defined(ACL_GROUP_OBJ) && defined(ACL_OTHER)
- acl_tag_t tag;
- if (acl_get_tag_type(entry, &tag) != 0) {
- continue;
- }
- if (tag != ACL_USER_OBJ && tag != ACL_GROUP_OBJ && tag != ACL_OTHER) {
- ret = true;
- break;
- }
-#else
- ret = true;
- break;
-#endif
- }
-
- acl_free(acl);
- return ret;
-}
-
-bool bfs_check_acl(const struct BFTW *ftwbuf) {
- if (ftwbuf->typeflag == BFTW_LNK) {
- return false;
- }
-
- int fd;
- const char *path = open_path(ftwbuf, &fd);
- if (!path) {
- return false;
- }
-
- bool ret = false;
- if (bfs_check_acl_type(path, ACL_TYPE_ACCESS)) {
- ret = true;
- } else if (bfs_check_acl_type(path, ACL_TYPE_DEFAULT)) {
- ret = true;
-#ifdef ACL_TYPE_EXTENDED
- } else if (bfs_check_acl_type(path, ACL_TYPE_EXTENDED)) {
- ret = true;
-#endif
- }
-
- close_path(ftwbuf, path, fd);
- return ret;
-}
-
-#else // !BFS_HAS_SYS_ACL
-
-bool bfs_check_acl(const struct BFTW *ftwbuf) {
- return false;
-}
-
-#endif
-
-#if BFS_HAS_POSIX1E_CAPABILITIES
-
-bool bfs_check_capabilities(const struct BFTW *ftwbuf) {
- bool ret = false;
-
- if (ftwbuf->typeflag == BFTW_LNK) {
- goto out;
- }
-
- int fd;
- const char *path = open_path(ftwbuf, &fd);
- if (!path) {
- goto out;
- }
-
- cap_t caps = cap_get_file(path);
- if (!caps) {
- goto out_close;
- }
-
- // TODO: Any better way to check for a non-empty capability set?
- char *text = cap_to_text(caps, NULL);
- if (!text) {
- goto out_free_caps;
- }
- ret = text[0];
-
- cap_free(text);
-out_free_caps:
- cap_free(caps);
-out_close:
- close_path(ftwbuf, path, fd);
-out:
- return ret;
-}
-
-#else // !BFS_HAS_POSIX1E_CAPABILITIES
-
-bool bfs_check_capabilities(const struct BFTW *ftwbuf) {
- return false;
-}
-
-#endif
diff --git a/tests.sh b/tests.sh
index 15e341d..c776ba4 100755
--- a/tests.sh
+++ b/tests.sh
@@ -279,6 +279,9 @@ bsd_tests=(
# Primaries
+ test_acl
+ test_L_acl
+
test_delete
test_depth_maxdepth_1
@@ -642,6 +645,9 @@ bfs_tests=(
)
sudo_tests=(
+ test_capable
+ test_L_capable
+
test_mount
test_xdev
@@ -2299,6 +2305,52 @@ function test_xtype_bind_mount() {
return $ret
}
+function test_acl() {
+ rm -rf scratch/*
+
+ if ! invoke_bfs scratch -acl 2>/dev/null; then
+ return 0
+ fi
+
+ touch scratch/{normal,acl}
+ setfacl -m "u:$(id -un):rw" scratch/acl
+ ln -s acl scratch/link
+
+ bfs_diff scratch -acl
+}
+
+function test_L_acl() {
+ rm -rf scratch/*
+
+ if ! invoke_bfs scratch -acl 2>/dev/null; then
+ return 0
+ fi
+
+ touch scratch/{normal,acl}
+ setfacl -m "u:$(id -un):rw" scratch/acl
+ ln -s acl scratch/link
+
+ bfs_diff -L scratch -acl
+}
+
+function test_capable() {
+ rm -rf scratch/*
+ touch scratch/{normal,capable}
+ sudo setcap all+ep scratch/capable
+ ln -s capable scratch/link
+
+ bfs_diff scratch -capable
+}
+
+function test_L_capable() {
+ rm -rf scratch/*
+ touch scratch/{normal,capable}
+ sudo setcap all+ep scratch/capable
+ ln -s capable scratch/link
+
+ bfs_diff -L scratch -capable
+}
+
BOL=
EOL='\n'
diff --git a/tests/test_L_acl.out b/tests/test_L_acl.out
new file mode 100644
index 0000000..1dae00a
--- /dev/null
+++ b/tests/test_L_acl.out
@@ -0,0 +1,2 @@
+scratch/acl
+scratch/link
diff --git a/tests/test_L_capable.out b/tests/test_L_capable.out
new file mode 100644
index 0000000..e5ba3c7
--- /dev/null
+++ b/tests/test_L_capable.out
@@ -0,0 +1,2 @@
+scratch/capable
+scratch/link
diff --git a/tests/test_acl.out b/tests/test_acl.out
new file mode 100644
index 0000000..ddf8446
--- /dev/null
+++ b/tests/test_acl.out
@@ -0,0 +1 @@
+scratch/acl
diff --git a/tests/test_capable.out b/tests/test_capable.out
new file mode 100644
index 0000000..78b5bd9
--- /dev/null
+++ b/tests/test_capable.out
@@ -0,0 +1 @@
+scratch/capable