CheckProperDrop class unit tests 10/228210/16
authorLukasz Pawelczyk <l.pawelczyk@samsung.com>
Thu, 19 Mar 2020 15:54:04 +0000 (16:54 +0100)
committerTomasz Swierczek <t.swierczek@samsung.com>
Fri, 10 Apr 2020 08:36:01 +0000 (08:36 +0000)
Change-Id: I1c867a319a5c14cf5ba67eb502e85505d00291c5

packaging/security-manager.spec
test/CMakeLists.txt
test/test_check_proper_drop.cpp [new file with mode: 0644]

index 127509f8940f8d816037b24fd888b6069ac270c8..753d4b60be7f47ec3ab6e5671eb377cf1dd2c38f 100644 (file)
@@ -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.
index 453b70a47c732d14166db386806a6dcb3ac62f31..7fc1cf334964b11e9755f5e38bf308ea4b585141 100644 (file)
@@ -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 (file)
index 0000000..b6235e9
--- /dev/null
@@ -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 <boost/test/unit_test.hpp>
+#include <boost/filesystem.hpp>
+#include <iostream>
+#include <vector>
+#include <string>
+#include <thread>
+#include <condition_variable>
+#include <mutex>
+#include <functional>
+#include <chrono>
+
+#include <sys/capability.h>
+#include <syscall.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <errno.h>
+
+#include <check-proper-drop.h>
+#include <dpl/exception.h>
+
+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<ch::milliseconds>(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<cap_value_t>& 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<std::mutex> 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<ThreadInfo>& 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<std::mutex> 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<std::mutex> 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<Thread> 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()