* @brief Implementation of proper privilege dropping check utilities, client side
*/
-#include <cassert>
-#include <cctype>
-#include <fstream>
#include <string>
-#include <sys/stat.h>
+#include <vector>
#include <check-proper-drop.h>
#include <dpl/exception.h>
#include <dpl/log/log.h>
#include <filesystem.h>
-#include <smack-labels.h>
namespace SecurityManager {
namespace CheckProperDrop {
DECLARE_EXCEPTION_TYPE(SecurityManager::Exception, DropError)
-class TaskData final {
- ::std::string m_label;
- ::std::string m_uid, m_gid, m_groups;
- ::std::string m_capInh, m_capPrm, m_capEff;
- ino_t m_nsIno[N_FLAG_BITS] = {};
-
- // Uid Gid Groups CapInh CapPrm CapEff
- static constexpr int N_WANTED_STATUS_LINES = 6;
-
- static bool fillIfHasId(::std::string &lval, const ::std::string &rval, const char *id)
- {
- if (rval.rfind(id, 0) != 0)
- return false;
- if (!lval.empty())
- ThrowMsg(DropError, "ambiguous lines (" << lval << "), (" << rval << ')');
- lval = rval;
- return true;
- }
-
- bool fillStatusLine(const std::string &line)
- {
- return fillIfHasId(m_uid, line, "Uid:")
- || fillIfHasId(m_gid, line, "Gid:")
- || fillIfHasId(m_groups, line, "Groups:")
- || fillIfHasId(m_capInh, line, "CapInh:")
- || fillIfHasId(m_capPrm, line, "CapPrm:")
- || fillIfHasId(m_capEff, line, "CapEff:");
- }
-
- void fillStatus(const ::std::string &taskRoot)
- {
- ::std::string statusPath = taskRoot + "status";
- ::std::ifstream stream(statusPath);
-
- size_t linesFilled = 0;
- for (::std::string line; ::std::getline(stream, line);)
- if (fillStatusLine(line))
- linesFilled++;
- if (linesFilled != N_WANTED_STATUS_LINES)
- ThrowMsg(DropError, '(' << statusPath << ") missing required information");
- }
-
- void fillNs(const ::std::string &taskRoot, BitFlags checkProperDropFlags)
- {
- for (size_t nsNameIdx = 0; nsNameIdx < N_FLAG_BITS; nsNameIdx++) {
- if (!(checkProperDropFlags & (1 << nsNameIdx)))
- continue;
-
- ::std::string nsPath = taskRoot + "ns/" + NS_NAMES[nsNameIdx];
- struct ::stat st;
- if (::stat(nsPath.c_str(), &st) != 0) {
- int err = errno;
- ThrowMsg(DropError, "stat failed (" << nsPath << ") errno=" << err);
- }
-
- m_nsIno[nsNameIdx] = st.st_ino;
- }
- }
-
- static void checkCaps(const ::std::string &capsLine)
- {
- assert(!::isspace(capsLine.back()));
- auto pos = capsLine.find_last_not_of("0");
- assert(pos != ::std::string::npos);
- if (!::isspace(capsLine[pos]) && capsLine[pos] != ':')
- ThrowMsg(DropError, "caps not zero (" << capsLine << ')');
- }
-
- template <class T>
- static void checkSame(const T &a, const T &b, const ::std::string &what)
- {
- if (a != b)
- ThrowMsg(DropError, "tasks differ [" << what << "]: (" << a << ") != (" << b << ")");
- }
-
-public:
- TaskData() {}
-
- void initialize(const ::std::string &taskId, BitFlags checkProperDropFlags) {
- m_label = SmackLabels::getSmackLabelFromPid(::std::stoul(taskId));
- ::std::string taskRoot = "/proc/self/task/" + taskId + "/";
- fillStatus(taskRoot);
- fillNs(taskRoot, checkProperDropFlags);
-
- checkCaps(m_capInh);
- checkCaps(m_capPrm);
- checkCaps(m_capEff);
- }
-
-
- TaskData(const ::std::string &taskId, BitFlags checkProperDropFlags)
- {
- initialize(taskId, checkProperDropFlags);
- }
-
-
- void checkSameAs(const TaskData &other) const {
- checkSame(m_label, other.m_label, "label");
- checkSame(m_uid, other.m_uid, "uid");
- checkSame(m_gid, other.m_gid, "gid");
- checkSame(m_groups, other.m_groups, "groups");
- for (size_t nsNameIdx = 0; nsNameIdx < N_FLAG_BITS; nsNameIdx++)
- checkSame(m_nsIno[nsNameIdx], other.m_nsIno[nsNameIdx], NS_NAMES[nsNameIdx]);
- }
-};
-
} // namespace
-void checkThreads(BitFlags checkProperDropFlags)
+void checkThreads(const std::vector<int> &synced_tids)
{
- LogDebug("checkProperDrop flags (" << static_cast<unsigned>(checkProperDropFlags) << ')');
-
- auto taskIds = FS::getSubDirectoriesFromDirectory("/proc/self/task", true);
- if (taskIds.empty())
- ThrowMsg(DropError, "no tasks found");
-
- TaskData lastTaskData;
- try {
- lastTaskData.initialize(taskIds.back(), checkProperDropFlags);
- } catch (...) {
- LogError("Offending taskId is: " << taskIds.back());
- throw;
- }
-
- taskIds.pop_back();
-
- for (const auto &taskId : taskIds)
- try {
- TaskData(taskId, checkProperDropFlags).checkSameAs(lastTaskData);
- } catch (...) {
- LogError("Offending taskId is: " << taskId);
- throw;
+ auto current_tids = FS::getSubDirectoriesFromDirectory("/proc/self/task", true);
+ for (auto &tid_s : current_tids) {
+ int tid = atoi(tid_s.c_str());
+ bool found = false;
+ for (auto &synced_tid : synced_tids)
+ if (synced_tid == tid) {
+ found = true;
+ break;
+ }
+ if (!found) {
+ LogError("New thread found that was not synchronized, must have not been dropped! TID: " << tid);
+ ThrowMsg(DropError, "New thread found that was not synchronized, must have not been dropped! TID: " << tid);
}
+ }
}
} // namespace CheckProperDrop
return ret < 0 ? -1 : 0;
}
-static inline int security_manager_sync_threads_internal(const std::string &app_label)
+static inline int security_manager_sync_threads_internal(const std::string &app_label, std::vector<int> &synced_tids)
{
static_assert(ATOMIC_INT_LOCK_FREE == 2, "std::atomic<int> is not always lock free");
LogDebug("number of threads to sync: " << files.size() - 1);
+ const pid_t cur_tid = Syscall::gettid();
+ synced_tids.push_back(cur_tid);
+
if (files.size() > 1) {
const pid_t cur_pid = getpid();
- const pid_t cur_tid = Syscall::gettid();
g_threads_count = files.size() - 1;
struct sigaction act;
int threads_gone = 0;
for (auto const &e : files) {
const int tid = atoi(e.c_str());
+
if (tid == static_cast<int>(cur_tid))
continue;
-
+ synced_tids.push_back(tid); // this will not add current TID (but its already added)
if (Syscall::tgkill(cur_pid, tid, SIGSETXID) < 0) {
const auto err = errno;
if (ESRCH == err) // thread already gone
PrepareAppFlags prepareAppFlags;
std::vector<gid_t> forbiddenGroups, allowedGroups;
std::vector<bool> privPathsStatusVector;
+ std::vector<int> synced_tids;
auto privilegePathMap = MountNS::getPrivilegePathMap(getuid());
int ret = prepareAppInitialSetupAndFetch(app_name, privilegePathMap, appLabel, pkgName, prepareAppFlags,
forbiddenGroups, allowedGroups, privPathsStatusVector);
return ret;
}
- ret = security_manager_sync_threads_internal(appLabel);
+ ret = security_manager_sync_threads_internal(appLabel, synced_tids);
if (ret != SECURITY_MANAGER_SUCCESS) {
LogError("Can't properly setup application threads (Smack label & capabilities) for application " << app_name);
return ret;
}
try {
- CheckProperDrop::checkThreads(prepareAppFlags >> PREPARE_APP_CPD_FLAG_SHIFT);
+ /*
+ * At this point in time, threads detected at security_manager_sync_threads_internal() stage should either:
+ * - be all synchronized (changed Smack label & capabilities)
+ * - be dead (if threads successfully ended)
+ * For those TIDs that are still alive, if their TIDs were on the list during call to sync_threads_internal(),
+ * we can be sure permissions are properly dropped - label_for_self_internal and cap_set_proc() must have finished
+ * with success for those threads, if we reached this point.
+ *
+ * If a thread is dead somehow, but was on the list - it doesn't pose security issue.
+ *
+ * A security issue exists only if a NEW thread was spawned that doesn't exist on the list from
+ * sync_threads_internal(). It can have improper Smack label & caps, so we cannot continue.
+ * Also, we can't reliably check contents of /proc to verify Smack labels, as it can contain
+ * stale information about processes Smack labels even after successful completion of set_label_for_self() function.
+ *
+ * Linux uses sequential TID/PID assignment, so theoretically TIDs can get re-used, but not in such small timeframe,
+ * so we're not taking this issue (TID reusage by kernel) into account here.
+ *
+ * Below call will throw an exception if a new thread will be detected thats not on the original list.
+ *
+ * Previous implementation that actually checked contents of /proc (Smack labels & caps) was dropped
+ * as it was verified experimentally that the proc filesystem could contain stale information about processes
+ * Smack labels/permissions even after successful completion of smack_set_label_for_self() & cap_set_proc() functions.
+ */
+ CheckProperDrop::checkThreads(synced_tids);
} catch (...) {
LogError("Privileges haven't been properly dropped for the whole process of application " << app_name);
/*
+++ /dev/null
-/*
- * Copyright (c) 2020 Samsung Electronics Co., Ltd. All rights reserved.
- *
- * This file is licensed under the terms of MIT License or the Apache License
- * Version 2.0 of your choice. See the LICENSE.MIT file for MIT license details.
- * See the LICENSE file or the notice below for Apache License Version 2.0
- * details.
- *
- * 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 <chrono>
-#include <condition_variable>
-#include <cstdio>
-#include <functional>
-#include <mutex>
-#include <string>
-#include <thread>
-
-#include <fcntl.h>
-#include <grp.h>
-#include <syscall.h>
-#include <sys/capability.h>
-#include <sys/fsuid.h>
-#include <sys/types.h>
-
-#include <boost/test/unit_test.hpp>
-#include <boost/filesystem.hpp>
-
-#include <check-proper-drop.h>
-#include <dpl/exception.h>
-
-#include <testmacros.h>
-
-using namespace SecurityManager;
-
-namespace {
-
-template <class IsError, class Result>
-Result logErrnoIfError(const IsError &isError, Result res,
- const char *prettyFunction, unsigned line, const char *expression)
-{
- if (isError(res))
- ::fprintf(stderr, "%s(%u) (%s) err (%m)\n", prettyFunction, line, expression);
- return res;
-}
-
-#define logErrnoIf(...) logErrnoIfError([](auto r) { return r; }, \
- (__VA_ARGS__), __PRETTY_FUNCTION__, __LINE__, #__VA_ARGS__)
-#define logErrnoIfNot(...) logErrnoIfError([](auto r) { return !r; }, \
- (__VA_ARGS__), __PRETTY_FUNCTION__, __LINE__, #__VA_ARGS__)
-#define logErrnoIfNegative(...) logErrnoIfError([](auto r) { return r < 0; }, \
- (__VA_ARGS__), __PRETTY_FUNCTION__, __LINE__, #__VA_ARGS__)
-
-constexpr uid_t APP_UID = 5001;
-constexpr gid_t APP_GID = 100;
-
-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;
- }
-};
-
-CheckProperDrop::BitFlags getCpdFlags()
-{
- static const auto cpdFlags = CheckProperDrop::computeFlags();
- BOOST_REQUIRE(cpdFlags >= 0);
- return cpdFlags;
-}
-
-bool isProperDrop(CheckProperDrop::BitFlags cpdFlags)
-{
- try {
- CheckProperDrop::checkThreads(cpdFlags);
- return true;
- } catch (...) {
- return false;
- }
-}
-
-bool dropCaps()
-{
- cap_t cap = logErrnoIfNot(::cap_init());
- if (!cap)
- return false;
- bool capsDropped = logErrnoIf(::cap_set_proc(cap)) == 0;
- ::cap_free(cap);
- return capsDropped;
-}
-
-void requireFork(const ::std::function<bool()> &childProcess)
-{
- pid_t pid = logErrnoIfNegative(::fork());
- BOOST_REQUIRE(pid >= 0);
- if (pid > 0) {
- int res;
- BOOST_REQUIRE(logErrnoIfNegative(::wait(&res)) == pid);
- BOOST_REQUIRE(WIFEXITED(res));
- BOOST_REQUIRE(WEXITSTATUS(res) == EXIT_SUCCESS);
- return;
- }
-
- // Boost test registers some handlers (ex. SIGABRT). If the child process
- // gets a SIGABRT, the handler causes the test suite to resume execution,
- // resulting in subsequent tests being executed twice (in the parent and
- // the child).
- for (auto signum : { SIGHUP, SIGINT, SIGQUIT, SIGILL, SIGTRAP, SIGABRT,
- SIGBUS, SIGFPE, SIGUSR1, SIGSEGV, SIGUSR2, SIGPIPE, SIGALRM, SIGTERM,
- SIGSTKFLT, SIGCHLD, SIGCONT, SIGTSTP, SIGTTIN, SIGTTOU, SIGURG,
- SIGXCPU, SIGXFSZ, SIGVTALRM, SIGPROF, SIGWINCH, SIGIO, SIGPWR, SIGSYS })
- ::signal(signum, SIG_DFL);
-
- // Catch all exceptions to hide them from boost test, thereby preventing
- // it from interfering. An exception merely means that the test failed.
- bool childProcessOk;
- try {
- childProcessOk = childProcess();
- } catch (...) {
- childProcessOk = false;
- }
-
- // bypass boost test atexit handlers
- ::_exit(childProcessOk ? EXIT_SUCCESS : EXIT_FAILURE);
-}
-
-void requireThread(const ::std::function<bool()> &alterThreadCreds,
- bool isProper = false, CheckProperDrop::BitFlags cpdFlags = getCpdFlags())
-{
- requireFork([&] {
- ::std::mutex mutex;
- ::std::condition_variable cond;
- bool kidOk = false;
- bool nowKid = true;
-
- auto enter = [&](::std::unique_lock<::std::mutex> &lock, bool isKid) {
- lock.lock();
- cond.wait(lock, [&]{ return isKid == nowKid; });
- };
- auto leave = [&](::std::unique_lock<::std::mutex> &lock, bool isKid) {
- nowKid = !isKid;
- lock.unlock();
- cond.notify_one();
- };
-
- ::std::thread kid([&] {
- kidOk = alterThreadCreds();
-
- ::std::unique_lock<::std::mutex> lock(mutex);
- leave(lock, true); // creds altered, signal parent
- enter(lock, true); // parent done checking, terminate
- });
-
- ::std::unique_lock<::std::mutex> lock(mutex, ::std::defer_lock);
- enter(lock, false); // kid has altered creds
- bool ok = dropCaps() && isProperDrop(cpdFlags) == isProper;
-
- leave(lock, false); // check done, signal kid
- kid.join();
-
- return ok && kidOk;
- });
-}
-
-void requireThreadNs(const char *ns, int unshareFlag)
-{
- size_t nsIdx = 0;
- for (; nsIdx < CheckProperDrop::N_FLAG_BITS; nsIdx++)
- if (!strcmp(ns, CheckProperDrop::NS_NAMES[nsIdx]))
- break;
- BOOST_REQUIRE(nsIdx < CheckProperDrop::N_FLAG_BITS);
- auto cpdFlags = getCpdFlags();
- auto nsBit = 1 << nsIdx;
- auto unshareThenDropCaps = [&] {
- return logErrnoIf(::unshare(unshareFlag)) == 0 && dropCaps();
- };
-
- if (cpdFlags & nsBit)
- requireThread(unshareThenDropCaps);
- else
- ::fprintf(stderr, "%s namespace not included in checkProperDrop flags, "
- "omitting failure test\n", ns);
-
- requireThread(unshareThenDropCaps, true, cpdFlags & ~nsBit);
-}
-
-} // namespace
-
-
-BOOST_AUTO_TEST_SUITE(CHECK_PROPER_DROP_TEST)
-
-POSITIVE_FIXTURE_TEST_CASE(T1700__no_threads, NoThreadsAssert)
-{
- auto cpdFlags = getCpdFlags();
- requireFork([&] {
- return dropCaps() && isProperDrop(cpdFlags);
- });
-}
-
-// This may happen when a rogue thread:
-// * Survives sync_threads_internal due to a lucky interleaving.
-// * Uses capset() to restore main thread capabilities to nonzero.
-// * Terminates before checkProperDrop() starts.
-NEGATIVE_FIXTURE_TEST_CASE(T1701__no_threads_privileged, NoThreadsAssert)
-{
- auto cpdFlags = getCpdFlags();
- requireFork([&] {
- return !isProperDrop(cpdFlags);
- });
-}
-
-POSITIVE_FIXTURE_TEST_CASE(T1702__thread_unmodified, NoThreadsAssert)
-{
- requireThread(dropCaps, true);
-}
-
-// Caps not dropped.
-NEGATIVE_FIXTURE_TEST_CASE(T1703__thread_privileged, NoThreadsAssert)
-{
- requireThread([]{ return true; });
-}
-
-// Wrong label.
-NEGATIVE_FIXTURE_TEST_CASE(T1704__thread_mislabeled, NoThreadsAssert)
-{
- requireThread([]{
- int fd = logErrnoIfNegative(TEMP_FAILURE_RETRY(::open(
- ("/proc/" + ::std::to_string(syscall(SYS_gettid)) + "/attr/current").c_str(),
- O_WRONLY)));
- if (fd < 0)
- return false;
- static constexpr char label[] = "rogueLabel";
- static constexpr auto labelLen = ::strlen(label);
- auto written = logErrnoIfNegative(TEMP_FAILURE_RETRY(::write(fd, label, labelLen)));
- ::close(fd);
- if (written != labelLen) {
- if (written >= 0)
- ::fprintf(stderr, "short smack label write, written=%u\n", unsigned(written));
- return false;
- }
- return dropCaps();
- });
-}
-
-// Wrong mnt ns, failure iff ns included in CheckProperDrop::computeFlags().
-NEGATIVE_FIXTURE_TEST_CASE(T1705__thread_ns_mnt, NoThreadsAssert)
-{
- requireThreadNs("mnt", CLONE_NEWNS);
-}
-
-NEGATIVE_FIXTURE_TEST_CASE(T1706__thread_ns_ipc, NoThreadsAssert)
-{
- requireThreadNs("ipc", CLONE_NEWIPC);
-}
-
-// Wrong net ns, failure iff ns included in CheckProperDrop::computeFlags().
-NEGATIVE_FIXTURE_TEST_CASE(T1707__thread_ns_net, NoThreadsAssert)
-{
- requireThreadNs("net", CLONE_NEWNET);
-}
-
-// Wrong uts ns, failure iff ns included in CheckProperDrop::computeFlags().
-NEGATIVE_FIXTURE_TEST_CASE(T1708__thread_ns_uts, NoThreadsAssert)
-{
- requireThreadNs("uts", CLONE_NEWUTS);
-}
-
-// Wrong uid.
-NEGATIVE_FIXTURE_TEST_CASE(T1709__thread_uid, NoThreadsAssert)
-{
- requireThread([]{
- ::setfsuid(APP_UID);
- return dropCaps();
- });
-}
-
-// Wrong gid.
-NEGATIVE_FIXTURE_TEST_CASE(T1710__thread_gid, NoThreadsAssert)
-{
- requireThread([]{
- ::setfsgid(APP_GID);
- return dropCaps();
- });
-}
-
-// Wrong supplementary groups.
-NEGATIVE_FIXTURE_TEST_CASE(T1711__thread_groups, NoThreadsAssert)
-{
- requireThread([]{
- ::gid_t dummy;
- // Use setgroups syscall directly to affect this thread only
- // (see man getgroups(2), NOTES, C library/kernel differences)
- return logErrnoIf(static_cast<int>(::syscall(SYS_setgroups, 0, &dummy))) == 0
- && dropCaps();
- });
-}
-
-BOOST_AUTO_TEST_SUITE_END()