From: Lukasz Pawelczyk Date: Thu, 19 Mar 2020 15:54:04 +0000 (+0100) Subject: CheckProperDrop class unit tests X-Git-Tag: submit/tizen/20200410.113048~3 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=c0eca3d00ed82d17f8f83e3788a19b6ac2ba1c15;p=platform%2Fcore%2Fsecurity%2Fsecurity-manager.git CheckProperDrop class unit tests Change-Id: I1c867a319a5c14cf5ba67eb502e85505d00291c5 --- diff --git a/packaging/security-manager.spec b/packaging/security-manager.spec index 127509f8..753d4b60 100644 --- a/packaging/security-manager.spec +++ b/packaging/security-manager.spec @@ -87,6 +87,7 @@ Summary: Security manager unit test binaries Group: Security/Development Requires: boost-iostreams Requires: boost-test +Requires: boost-filesystem %description -n security-manager-tests Internal test for security manager implementation. diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 453b70a4..7fc1cf33 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -27,8 +27,12 @@ PKG_CHECK_MODULES(COMMON_DEP REQUIRED libtzplatform-config security-privilege-manager mount + libcap + libprocps ) +FIND_PACKAGE(Threads REQUIRED) + IF(DPL_WITH_DLOG) PKG_CHECK_MODULES(DLOG_DEP REQUIRED dlog) ENDIF(DPL_WITH_DLOG) @@ -60,6 +64,7 @@ SET(SM_TESTS_SOURCES ${SM_TEST_SRC}/test_privilege_db_migration.cpp ${SM_TEST_SRC}/test_smack-labels.cpp ${SM_TEST_SRC}/test_smack-rules.cpp + ${SM_TEST_SRC}/test_check_proper_drop.cpp ${DPL_PATH}/core/src/assert.cpp ${DPL_PATH}/core/src/colors.cpp ${DPL_PATH}/core/src/errno_string.cpp @@ -80,6 +85,7 @@ SET(SM_TESTS_SOURCES ${PROJECT_SOURCE_DIR}/src/common/filesystem.cpp ${PROJECT_SOURCE_DIR}/src/common/tzplatform-config.cpp ${PROJECT_SOURCE_DIR}/src/common/utils.cpp + ${PROJECT_SOURCE_DIR}/src/client/check-proper-drop.cpp ${GEN_PATH}/db.h ) @@ -131,6 +137,7 @@ INCLUDE_DIRECTORIES( ${COMMON_DEP_INCLUDE_DIRS} ${DLOG_DEP_INCLUDE_DIRS} ${SM_TEST_SRC} + ${Threads_INCLUDE_DIRS} ${PROJECT_SOURCE_DIR}/src/include ${PROJECT_SOURCE_DIR}/src/client/include ${PROJECT_SOURCE_DIR}/src/common/include @@ -152,8 +159,10 @@ ADD_DEPENDENCIES(${TARGET_SM_PERFORMANCE_TESTS} generate) TARGET_LINK_LIBRARIES(${TARGET_SM_TESTS} ${COMMON_DEP_LIBRARIES} ${DLOG_DEP_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} boost_unit_test_framework boost_iostreams + boost_filesystem -ldl -lcrypt ) diff --git a/test/test_check_proper_drop.cpp b/test/test_check_proper_drop.cpp new file mode 100644 index 00000000..b6235e94 --- /dev/null +++ b/test/test_check_proper_drop.cpp @@ -0,0 +1,576 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +/** + * @file test_check_proper_drop.cpp + * @author Lukasz Pawelczyk (l.pawelczyk@samsung.com) + * @version 1.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace SecurityManager; + + +namespace { + +std::mutex mutex; +std::condition_variable cond; +std::condition_variable count; +unsigned counter = 0; +bool cancel = false; + +const ::size_t LABEL_SIZE = 255; + +const std::string NO_LABEL = ""; +// should work with any label if not for onlycaps in Tizen: +const std::string OTHER_LABEL = "User::Shell"; +const std::string ROGUE_LABEL = "rogue_label"; + +using Caps = std::vector<::cap_value_t>; + +const Caps NO_CAPS; +const Caps SMACK_CAPS = { + CAP_MAC_ADMIN, + CAP_MAC_OVERRIDE +}; +const Caps ROGUE_CAPS = { + CAP_NET_ADMIN, + CAP_SYS_ADMIN +}; + +namespace fs = boost::filesystem; +namespace ch = std::chrono; + +/* In theory after thread has been joined it is guaranteed that the + * thread has been terminated. In practive its /proc/PID/task entries + * might still exist and it's sometimes even possible to get its + * status, caps, label, etc. This fixture class waits for all the + * threads to be cleaned up properly by the kernel. It's used by tests + * that require no other threads to exist in the moment the test + * starts. */ +class NoThreadsAssert +{ +public: + NoThreadsAssert() + { + const fs::path path = "/proc/self/task"; + if (!fs::is_directory(path)) { + ThrowMsg(CommonException::InternalError, + "Something is wrong with the /proc. Is it mounted?"); + } + + ch::steady_clock::time_point begin = ch::steady_clock::now(); + + for (;;) { + size_t n = number_of_entries(path); + if (n == 1) + break; + + ch::steady_clock::time_point now = ch::steady_clock::now(); + auto diff = ch::duration_cast(now - begin).count(); + if (diff > 3000) { + ThrowMsg(CommonException::InternalError, + "Timed out, process still has threads, test cannot proceed"); + } + std::this_thread::sleep_for(ch::milliseconds(10)); + } + } + +private: + size_t number_of_entries(const fs::path& dir) + { + size_t count = 0; + for (const fs::directory_entry& e: fs::directory_iterator(dir)) { + (void)e; + ++count; + } + return count; + } +}; + +class Smack +{ +public: + Smack(bool should_restore) + : restore(should_restore) + { + const ::pid_t tid = ::syscall(SYS_gettid); + path = "/proc/" + std::to_string(tid) + "/attr/current"; + } + + ~Smack() + { + if (saved_label.empty()) + return; + + if (restore) { + bool ret = set_self_label(saved_label); + if (!ret) { + std::cout + << "Failed to restore label, further tests might fail: " + << ::strerror(errno) << std::endl; + } + } + } + + bool set(const std::string& label) + { + if (saved_label.empty()) { + saved_label = get_self_label(); + if (saved_label.empty()) { + std::cout << "Failed to get current label: " + << ::strerror(errno) << std::endl; + return false; + } + } + + bool ret = set_self_label(label); + if (!ret) { + std::cout << "Failed to set new label: " + << ::strerror(errno) << std::endl; + return false; + } + + return true; + } + +private: + bool set_self_label(const std::string& label) + { + int fd = ::open(path.c_str(), O_WRONLY); + if (fd < 0) + return false; + + int ret = TEMP_FAILURE_RETRY(::write(fd, label.c_str(), label.length())); + + ::close(fd); + return ret == (::ssize_t)label.length(); + } + + std::string get_self_label() + { + char label[LABEL_SIZE + 1] = {0}; + + int fd = ::open(path.c_str(), O_RDONLY); + if (fd < 0) + return ""; + + TEMP_FAILURE_RETRY(::read(fd, label, LABEL_SIZE)); + + ::close(fd); + return label; + } + + std::string saved_label; + bool restore; + + std::string path; +}; + +class Privs +{ +public: + Privs(bool should_restore) + : saved_cap(NULL), + restore(should_restore) + {} + + ~Privs() + { + if (saved_cap == NULL) + return; + + if (restore) { + int ret = ::cap_set_proc(saved_cap); + if (ret != 0) { + std::cout + << "Failed to restore capabilities, further tests might fail: " + << ::strerror(errno) << std::endl; + } + } + + ::cap_free(saved_cap); + } + + bool drop(const std::vector& to_drop) + { + int ret; + cap_t new_cap = NULL; + + if (saved_cap == NULL) { + saved_cap = ::cap_get_proc(); + if (saved_cap == NULL) { + std::cout << "Failed to get current capabilities: " + << ::strerror(errno) << std::endl; + goto fail; + } + } + + new_cap = ::cap_dup(saved_cap); + if (new_cap == NULL) { + std::cout << "Failed to duplicate capabilities: " + << ::strerror(errno) << std::endl; + goto fail; + } + + ret = ::cap_set_flag(new_cap, CAP_EFFECTIVE, to_drop.size(), + to_drop.data(), CAP_CLEAR); + if (ret != 0) { + std::cout << "Failed to configure capabilities: " + << ::strerror(errno) << std::endl; + goto fail; + } + + ret = ::cap_set_proc(new_cap); + if (ret != 0) { + std::cout << "Failed to drop capabilities: " + << ::strerror(errno) << std::endl; + goto fail; + } + + ::cap_free(new_cap); + return true; + + fail: + ::cap_free(new_cap); + return false; + } + +private: + cap_t saved_cap; + bool restore; +}; + +struct Thread { + Thread(const std::string& l, const Caps& c) + : label(l), + caps(c), + ret(false), + thread(std::thread(&Thread::thread_routine, this)) + {} + + ~Thread() + { + if (thread.joinable()) { + thread.join(); + } + } + + Thread(const Thread&) = delete; + Thread(Thread&& other) = default; + + Thread& operator=(const Thread&) = delete; + Thread& operator=(Thread&& other) = default; + + void thread_routine() + { + ret = false; + std::string id = "Thread: (unknown) "; + + try { + const ::pid_t tid = ::syscall(SYS_gettid); + id = "Thread: " + std::to_string(tid) + "/" + label + " "; + + std::cout << id << "reporting for duty" << std::endl; + + if (!label.empty()) { + Smack smack(false); + if (!smack.set(label)) { + std::cout << id << "failed to set label" << std::endl; + return; + } + } + + if (!caps.empty()) { + Privs privs(false); + if (!privs.drop(caps)) { + std::cout << id << "failed to drop privs" << std::endl; + return; + } + } + + { + std::unique_lock lock(mutex); + counter++; + count.notify_one(); + cond.wait(lock, [] { return cancel; }); + } + + std::cout << id << "properly canceled" << std::endl; + ret = true; + } catch (const std::exception &e) { + std::cout << id << "thrown: " << e.what() << std::endl; + } + } + + std::string label; + Caps caps; + + bool ret; + std::thread thread; +}; + +struct ThreadInfo { + std::string label; + Caps caps; +}; + +class Threads { +public: + Threads(const std::vector& thread_infos) + { + /* Reset the global variables before anything else. They're + * used to synchronize threads' start and cancel */ + counter = 0; + cancel = false; + + /* This is important, it's not only an optimization. Without + * this the vector will reallocate and move its objects while + * threads are running. */ + threads.reserve(thread_infos.size()); + + for (auto &ti: thread_infos) { + threads.emplace_back(ti.label, ti.caps); + } + + /* wait for all threads to launch */ + std::unique_lock lock(mutex); + bool ret = count.wait_for(lock, ch::seconds(3), + [this] { return counter == threads.size(); }); + if (!ret) { + std::cout << "Timed out waiting for threads" << std::endl; + } + } + + ~Threads() + { + /* Emergency handle if cancel_threads_and_check_ret() was not + * called. Threads will join by themselves in their own + * destructors */ + if (!threads.empty()) { + cancel = true; + cond.notify_all(); + } + } + + bool cancel_threads_and_check_ret() + { + { + std::unique_lock lock(mutex); + cancel = true; + cond.notify_all(); + } + + bool all_true = true; + for (auto &t: threads) { + t.thread.join(); + if (!t.ret) { + std::cout << "Thread with label: " << t.label + << " returned false" << std::endl; + all_true = false; + } + } + + threads.clear(); + return all_true; + } + +private: + std::vector threads; +}; + +} // namespace + + +BOOST_AUTO_TEST_SUITE(CHECK_PROPER_DROP_TEST) + +/* Everything should succeed */ +BOOST_FIXTURE_TEST_CASE(T1700__no_threads, NoThreadsAssert) +{ + CheckProperDrop cpd; + bool ret; + + BOOST_REQUIRE_NO_THROW(ret = cpd.getThreads()); + BOOST_REQUIRE(ret == true); + + BOOST_REQUIRE_NO_THROW(ret = cpd.checkThreads()); + BOOST_REQUIRE(ret == true); +} + +/* Everything should succeed */ +BOOST_FIXTURE_TEST_CASE(T1701__threads_unmodified, NoThreadsAssert) +{ + CheckProperDrop cpd; + bool ret; + + Threads t({{NO_LABEL, NO_CAPS}, + {NO_LABEL, NO_CAPS}, + {NO_LABEL, NO_CAPS}}); + + BOOST_REQUIRE_NO_THROW(ret = cpd.getThreads()); + BOOST_REQUIRE(ret == true); + + BOOST_REQUIRE_NO_THROW(ret = cpd.checkThreads()); + BOOST_REQUIRE(ret == true); + + BOOST_REQUIRE(t.cancel_threads_and_check_ret() == true); +} + +/* Everything should succeed */ +BOOST_FIXTURE_TEST_CASE(T1702__threads_unprivileged, NoThreadsAssert) +{ + CheckProperDrop cpd; + bool ret; + Privs privs(true); + + Threads t({{NO_LABEL, SMACK_CAPS}, + {NO_LABEL, SMACK_CAPS}, + {NO_LABEL, SMACK_CAPS}}); + BOOST_REQUIRE(privs.drop(SMACK_CAPS) == true); + + BOOST_REQUIRE_NO_THROW(ret = cpd.getThreads()); + BOOST_REQUIRE(ret == true); + + BOOST_REQUIRE_NO_THROW(ret = cpd.checkThreads()); + BOOST_REQUIRE(ret == true); + + BOOST_REQUIRE(t.cancel_threads_and_check_ret() == true); +} + +/* Everything should succeed */ +BOOST_FIXTURE_TEST_CASE(T1704__threads_labeled, NoThreadsAssert) +{ + CheckProperDrop cpd; + bool ret; + Smack smack(true); + + Threads t({{OTHER_LABEL, NO_CAPS}, + {OTHER_LABEL, NO_CAPS}, + {OTHER_LABEL, NO_CAPS}}); + BOOST_REQUIRE(smack.set(OTHER_LABEL) == true); + + BOOST_REQUIRE_NO_THROW(ret = cpd.getThreads()); + BOOST_REQUIRE(ret == true); + + BOOST_REQUIRE_NO_THROW(ret = cpd.checkThreads()); + BOOST_REQUIRE(ret == true); + + BOOST_REQUIRE(t.cancel_threads_and_check_ret() == true); +} + +/* Everything should succeed */ +BOOST_FIXTURE_TEST_CASE(T1704__threads_labeled_unprivileged, NoThreadsAssert) +{ + CheckProperDrop cpd; + bool ret; + Smack smack(true); + Privs privs(true); + + Threads t({{OTHER_LABEL, SMACK_CAPS}, + {OTHER_LABEL, SMACK_CAPS}, + {OTHER_LABEL, SMACK_CAPS}}); + BOOST_REQUIRE(smack.set(OTHER_LABEL) == true); + BOOST_REQUIRE(privs.drop(SMACK_CAPS) == true); + + BOOST_REQUIRE_NO_THROW(ret = cpd.getThreads()); + BOOST_REQUIRE(ret == true); + + BOOST_REQUIRE_NO_THROW(ret = cpd.checkThreads()); + BOOST_REQUIRE(ret == true); + + BOOST_REQUIRE(t.cancel_threads_and_check_ret() == true); +} + +/* checkThreads should fail due to different label between main + * thread and one of the children */ +BOOST_FIXTURE_TEST_CASE(T1711__threads__rogue_label, NoThreadsAssert) +{ + CheckProperDrop cpd; + bool ret; + + Threads t({{ NO_LABEL, NO_CAPS}, + { NO_LABEL, NO_CAPS}, + {ROGUE_LABEL, NO_CAPS}}); + + BOOST_REQUIRE_NO_THROW(ret = cpd.getThreads()); + BOOST_REQUIRE(ret == true); + + BOOST_REQUIRE_NO_THROW(ret = cpd.checkThreads()); + BOOST_REQUIRE(ret == false); + + BOOST_REQUIRE(t.cancel_threads_and_check_ret() == true); +} + +/* checkThreads should fail due to different caps between main + * thread and one of the children */ +BOOST_FIXTURE_TEST_CASE(T1712__threads__rogue_caps, NoThreadsAssert) +{ + CheckProperDrop cpd; + bool ret; + + Threads t({{NO_LABEL, NO_CAPS}, + {NO_LABEL, NO_CAPS}, + {NO_LABEL, ROGUE_CAPS}}); + + BOOST_REQUIRE_NO_THROW(ret = cpd.getThreads()); + BOOST_REQUIRE(ret == true); + + BOOST_REQUIRE_NO_THROW(ret = cpd.checkThreads()); + BOOST_REQUIRE(ret == false); + + BOOST_REQUIRE(t.cancel_threads_and_check_ret() == true); +} + +/* getThreads should fail due to the main thread having no access to + * one of the children */ +BOOST_FIXTURE_TEST_CASE(T1721__threads_unprivileged__rogue_label, NoThreadsAssert) +{ + CheckProperDrop cpd; + bool ret; + Privs privs(true); + + Threads t({{ NO_LABEL, SMACK_CAPS}, + { NO_LABEL, SMACK_CAPS}, + {ROGUE_LABEL, SMACK_CAPS}}); + BOOST_REQUIRE(privs.drop(SMACK_CAPS) == true); + + BOOST_REQUIRE_NO_THROW(ret = cpd.getThreads()); + BOOST_REQUIRE(ret == false); + + BOOST_REQUIRE(t.cancel_threads_and_check_ret() == true); +} + +BOOST_AUTO_TEST_SUITE_END()