blob: e2c286afdf8d19a30c5726aed3f34e08b782eb43 [file] [log] [blame]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <pwd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sysexits.h>
#include <cstring>
#include <map>
#include <optional>
#include <string>
#include <utility>
#include <base/check_op.h>
#include <base/command_line.h>
#include <base/files/file_util.h>
#include <base/files/scoped_temp_dir.h>
#include <base/strings/stringprintf.h>
#include <base/task/single_thread_task_runner.h>
#include <base/test/bind.h>
#include <base/test/test_timeouts.h>
#include <base/threading/thread.h>
#include <brillo/process/process.h>
#include <brillo/process/process_reaper.h>
#include <gtest/gtest.h>
#include "mojo_service_manager/daemon/daemon.h"
#include "mojo_service_manager/daemon/daemon_test_helper.h"
namespace chromeos::mojo_service_manager {
mojom::ProcessIdentityPtr GetProcessIdentityFromPeerSocketForTest(
const Daemon& daemon, const base::ScopedFD& peer) {
return daemon.GetProcessIdentityFromPeerSocket(peer);
}
namespace {
constexpr char kFakeSecurityContext[] = "u:r:cros_fake:s0";
constexpr char kDaemonTestHelperExecName[] = "daemon_test_helper";
constexpr char kTestSocketName[] = "test_socket";
class DaemonTest : public ::testing::Test {
public:
void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); }
base::FilePath GetSocketPath() {
return temp_dir_.GetPath().Append(kTestSocketName);
}
void RunDaemonAndExpectTestSubprocessResult(Daemon::Delegate* delegate,
DaemonTestHelperResult result) {
Daemon daemon{delegate, GetSocketPath(), {}, Configuration{}};
brillo::ProcessImpl process;
process.AddArg(base::CommandLine::ForCurrentProcess()
->GetProgram()
.DirName()
.Append(kDaemonTestHelperExecName)
.value());
process.AddArg(base::StringPrintf("--%s=%s", kSocketPathSwitch,
GetSocketPath().value().c_str()));
process.Start();
brillo::ProcessReaper reaper;
reaper.Register(&daemon);
reaper.WatchForChild(FROM_HERE, process.pid(),
base::BindLambdaForTesting([&](const siginfo_t& info) {
EXPECT_EQ(info.si_pid, process.pid());
// Now child process has stopped so we should release
// it to prevent it being killed again.
process.Release();
EXPECT_EQ(info.si_code, CLD_EXITED);
EXPECT_EQ(info.si_status, static_cast<int>(result));
daemon.Quit();
}));
EXPECT_EQ(daemon.Run(), EX_OK);
}
private:
base::ScopedTempDir temp_dir_;
};
// Fake Daemon::Delegate to return fake |getsockopt|. If the expected field is
// set to nullopt, the |getsockopt| returns error when trying to report that
// field.
class FakeDelegate : public Daemon::Delegate {
public:
FakeDelegate();
FakeDelegate(const FakeDelegate&) = delete;
FakeDelegate& operator=(const FakeDelegate&) = delete;
~FakeDelegate() override;
// Overrides Daemon::Delegate.
int GetSockOpt(const base::ScopedFD& socket,
int level,
int optname,
void* optval,
socklen_t* optlen) const override;
const struct passwd* GetPWUid(uid_t uid) const override;
ServicePolicyMap LoadPolicyFiles(
const std::vector<base::FilePath>& policy_dir_paths) const override;
// Sets this to change `ucred` returned by `GetSockOpt`.
std::optional<struct ucred> ucred_ = std::nullopt;
// Sets this to change `security_context` returned by `GetSockOpt`.
std::optional<std::string> security_context_ = std::nullopt;
// Sets this to change `passwd` returned by `GetPWUid`.
std::map<uid_t, struct passwd> passwd_map_;
// Sets this to raise a EINTR for `GetPWUid`. Use static variable so it can be
// modified in a const method.
static bool raise_a_eintr_for_getpwuid_;
};
bool FakeDelegate::raise_a_eintr_for_getpwuid_ = false;
FakeDelegate::FakeDelegate() = default;
FakeDelegate::~FakeDelegate() = default;
int FakeDelegate::GetSockOpt(const base::ScopedFD& socket,
int level,
int optname,
void* optval,
socklen_t* optlen) const {
switch (optname) {
case SO_PEERCRED:
if (!ucred_)
return -1;
CHECK_EQ(*optlen, sizeof(struct ucred));
*reinterpret_cast<struct ucred*>(optval) = ucred_.value();
return 0;
case SO_PEERSEC:
if (!security_context_)
return -1;
CHECK_GE(*optlen, security_context_->size());
*optlen = security_context_->size();
strncpy(reinterpret_cast<char*>(optval), security_context_->c_str(),
security_context_->size());
return 0;
default:
CHECK(false);
return 0;
}
}
const struct passwd* FakeDelegate::GetPWUid(uid_t uid) const {
if (raise_a_eintr_for_getpwuid_) {
raise_a_eintr_for_getpwuid_ = false;
errno = EINTR;
return nullptr;
}
const auto it = passwd_map_.find(uid);
if (it != passwd_map_.end()) {
errno = 0;
return &(it->second);
}
errno = ESRCH;
return nullptr;
}
ServicePolicyMap FakeDelegate::LoadPolicyFiles(
const std::vector<base::FilePath>& policy_dir_paths) const {
return ServicePolicyMap{};
}
TEST_F(DaemonTest, FailToListenSocket) {
// Create the socket file so the daemon will fail to create it.
ASSERT_TRUE(base::WriteFile(GetSocketPath(), "test"));
FakeDelegate delegate{};
Daemon daemon{&delegate, GetSocketPath(), {}, Configuration{}};
EXPECT_NE(daemon.Run(), EX_OK);
}
TEST_F(DaemonTest, FailToGetSocketCred) {
// Set ucred to nullopt to fail the test.
FakeDelegate delegate{};
delegate.ucred_ = std::nullopt;
delegate.security_context_ = kFakeSecurityContext;
RunDaemonAndExpectTestSubprocessResult(
&delegate, DaemonTestHelperResult::kResetWithOsError);
}
TEST_F(DaemonTest, FailToGetSocketSecurityContext) {
// Set security context to nullopt to fail the test.
struct ucred ucred = {};
FakeDelegate delegate{};
delegate.ucred_ = ucred;
delegate.security_context_ = std::nullopt;
RunDaemonAndExpectTestSubprocessResult(
&delegate, DaemonTestHelperResult::kResetWithOsError);
}
TEST_F(DaemonTest, FailToGetSocketSecurityContextEmptyString) {
// Set security context to empty string to fail the test.
struct ucred ucred = {};
FakeDelegate delegate{};
delegate.ucred_ = ucred;
delegate.security_context_ = std::string();
RunDaemonAndExpectTestSubprocessResult(
&delegate, DaemonTestHelperResult::kResetWithOsError);
}
TEST_F(DaemonTest, Connect) {
struct ucred ucred = {};
FakeDelegate delegate{};
delegate.ucred_ = ucred;
delegate.security_context_ = kFakeSecurityContext;
RunDaemonAndExpectTestSubprocessResult(
&delegate, DaemonTestHelperResult::kConnectSuccessfully);
}
TEST(DaemonUtilTest, GetSEContextStringFromChar) {
// The length doesn't contain the null-terminator.
EXPECT_EQ(GetSEContextStringFromChar("", 0), "");
EXPECT_EQ(GetSEContextStringFromChar("a", 1), "a");
EXPECT_EQ(GetSEContextStringFromChar("aa", 2), "aa");
// The length contains the null-terminator.
EXPECT_EQ(GetSEContextStringFromChar("\0", 1), "");
EXPECT_EQ(GetSEContextStringFromChar("a\0", 2), "a");
EXPECT_EQ(GetSEContextStringFromChar("aa\0", 3), "aa");
// The length doesn't contain the null-terminator and the last char is not
// null-terminator.
EXPECT_EQ(GetSEContextStringFromChar("a", 0), "");
EXPECT_EQ(GetSEContextStringFromChar("aa", 1), "a");
EXPECT_EQ(GetSEContextStringFromChar("aaa", 2), "aa");
}
class DaemonGetProcessIdentityTest : public ::testing::Test {
protected:
void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); }
base::FilePath GetSocketPath() {
return temp_dir_.GetPath().Append(kTestSocketName);
}
mojom::ProcessIdentityPtr GetProcessIdentityFromPeerSocket(
Daemon::Delegate* delegate) {
Daemon daemon{delegate, GetSocketPath(), {}, Configuration{}};
base::ScopedFD fake_fd;
return GetProcessIdentityFromPeerSocketForTest(daemon, fake_fd);
}
private:
base::ScopedTempDir temp_dir_;
};
TEST_F(DaemonGetProcessIdentityTest, NoUsername) {
struct ucred ucred = {.uid = 123};
FakeDelegate delegate{};
delegate.ucred_ = ucred;
delegate.security_context_ = kFakeSecurityContext;
auto process_identity = GetProcessIdentityFromPeerSocket(&delegate);
EXPECT_TRUE(process_identity);
EXPECT_EQ(process_identity->uid, 123);
EXPECT_EQ(process_identity->username, std::nullopt);
}
TEST_F(DaemonGetProcessIdentityTest, Username) {
struct ucred ucred = {.uid = 123};
FakeDelegate delegate{};
delegate.ucred_ = ucred;
delegate.security_context_ = kFakeSecurityContext;
char username[] = "fake_username";
delegate.passwd_map_[123].pw_name = username;
auto process_identity = GetProcessIdentityFromPeerSocket(&delegate);
EXPECT_TRUE(process_identity);
EXPECT_EQ(process_identity->uid, 123);
EXPECT_EQ(process_identity->username, username);
}
TEST_F(DaemonGetProcessIdentityTest, UsernameEINTR) {
struct ucred ucred = {.uid = 123};
FakeDelegate delegate{};
delegate.ucred_ = ucred;
delegate.security_context_ = kFakeSecurityContext;
char username[] = "fake_username";
delegate.passwd_map_[123].pw_name = username;
FakeDelegate::raise_a_eintr_for_getpwuid_ = true;
auto process_identity = GetProcessIdentityFromPeerSocket(&delegate);
EXPECT_TRUE(process_identity);
EXPECT_EQ(process_identity->uid, 123);
EXPECT_EQ(process_identity->username, username);
}
} // namespace
} // namespace chromeos::mojo_service_manager