diff options
author | jln@chromium.org <jln@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-11-05 18:56:13 +0000 |
---|---|---|
committer | jln@chromium.org <jln@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-11-05 18:56:13 +0000 |
commit | 486efc8695c31bd2084db07bd47f104451553cc6 (patch) | |
tree | 44fe48d534c506bdf3c91897ad223d7fe62f785e /sandbox | |
parent | 2fb8a619c491b30570f037a17e4e0d58f274ebc5 (diff) | |
download | chromium_src-486efc8695c31bd2084db07bd47f104451553cc6.zip chromium_src-486efc8695c31bd2084db07bd47f104451553cc6.tar.gz chromium_src-486efc8695c31bd2084db07bd47f104451553cc6.tar.bz2 |
Linux: add basic unprivileged namespace support.
The Credentials class now has basic support for unprivileged namespaces.
BUG=312380
R=jorgelo@chromium.org
Review URL: https://codereview.chromium.org/54643010
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@233041 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'sandbox')
-rw-r--r-- | sandbox/linux/services/credentials.cc | 144 | ||||
-rw-r--r-- | sandbox/linux/services/credentials.h | 26 | ||||
-rw-r--r-- | sandbox/linux/services/credentials_unittest.cc | 140 | ||||
-rw-r--r-- | sandbox/linux/tests/main.cc | 3 |
4 files changed, 303 insertions, 10 deletions
diff --git a/sandbox/linux/services/credentials.cc b/sandbox/linux/services/credentials.cc index a6387d2..0af5a42b 100644 --- a/sandbox/linux/services/credentials.cc +++ b/sandbox/linux/services/credentials.cc @@ -4,11 +4,17 @@ #include "sandbox/linux/services/credentials.h" +#include <errno.h> #include <stdio.h> #include <sys/capability.h> +#include <unistd.h> #include "base/basictypes.h" +#include "base/bind.h" #include "base/logging.h" +#include "base/strings/string_number_conversions.h" +#include "base/template_util.h" +#include "base/threading/thread.h" namespace { @@ -32,6 +38,101 @@ struct CapTextFreeDeleter { // Wrapper to manage the result from libcap2's cap_from_text(). typedef scoped_ptr<char, CapTextFreeDeleter> ScopedCapText; +struct FILECloser { + inline void operator()(FILE* f) const { + DCHECK(f); + PCHECK(0 == fclose(f)); + } +}; + +// Don't use ScopedFILE in base/file_util.h since it doesn't check fclose(). +// TODO(jln): fix base/. +typedef scoped_ptr<FILE, FILECloser> ScopedFILE; + +COMPILE_ASSERT((base::is_same<uid_t, gid_t>::value), UidAndGidAreSameType); +// generic_id_t can be used for either uid_t or gid_t. +typedef uid_t generic_id_t; + +// Write a uid or gid mapping from |id| to |id| in |map_file|. +bool WriteToIdMapFile(const char* map_file, generic_id_t id) { + ScopedFILE f(fopen(map_file, "w")); + PCHECK(f); + const uid_t inside_id = id; + const uid_t outside_id = id; + int num = fprintf(f.get(), "%d %d 1\n", inside_id, outside_id); + if (num < 0) return false; + // Manually call fflush() to catch permission failures. + int ret = fflush(f.get()); + if (ret) { + VLOG(1) << "Could not write to id map file"; + return false; + } + return true; +} + +// Checks that the set of RES-uids and the set of RES-gids have +// one element each and return that element in |resuid| and |resgid| +// respectively. It's ok to pass NULL as one or both of the ids. +bool GetRESIds(uid_t* resuid, gid_t* resgid) { + uid_t ruid, euid, suid; + gid_t rgid, egid, sgid; + PCHECK(getresuid(&ruid, &euid, &suid) == 0); + PCHECK(getresgid(&rgid, &egid, &sgid) == 0); + const bool uids_are_equal = (ruid == euid) && (ruid == suid); + const bool gids_are_equal = (rgid == egid) && (rgid == sgid); + if (!uids_are_equal || !gids_are_equal) return false; + if (resuid) *resuid = euid; + if (resgid) *resgid = egid; + return true; +} + +// chroot() and chdir() to /proc/<tid>/fdinfo. +void ChrootToThreadFdInfo(base::PlatformThreadId tid, bool* result) { + DCHECK(result); + *result = false; + + COMPILE_ASSERT((base::is_same<base::PlatformThreadId, int>::value), + TidIsAnInt); + const std::string current_thread_fdinfo = "/proc/" + + base::IntToString(tid) + "/fdinfo/"; + + // Make extra sure that /proc/<tid>/fdinfo is unique to the thread. + CHECK(0 == unshare(CLONE_FILES)); + int chroot_ret = chroot(current_thread_fdinfo.c_str()); + if (chroot_ret) { + PLOG(ERROR) << "Could not chroot"; + return; + } + + // CWD is essentially an implicit file descriptor, so be careful to not leave + // it behind. + PCHECK(0 == chdir("/")); + + *result = true; + return; +} + +// chroot() to an empty dir that is "safe". To be safe, it must not contain +// any subdirectory (chroot-ing there would allow a chroot escape) and it must +// be impossible to create an empty directory there. +// We achieve this by doing the following: +// 1. We create a new thread, which will create a new /proc/<tid>/ directory +// 2. We chroot to /proc/<tid>/fdinfo/ +// This is already "safe", since fdinfo/ does not contain another directory and +// one cannot create another directory there. +// 3. The thread dies +// After (3) happens, the directory is not available anymore in /proc. +bool ChrootToSafeEmptyDir() { + base::Thread chrooter("sandbox_chrooter"); + if (!chrooter.Start()) return false; + bool is_chrooted = false; + chrooter.message_loop()->PostTask(FROM_HERE, + base::Bind(&ChrootToThreadFdInfo, chrooter.thread_id(), &is_chrooted)); + // Make sure our task has run before committing the return value. + chrooter.Stop(); + return is_chrooted; +} + } // namespace. namespace sandbox { @@ -42,13 +143,15 @@ Credentials::Credentials() { Credentials::~Credentials() { } -void Credentials::DropAllCapabilities() { +bool Credentials::DropAllCapabilities() { ScopedCap cap(cap_init()); CHECK(cap); PCHECK(0 == cap_set_proc(cap.get())); + // We never let this function fail. + return true; } -bool Credentials::HasAnyCapability() { +bool Credentials::HasAnyCapability() const { ScopedCap current_cap(cap_get_proc()); CHECK(current_cap); ScopedCap empty_cap(cap_init()); @@ -56,7 +159,7 @@ bool Credentials::HasAnyCapability() { return cap_compare(current_cap.get(), empty_cap.get()) != 0; } -scoped_ptr<std::string> Credentials::GetCurrentCapString() { +scoped_ptr<std::string> Credentials::GetCurrentCapString() const { ScopedCap current_cap(cap_get_proc()); CHECK(current_cap); ScopedCapText cap_text(cap_to_text(current_cap.get(), NULL)); @@ -64,4 +167,39 @@ scoped_ptr<std::string> Credentials::GetCurrentCapString() { return scoped_ptr<std::string> (new std::string(cap_text.get())); } +bool Credentials::MoveToNewUserNS() { + uid_t uid; + gid_t gid; + if (!GetRESIds(&uid, &gid)) { + // If all the uids (or gids) are not equal to each other, the security + // model will most likely confuse the caller, abort. + DVLOG(1) << "uids or gids differ!"; + return false; + } + int ret = unshare(CLONE_NEWUSER); + // EPERM can happen if already in a chroot. EUSERS if too many nested + // namespaces are used. EINVAL for kernels that don't support the feature. + // Valgrind will ENOSYS unshare(). + PCHECK(!ret || errno == EPERM || errno == EUSERS || errno == EINVAL || + errno == ENOSYS); + if (ret) { + VLOG(1) << "Looks like unprivileged CLONE_NEWUSER may not be available " + << "on this kernel."; + return false; + } + // The current {r,e,s}{u,g}id is now an overflow id (c.f. + // /proc/sys/kernel/overflowuid). Setup the uid and gid maps. + DCHECK(GetRESIds(NULL, NULL)); + const char kGidMapFile[] = "/proc/self/gid_map"; + const char kUidMapFile[] = "/proc/self/uid_map"; + CHECK(WriteToIdMapFile(kGidMapFile, gid)); + CHECK(WriteToIdMapFile(kUidMapFile, uid)); + DCHECK(GetRESIds(NULL, NULL)); + return true; +} + +bool Credentials::DropFileSystemAccess() { + return ChrootToSafeEmptyDir(); +} + } // namespace sandbox. diff --git a/sandbox/linux/services/credentials.h b/sandbox/linux/services/credentials.h index 3ea3cfc..80b2ec1 100644 --- a/sandbox/linux/services/credentials.h +++ b/sandbox/linux/services/credentials.h @@ -28,14 +28,34 @@ class Credentials { // Drop all capabilities in the effective, inheritable and permitted sets for // the current process. - void DropAllCapabilities(); + bool DropAllCapabilities(); // Return true iff there is any capability in any of the capabilities sets // of the current process. - bool HasAnyCapability(); + bool HasAnyCapability() const; // Returns the capabilities of the current process in textual form, as // documented in libcap2's cap_to_text(3). This is mostly useful for // debugging and tests. - scoped_ptr<std::string> GetCurrentCapString(); + scoped_ptr<std::string> GetCurrentCapString() const; + + // Move the current process to a new "user namespace" as supported by Linux + // 3.8+ (CLONE_NEWUSER). + // The uid map will be set-up so that the perceived uid and gid will not + // change. + // If this call succeeds, the current process will be granted a full set of + // capabilities in the new namespace. + bool MoveToNewUserNS(); + + // Remove the ability of the process to access the file system. File + // descriptors which are already open prior to calling this API remain + // available. + // The implementation currently uses chroot(2) and requires CAP_SYS_CHROOT. + // CAP_SYS_CHROOT can be acquired by using the MoveToNewUserNS() API. + // Make sure to call DropAllCapabilities() after this call to prevent + // escapes. + // To be secure, it's very important for this API to not be called with any + // directory file descriptor present. TODO(jln): integrate with + // crbug.com/269806 when available. + bool DropFileSystemAccess(); private: DISALLOW_COPY_AND_ASSIGN(Credentials); diff --git a/sandbox/linux/services/credentials_unittest.cc b/sandbox/linux/services/credentials_unittest.cc index 7c705a4..355d4ab 100644 --- a/sandbox/linux/services/credentials_unittest.cc +++ b/sandbox/linux/services/credentials_unittest.cc @@ -4,6 +4,9 @@ #include "sandbox/linux/services/credentials.h" +#include <errno.h> +#include <unistd.h> + #include "base/logging.h" #include "base/memory/scoped_ptr.h" #include "sandbox/linux/tests/unit_tests.h" @@ -11,6 +14,34 @@ namespace sandbox { +namespace { + +bool DirectoryExists(const char* path) { + struct stat dir; + errno = 0; + int ret = stat(path, &dir); + return -1 != ret || ENOENT != errno; +} + +bool WorkingDirectoryIsRoot() { + char current_dir[PATH_MAX]; + char* cwd = getcwd(current_dir, sizeof(current_dir)); + PCHECK(cwd); + if (strcmp("/", cwd)) return false; + + // The current directory is the root. Add a few paranoid checks. + struct stat current; + CHECK_EQ(0, stat(".", ¤t)); + struct stat parrent; + CHECK_EQ(0, stat("..", &parrent)); + CHECK_EQ(current.st_dev, parrent.st_dev); + CHECK_EQ(current.st_ino, parrent.st_ino); + CHECK_EQ(current.st_mode, parrent.st_mode); + CHECK_EQ(current.st_uid, parrent.st_uid); + CHECK_EQ(current.st_gid, parrent.st_gid); + return true; +} + // Give dynamic tools a simple thing to test. TEST(Credentials, CreateAndDestroy) { { @@ -22,15 +53,116 @@ TEST(Credentials, CreateAndDestroy) { SANDBOX_TEST(Credentials, DropAllCaps) { Credentials creds; - creds.DropAllCapabilities(); - SANDBOX_ASSERT(!creds.HasAnyCapability()); + CHECK(creds.DropAllCapabilities()); + CHECK(!creds.HasAnyCapability()); } SANDBOX_TEST(Credentials, GetCurrentCapString) { Credentials creds; - creds.DropAllCapabilities(); + CHECK(creds.DropAllCapabilities()); const char kNoCapabilityText[] = "="; - SANDBOX_ASSERT(*creds.GetCurrentCapString() == kNoCapabilityText); + CHECK(*creds.GetCurrentCapString() == kNoCapabilityText); +} + +SANDBOX_TEST(Credentials, MoveToNewUserNS) { + Credentials creds; + creds.DropAllCapabilities(); + bool userns_supported = creds.MoveToNewUserNS(); + printf("Unprivileged CLONE_NEWUSER supported: %s\n", + userns_supported ? "true." : "false."); + if (!userns_supported) { + printf("This kernel does not support unprivileged namespaces. " + "USERNS tests will all pass.\n"); + return; + } + CHECK(creds.HasAnyCapability()); + creds.DropAllCapabilities(); + CHECK(!creds.HasAnyCapability()); +} + +SANDBOX_TEST(Credentials, UidIsPreserved) { + Credentials creds; + creds.DropAllCapabilities(); + uid_t old_ruid, old_euid, old_suid; + gid_t old_rgid, old_egid, old_sgid; + PCHECK(0 == getresuid(&old_ruid, &old_euid, &old_suid)); + PCHECK(0 == getresgid(&old_rgid, &old_egid, &old_sgid)); + // Probably missing kernel support. + if (!creds.MoveToNewUserNS()) return; + uid_t new_ruid, new_euid, new_suid; + PCHECK(0 == getresuid(&new_ruid, &new_euid, &new_suid)); + CHECK(old_ruid == new_ruid); + CHECK(old_euid == new_euid); + CHECK(old_suid == new_suid); + + gid_t new_rgid, new_egid, new_sgid; + PCHECK(0 == getresgid(&new_rgid, &new_egid, &new_sgid)); + CHECK(old_rgid == new_rgid); + CHECK(old_egid == new_egid); + CHECK(old_sgid == new_sgid); +} + +bool NewUserNSCycle(Credentials* creds) { + DCHECK(creds); + if (!creds->MoveToNewUserNS() || + !creds->HasAnyCapability() || + !creds->DropAllCapabilities() || + creds->HasAnyCapability()) { + return false; + } + return true; +} + +SANDBOX_TEST(Credentials, NestedUserNS) { + Credentials creds; + CHECK(creds.DropAllCapabilities()); + // Probably missing kernel support. + if (!creds.MoveToNewUserNS()) return; + creds.DropAllCapabilities(); + // As of 3.12, the kernel has a limit of 32. See create_user_ns(). + const int kNestLevel = 10; + for (int i = 0; i < kNestLevel; ++i) { + CHECK(NewUserNSCycle(&creds)) << "Creating new user NS failed at iteration " + << i << "."; + } } +// Test the WorkingDirectoryIsRoot() helper. +TEST(Credentials, CanDetectRoot) { + ASSERT_EQ(0, chdir("/proc/")); + ASSERT_FALSE(WorkingDirectoryIsRoot()); + ASSERT_EQ(0, chdir("/")); + ASSERT_TRUE(WorkingDirectoryIsRoot()); +} + +SANDBOX_TEST(Credentials, DropFileSystemAccessIsSafe) { + Credentials creds; + CHECK(creds.DropAllCapabilities()); + // Probably missing kernel support. + if (!creds.MoveToNewUserNS()) return; + CHECK(creds.DropFileSystemAccess()); + CHECK(!DirectoryExists("/proc")); + CHECK(WorkingDirectoryIsRoot()); + // We want the chroot to never have a subdirectory. A subdirectory + // could allow a chroot escape. + CHECK_NE(0, mkdir("/test", 0700)); +} + +// Check that after dropping filesystem access and dropping privileges +// it is not possible to regain capabilities. +SANDBOX_TEST(Credentials, CannotRegainPrivileges) { + Credentials creds; + CHECK(creds.DropAllCapabilities()); + // Probably missing kernel support. + if (!creds.MoveToNewUserNS()) return; + CHECK(creds.DropFileSystemAccess()); + CHECK(creds.DropAllCapabilities()); + + // The kernel should now prevent us from regaining capabilities because we + // are in a chroot. + CHECK(!creds.MoveToNewUserNS()); +} + +} // namespace. + } // namespace sandbox. diff --git a/sandbox/linux/tests/main.cc b/sandbox/linux/tests/main.cc index 8142545..754b310 100644 --- a/sandbox/linux/tests/main.cc +++ b/sandbox/linux/tests/main.cc @@ -2,9 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "base/at_exit.h" #include "testing/gtest/include/gtest/gtest.h" int main(int argc, char *argv[]) { + // The use of Callbacks requires an AtExitManager. + base::AtExitManager exit_manager; testing::InitGoogleTest(&argc, argv); // Always go through re-execution for death tests. // This makes gtest only marginally slower for us and has the |