--- /dev/null
+/*
+ * 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()