>From 9ffdaaacb7ba069dc4a01dfc731cca4ded2c7b2f Mon Sep 17 00:00:00 2001 From: Lukas Slebodnik Date: Tue, 23 Sep 2014 15:30:48 +0200 Subject: [PATCH 2/2] TESTS: Add test for mmap cache client crash Test create race condition between two threads when memory cache is invalidated(SSS_MC_HEADER_RECYCLED). Unit test for: https://fedorahosted.org/sssd/ticket/2445 --- Makefile.am | 17 ++ src/tests/cmocka/test_mc_client.c | 327 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 344 insertions(+) create mode 100644 src/tests/cmocka/test_mc_client.c diff --git a/Makefile.am b/Makefile.am index 6386d9aff8c80181acba7c8e0988a80e1ded6178..2eadc4960e56deb515bf24c68212c9dca8ff048f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -214,6 +214,7 @@ if HAVE_CMOCKA test_search_bases \ sdap-tests \ test_sysdb_views \ + mc_client_tests \ $(NULL) if BUILD_IFP @@ -2034,6 +2035,22 @@ sdap_tests_LDADD = \ libsss_test_common.la \ $(NULL) +mc_client_tests_SOURCES = \ + src/tests/cmocka/test_mc_client.c \ + $(NULL) +mc_client_tests_CFLAGS = \ + $(AM_CFLAGS) \ + $(NULL) +mc_client_tests_LDADD = \ + $(CMOCKA_LIBS) \ + $(POPT_LIBS) \ + $(LIBADD_DL) \ + libsss_debug.la \ + -lpthread \ + libnss_sss_tests.la \ + libsss_test_common.la \ + $(NULL) + if BUILD_IFP ifp_tests_SOURCES = \ $(TEST_MOCK_RESP_OBJ) \ diff --git a/src/tests/cmocka/test_mc_client.c b/src/tests/cmocka/test_mc_client.c new file mode 100644 index 0000000000000000000000000000000000000000..4347cce1969763aea85294bd74dae55c1c3f76ab --- /dev/null +++ b/src/tests/cmocka/test_mc_client.c @@ -0,0 +1,327 @@ +/* + Authors: + Lukas Slebodnik + + Copyright (C) 2014 Red Hat + + SSSD tests: Memory cache client Tests + + 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 3 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 . +*/ + +/* we need to define extra feature macros for RTLD_NEXT */ +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "util/io.h" +#include "sss_client/nss_mc.h" +#include "tests/cmocka/common_mock.h" + +enum { + CALL_REAL = 0, + CALL_THREAD1, + CALL_THREAD2, +}; + +/* Barrier variable */ +pthread_barrier_t barr; + +static struct sss_cli_mc_ctx mc_ctx = { false, -1, 0, NULL, 0, NULL, 0, NULL, + 0 }; + +/* + *************************************************************************** + * Mocked functions * + *************************************************************************** + */ + +int __real_sss_open_cloexec(const char *pathname, int flags, int *ret) +{ + typedef int (*sss_open_cloexec_ptr)(const char *, int, int *); + + sss_open_cloexec_ptr real_fun + = (sss_open_cloexec_ptr)dlsym(RTLD_NEXT, "sss_open_cloexec"); + + assert_non_null(real_fun); + return real_fun(pathname, flags, ret); +} + +int __wrap_sss_open_cloexec(const char *pathname, int flags, int *ret) +{ + const char *fake_path = sss_mock_ptr_type(const char *); + + return __real_sss_open_cloexec(fake_path, flags, ret); +} + +int sss_open_cloexec(const char *pathname, int flags, int *ret) +{ + return __wrap_sss_open_cloexec(pathname, flags, ret); +} + +/* Intercept header checking of memory cache */ +errno_t sss_nss_check_header_thread1(struct sss_cli_mc_ctx *ctx); +errno_t sss_nss_check_header_thread2(struct sss_cli_mc_ctx *ctx); + +errno_t __real_sss_nss_check_header(struct sss_cli_mc_ctx *ctx) +{ + typedef errno_t (*sss_nss_check_header_ptr)(struct sss_cli_mc_ctx * ctx); + + sss_nss_check_header_ptr real_fun + = (sss_nss_check_header_ptr)dlsym(RTLD_NEXT, "sss_nss_check_header"); + + assert_non_null(real_fun); + return real_fun(ctx); +} + +errno_t __wrap_sss_nss_check_header(struct sss_cli_mc_ctx *ctx) +{ + int id = sss_mock_type(int); + switch (id) { + case CALL_REAL: + /* execure real implementation */ + return __real_sss_nss_check_header(ctx); + break; + case CALL_THREAD1: + return sss_nss_check_header_thread1(ctx); + case CALL_THREAD2: + return sss_nss_check_header_thread2(ctx); + default: + /* should not be called */ + fail(); + break; + } + + return 0; +} + +errno_t sss_nss_check_header(struct sss_cli_mc_ctx *ctx) +{ + return __wrap_sss_nss_check_header(ctx); +} + +errno_t sss_nss_check_header_thread1(struct sss_cli_mc_ctx *ctx) +{ + errno_t ret; + int rc; + + ret = __real_sss_nss_check_header(ctx); + rc = pthread_barrier_wait(&barr); + if (rc != 0 && rc != PTHREAD_BARRIER_SERIAL_THREAD) { + fail(); + } + + assert_int_equal(ret, EINVAL); + + return ret; +} + +errno_t sss_nss_check_header_thread2(struct sss_cli_mc_ctx *ctx) +{ + errno_t ret; + int rc = pthread_barrier_wait(&barr); + if (rc != 0 && rc != PTHREAD_BARRIER_SERIAL_THREAD) { + fail(); + } + + /* + * 1 seconds is exaggerated, but it's better to be sure. + * 1 second is enough time to unmap file, close file and clear context + * With real race condition, crash happened after 1st thread unmapped file + * and 2nd thread want to test header of mmaped file + */ + sleep(1); + ret = __real_sss_nss_check_header(ctx); + assert_int_equal(ret, EINVAL); + + return ret; +} + +void *entry_point(void *arg) +{ + errno_t ret; + const errno_t expected_ret = (const errno_t)(intptr_t)arg; + + ret = sss_nss_mc_get_ctx("str_will_be_mocked", &mc_ctx); + assert_int_equal(ret, expected_ret); + + return NULL; +} + +/* + *************************************************************************** + * Memory cache related functions * + *************************************************************************** + */ + +const char *TEST_MC_CACHE = TEST_DIR "/dummy"; + +static void create_fake_cache(const char *file_name) +{ + int ret; + int fd; + ssize_t size; + struct sss_mc_header h; + + ret = unlink(file_name); + if (ret != 0 && errno != ENOENT) { + fail(); + } + + fd = creat(file_name, S_IRUSR | S_IWUSR); + assert_return_code(fd, errno); + + h.b1 = h.b2 = 0xf0000001; + h.major_vno = SSS_MC_MAJOR_VNO; + h.minor_vno = SSS_MC_MINOR_VNO; + h.status = SSS_MC_HEADER_ALIVE; + h.seed = time(NULL); + h.dt_size = 6000000; + h.ft_size = 6250; + h.ht_size = 400000; + h.data_table = MC_HEADER_SIZE; + h.free_table = h.data_table + MC_ALIGN64(h.dt_size); + h.hash_table = h.free_table + MC_ALIGN64(h.ft_size); + h.reserved = 0; + + size = write(fd, &h, sizeof(h)); + assert_int_equal(size, sizeof(h)); + + ret = ftruncate(fd, h.hash_table + MC_ALIGN64(h.ht_size)); + assert_return_code(ret, errno); +} + +static void set_header_status(struct sss_cli_mc_ctx *ctx, + const char *file_name, uint32_t status) +{ + int fd; + int ret; + const off_t STATUS_OFFSET = offsetof(struct sss_mc_header, status); + + fd = open(file_name, O_RDWR); + assert_return_code(fd, errno); + + ret = lseek(fd, STATUS_OFFSET, SEEK_SET); + assert_return_code(ret, errno); + + ret = write(fd, &status, sizeof(status)); + assert_return_code(ret, errno); + + ret = close(fd); + assert_int_equal(ret, 0); +} + +/* + *************************************************************************** + * Unit tests * + *************************************************************************** + */ + +static void test_client_reinit_race_condition(void **state) +{ + errno_t ret; + const int THREADS_COUNT = 2; + int i; + pthread_t thr[THREADS_COUNT]; + + create_fake_cache(TEST_MC_CACHE); + + will_return(__wrap_sss_open_cloexec, TEST_MC_CACHE); + will_return(__wrap_sss_nss_check_header, CALL_REAL); + + ret = sss_nss_mc_get_ctx("str_will_be_mocked", &mc_ctx); + assert_int_equal(ret, ERR_OK); + + will_return(__wrap_sss_nss_check_header, CALL_REAL); + ret = sss_nss_check_header(&mc_ctx); + assert_int_equal(ret, ERR_OK); + + set_header_status(&mc_ctx, TEST_MC_CACHE, SSS_MC_HEADER_RECYCLED); + + will_return(__wrap_sss_nss_check_header, CALL_REAL); + ret = sss_nss_check_header(&mc_ctx); + assert_int_equal(ret, EINVAL); + + will_return(__wrap_sss_nss_check_header, CALL_THREAD1); + will_return(__wrap_sss_nss_check_header, CALL_THREAD2); + sss_will_return_always(__wrap_sss_nss_check_header, 3); + + ret = pthread_barrier_init(&barr, NULL, THREADS_COUNT); + assert_int_equal(ret, 0); + + for (i = 0; i < THREADS_COUNT; ++i) { + ret = pthread_create(&thr[i], NULL, &entry_point, (void *)EINVAL); + assert_int_equal(ret, 0); + } + + /* cleanup */ + for (i = 0; i < THREADS_COUNT; ++i) { + ret = pthread_join(thr[i], NULL); + assert_int_equal(ret, 0); + } + + ret = pthread_barrier_destroy(&barr); + assert_int_equal(ret, 0); +} + +int main(int argc, const char *argv[]) +{ + int no_cleanup = 0; + poptContext pc; + int opt; + int ret; + struct poptOption long_options[] = { + SSSD_DEBUG_OPTS + {"no-cleanup", 'n', POPT_ARG_NONE, &no_cleanup, 0, + _("Do not delete the test database after a test run"), NULL }, + POPT_TABLEEND + }; + const UnitTest tests[] = { + unit_test(test_client_reinit_race_condition), + }; + + /* Set debug level to invalid value so we can deside if -d 0 was used. */ + debug_level = SSSDBG_INVALID; + + pc = poptGetContext(argv[0], argc, argv, long_options, 0); + while ((opt = poptGetNextOpt(pc)) != -1) { + switch (opt) { + default: + fprintf(stderr, "\nInvalid option %s: %s\n\n", + poptBadOption(pc, 0), poptStrerror(opt)); + poptPrintUsage(pc, stderr, 0); + return 1; + } + } + poptFreeContext(pc); + + DEBUG_CLI_INIT(debug_level); + + /* Even though normally the tests should clean up after themselves + * they might not after a failed run. Remove the old db to be sure */ + tests_set_cwd(); + + ret = run_tests(tests); + if (ret == 0 && !no_cleanup) { + //test_dom_suite_cleanup(TESTS_PATH, TEST_CONF_DB, TEST_SYSDB_FILE); + } + return ret; +} -- 2.1.0