diff --git a/t/Makefile.am b/t/Makefile.am index 6243123..46a2816 100644 --- a/t/Makefile.am +++ b/t/Makefile.am @@ -58,7 +58,8 @@ TESTS = t000-mirror-read.t \ t201-event-err.t \ t202-event-deny.t \ t203-event-null.t \ - t204-event-allow.t + t204-event-allow.t \ + t205-event-locking.t if ENABLE_VFSAPI TESTS += t500-vfs-mirror-basic.t \ diff --git a/t/t203-event-null.t b/t/t203-event-null.t index 2d59002..8e8cf58 100755 --- a/t/t203-event-null.t +++ b/t/t203-event-null.t @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see http://www.gnu.org/licenses/ . -test_description='projfs file operation permission denial tests +test_description='projfs file operation permission null-denial tests Check that projfs file operation permission requests respond to denial responses caused by event handlers returning null. diff --git a/t/t205-event-locking.t b/t/t205-event-locking.t new file mode 100755 index 0000000..34c886f --- /dev/null +++ b/t/t205-event-locking.t @@ -0,0 +1,44 @@ +#!/bin/sh +# +# Copyright (C) 2018-2019 GitHub, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/ . + +test_description='projfs file operation locking tests + +Check that projfs file operation notification events are issued serially for a +given path. +' + +. ./test-lib.sh +. "$TEST_DIRECTORY"/test-lib-event.sh + +rm lock >/dev/null 2>/dev/null +projfs_start test_projfs_handlers source target --timeout 1 --lock-file lock || exit 1 + +# wait_mount will trigger a projection, so we need to reset it to empty +setfattr -n user.projection.empty -v 0x01 source + +test_expect_success 'test concurrent access does not trigger failure' ' + projfs_run_twice ls target +' + +projfs_stop || exit 1 + +test_expect_success 'check no event error messages' ' + test_must_be_empty test_projfs_handlers.err +' + +test_done + diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh index 8fdfead..8217db4 100644 --- a/t/test-lib-functions.sh +++ b/t/test-lib-functions.sh @@ -754,6 +754,25 @@ projfs_start () { trap projfs_stop EXIT } +# Run the given command twice in parallel, wait for both to complete, and +# return with 1 if at least one of the executions fails. +projfs_run_twice () { + "$@" & + pidA="$!" + "$@" & + pidB="$!" + + ret=0 + if ! wait $pidA; then + ret=1 + fi + if ! wait $pidB; then + ret=1 + fi + + return $ret +} + # Stop the projected filesystem command that was started by projfs_start() # and wait for its umount operation to be completed. projfs_stop () { diff --git a/t/test_common.c b/t/test_common.c index 62092f3..76803e6 100644 --- a/t/test_common.c +++ b/t/test_common.c @@ -101,9 +101,10 @@ static const struct retval vfsapi_retvals[] = { static const struct option all_long_opts[] = { { "help", no_argument, NULL, TEST_OPT_NUM_HELP }, - { "retval", required_argument, NULL, TEST_OPT_NUM_RETVAL}, - { "retval-file", required_argument, NULL, TEST_OPT_NUM_RETFILE}, - { "timeout", required_argument, NULL, TEST_OPT_NUM_TIMEOUT} + { "retval", required_argument, NULL, TEST_OPT_NUM_RETVAL }, + { "retval-file", required_argument, NULL, TEST_OPT_NUM_RETFILE }, + { "timeout", required_argument, NULL, TEST_OPT_NUM_TIMEOUT }, + { "lock-file", required_argument, NULL, TEST_OPT_NUM_LOCKFILE }, }; struct opt_usage { @@ -115,13 +116,15 @@ static const struct opt_usage all_opts_usage[] = { { NULL, 1 }, { "allow|deny|null|", 1 }, { "", 1 }, - { "", 1 } + { "", 1 }, + { "", 1 }, }; /* option values */ static int optval_retval; static const char *optval_retfile; static long int optval_timeout; +static const char *optval_lockfile; static unsigned int opt_set_flags = TEST_OPT_NONE; @@ -351,6 +354,11 @@ void test_parse_opts(int argc, char *const argv[], unsigned int opt_flags, opt_set_flags |= TEST_OPT_TIMEOUT; break; + case TEST_OPT_NUM_LOCKFILE: + optval_lockfile = optarg; + opt_set_flags |= TEST_OPT_LOCKFILE; + break; + case '?': if (optopt > 0) test_exit_error(argv[0], @@ -442,6 +450,12 @@ unsigned int test_get_opts(unsigned int opt_flags, ...) *l = optval_timeout; break; + case TEST_OPT_LOCKFILE: + s = va_arg(ap, const char**); + if (ret_flag != TEST_OPT_NONE) + *s = optval_lockfile; + break; + default: errx(EXIT_FAILURE, "unknown option flag: %u", opt_flag); diff --git a/t/test_common.h b/t/test_common.h index 19382ff..f7028b2 100644 --- a/t/test_common.h +++ b/t/test_common.h @@ -30,11 +30,13 @@ #define TEST_OPT_NUM_RETVAL 1 #define TEST_OPT_NUM_RETFILE 2 #define TEST_OPT_NUM_TIMEOUT 3 +#define TEST_OPT_NUM_LOCKFILE 4 #define TEST_OPT_HELP (0x0001 << TEST_OPT_NUM_HELP) #define TEST_OPT_RETVAL (0x0001 << TEST_OPT_NUM_RETVAL) #define TEST_OPT_RETFILE (0x0001 << TEST_OPT_NUM_RETFILE) #define TEST_OPT_TIMEOUT (0x0001 << TEST_OPT_NUM_TIMEOUT) +#define TEST_OPT_LOCKFILE (0x0001 << TEST_OPT_NUM_LOCKFILE) #define TEST_OPT_NONE 0x0000 #define TEST_OPT_VFSAPI 0x8000 // not a command-line option diff --git a/t/test_projfs_handlers.c b/t/test_projfs_handlers.c index 60937d2..ad6ebee 100644 --- a/t/test_projfs_handlers.c +++ b/t/test_projfs_handlers.c @@ -23,6 +23,7 @@ #include #include #include +#include #include "test_common.h" @@ -30,11 +31,13 @@ static int test_handle_event(struct projfs_event *event, const char *desc, int proj, int perm) { unsigned int opt_flags, ret_flags; - const char *retfile; - int ret; + const char *retfile, *lockfile = NULL; + int ret, timeout = 0, fd = 0, res; - opt_flags = test_get_opts((TEST_OPT_RETVAL | TEST_OPT_RETFILE), - &ret, &ret_flags, &retfile); + opt_flags = test_get_opts((TEST_OPT_RETVAL | TEST_OPT_RETFILE | + TEST_OPT_TIMEOUT | TEST_OPT_LOCKFILE), + &ret, &ret_flags, &retfile, &timeout, + &lockfile); if ((opt_flags & TEST_OPT_RETFILE) == TEST_OPT_NONE || (ret_flags & TEST_FILE_EXIST) != TEST_FILE_NONE) { @@ -57,9 +60,27 @@ static int test_handle_event(struct projfs_event *event, const char *desc, // TODO: hydrate file/dir based on projection list } + if (lockfile) { + fd = open(lockfile, (O_CREAT | O_EXCL | O_RDWR), 0600); + if (fd == -1 && errno == EEXIST) + return -EEXIST; + else if (fd == -1) + return -EINVAL; + } + + if (timeout) + sleep(timeout); + + if (lockfile) { + close(fd); + res = unlink(lockfile); + if (res == -1) + return -EINVAL; + } + if ((ret_flags & TEST_VAL_SET) == TEST_VAL_UNSET) ret = perm ? PROJFS_ALLOW : 0; - else if(!perm && ret > 0) + else if (!perm && ret > 0) ret = 0; return ret; @@ -87,7 +108,8 @@ int main(int argc, char *const argv[]) struct projfs_handlers handlers = { 0 }; test_parse_mount_opts(argc, argv, - (TEST_OPT_RETVAL | TEST_OPT_RETFILE), + (TEST_OPT_RETVAL | TEST_OPT_RETFILE | + TEST_OPT_TIMEOUT | TEST_OPT_LOCKFILE), &lower_path, &mount_path); handlers.handle_proj_event = &test_proj_event;