blob: e5be0ec9cc31f1ccdb54bca4b49f3f84279c10b0 [file] [log] [blame]
// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <atomic>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <threads.h>
#include <unistd.h>
#include <fbl/unique_fd.h>
#include <unittest/unittest.h>
#include <zircon/syscalls.h>
#include "filesystems.h"
#include "misc.h"
// Try repeatedly creating and removing a file within a directory,
// as fast as possible, in an attempt to trigger filesystem-internal
// threading races between creation and deletion of a file.
template <bool reuse_subdirectory>
bool TestInodeReuse(void) {
BEGIN_TEST;
ASSERT_EQ(mkdir("::reuse", 0755), 0);
DIR* d = opendir("::reuse");
ASSERT_NONNULL(d);
for (size_t i = 0; i < 1000; i++) {
ASSERT_EQ(mkdirat(dirfd(d), "foo", 0666), 0);
if (reuse_subdirectory) {
ASSERT_EQ(mkdirat(dirfd(d), "foo/bar", 0666), 0);
ASSERT_EQ(unlinkat(dirfd(d), "foo/bar", 0), 0);
}
ASSERT_EQ(unlinkat(dirfd(d), "foo", 0), 0);
}
ASSERT_EQ(closedir(d), 0);
ASSERT_EQ(rmdir("::reuse"), 0);
END_TEST;
}
// Return codes from helper threads
constexpr int kSuccess = 1;
constexpr int kFailure = -1;
constexpr int kUnexpectedFailure = -2;
using thrd_cb_t = int(void*);
// Launch some threads, and have them all execute callback
// 'cb'.
//
// It is expected that:
// - kSuccessCount threads will return "kSuccess"
// - ALL OTHER threads will return "kFailure"
//
// In any other condition, this helper fails.
// For example, returning "kUnexpectedFailure" from cb
// is an easy way to fail the entire test from a background thread.
template <size_t kNumThreads, size_t kSuccessCount>
bool thread_action_test(thrd_cb_t cb, void* arg = nullptr) {
BEGIN_HELPER;
static_assert(kNumThreads >= kSuccessCount, "Need more threads or less successes");
thrd_t threads[kNumThreads];
for (size_t i = 0; i < kNumThreads; i++) {
ASSERT_EQ(thrd_create(&threads[i], cb, arg), thrd_success);
}
// Join all threads first before checking whether they were successful. This way all threads
// will be cleaned up even if we encounter a failure.
int success[kNumThreads];
int result[kNumThreads];
for (size_t i = 0; i < kNumThreads; i++) {
success[i] = thrd_join(threads[i], &result[i]);
};
size_t success_count = 0;
for (size_t i = 0; i < kNumThreads; i++) {
ASSERT_EQ(success[i], thrd_success);
if (result[i] == kSuccess) {
success_count++;
ASSERT_LE(success_count, kSuccessCount, "Too many succeeding threads");
} else {
ASSERT_EQ(result[i], kFailure, "Unexpected return code from worker thread");
}
}
ASSERT_EQ(success_count, kSuccessCount, "Not enough succeeding threads");
END_HELPER;
}
constexpr size_t kIterCount = 10;
bool TestCreateUnlinkExclusive(void) {
BEGIN_TEST;
for (size_t i = 0; i < kIterCount; i++) {
ASSERT_TRUE((thread_action_test<10, 1>([](void* arg) {
fbl::unique_fd fd(open("::exclusive", O_RDWR | O_CREAT | O_EXCL));
if (fd) {
return close(fd.release()) == 0 ? kSuccess : kUnexpectedFailure;
} else if (errno == EEXIST) {
return kFailure;
}
return kUnexpectedFailure;
})));
ASSERT_TRUE((thread_action_test<10, 1>([](void* arg) {
if (unlink("::exclusive") == 0) {
return kSuccess;
} else if (errno == ENOENT) {
return kFailure;
}
return kUnexpectedFailure;
})));
}
END_TEST;
}
bool TestMkdirRmdirExclusive(void) {
BEGIN_TEST;
for (size_t i = 0; i < kIterCount; i++) {
ASSERT_TRUE((thread_action_test<10, 1>([](void* arg) {
if (mkdir("::exclusive", 0666) == 0) {
return kSuccess;
} else if (errno == EEXIST) {
return kFailure;
}
return kUnexpectedFailure;
})));
ASSERT_TRUE((thread_action_test<10, 1>([](void* arg) {
if (rmdir("::exclusive") == 0) {
return kSuccess;
} else if (errno == ENOENT) {
return kFailure;
}
return kUnexpectedFailure;
})));
}
END_TEST;
}
bool TestRenameExclusive(void) {
BEGIN_TEST;
for (size_t i = 0; i < kIterCount; i++) {
// Test case of renaming from a single source.
ASSERT_EQ(mkdir("::rename_start", 0666), 0);
ASSERT_TRUE((thread_action_test<10, 1>([](void* arg) {
if (rename("::rename_start", "::rename_end") == 0) {
return kSuccess;
} else if (errno == ENOENT) {
return kFailure;
}
return kUnexpectedFailure;
})));
ASSERT_EQ(rmdir("::rename_end"), 0);
// Test case of renaming from multiple sources at once,
// to a single destination
std::atomic<uint32_t> ctr{0};
ASSERT_TRUE((thread_action_test<10, 1>(
[](void* arg) {
auto ctr = reinterpret_cast<std::atomic<uint32_t>*>(arg);
char start[128];
snprintf(start, sizeof(start) - 1, "::rename_start_%u", ctr->fetch_add(1));
if (mkdir(start, 0666)) {
return kUnexpectedFailure;
}
// Give the target a child, so it cannot be overwritten as a target
char child[256];
snprintf(child, sizeof(child) - 1, "%s/child", start);
if (mkdir(child, 0666)) {
return kUnexpectedFailure;
}
if (rename(start, "::rename_end") == 0) {
return kSuccess;
} else if (errno == ENOTEMPTY || errno == EEXIST) {
return rmdir(child) == 0 && rmdir(start) == 0 ? kFailure : kUnexpectedFailure;
}
return kUnexpectedFailure;
},
&ctr)));
DIR* dir = opendir("::rename_end");
ASSERT_NONNULL(dir);
struct dirent* de;
while ((de = readdir(dir)) && de != nullptr) {
unlinkat(dirfd(dir), de->d_name, AT_REMOVEDIR);
}
ASSERT_EQ(closedir(dir), 0);
ASSERT_EQ(rmdir("::rename_end"), 0);
}
END_TEST;
}
bool TestRenameOverwrite(void) {
BEGIN_TEST;
for (size_t i = 0; i < kIterCount; i++) {
// Test case of renaming from multiple sources at once,
// to a single destination
std::atomic<uint32_t> ctr{0};
ASSERT_TRUE((thread_action_test<10, 10>(
[](void* arg) {
auto ctr = reinterpret_cast<std::atomic<uint32_t>*>(arg);
char start[128];
snprintf(start, sizeof(start) - 1, "::rename_start_%u", ctr->fetch_add(1));
if (mkdir(start, 0666)) {
return kUnexpectedFailure;
}
if (rename(start, "::rename_end") == 0) {
return kSuccess;
}
return kUnexpectedFailure;
},
&ctr)));
ASSERT_EQ(rmdir("::rename_end"), 0);
}
END_TEST;
}
bool TestLinkExclusive(void) {
BEGIN_TEST;
if (!test_info->supports_hardlinks) {
return true;
}
for (size_t i = 0; i < kIterCount; i++) {
fbl::unique_fd fd(open("::link_start", O_RDWR | O_CREAT | O_EXCL));
ASSERT_TRUE(fd);
ASSERT_EQ(close(fd.release()), 0);
ASSERT_TRUE((thread_action_test<10, 1>([](void* arg) {
if (link("::link_start", "::link_end") == 0) {
return kSuccess;
} else if (errno == EEXIST) {
return kFailure;
}
return kUnexpectedFailure;
})));
ASSERT_EQ(unlink("::link_start"), 0);
ASSERT_EQ(unlink("::link_end"), 0);
ASSERT_TRUE(check_remount());
}
END_TEST;
}
RUN_FOR_ALL_FILESYSTEMS(threading_tests, RUN_TEST_LARGE((TestInodeReuse<false>))
RUN_TEST_LARGE((TestInodeReuse<true>))
RUN_TEST_MEDIUM(TestCreateUnlinkExclusive)
RUN_TEST_MEDIUM(TestMkdirRmdirExclusive)
RUN_TEST_LARGE(TestRenameExclusive)
RUN_TEST_LARGE(TestRenameOverwrite)
RUN_TEST_LARGE(TestLinkExclusive))