| // 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 "mojo_service_manager/daemon/daemon.h" |
| |
| #include <linux/limits.h> |
| #include <pwd.h> |
| #include <sys/socket.h> |
| #include <sys/stat.h> |
| #include <sys/types.h> |
| #include <sys/un.h> |
| #include <sysexits.h> |
| |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| |
| #include <base/check_op.h> |
| #include <base/files/file_util.h> |
| #include <base/functional/bind.h> |
| #include <base/logging.h> |
| #include <base/posix/eintr_wrapper.h> |
| #include <base/task/single_thread_task_runner.h> |
| #include <chromeos/constants/mojo_service_manager.h> |
| #include <mojo/public/cpp/platform/platform_channel_endpoint.h> |
| #include <mojo/public/cpp/system/invitation.h> |
| |
| #include "mojo_service_manager/daemon/service_manager.h" |
| #include "mojo_service_manager/daemon/service_policy_loader.h" |
| |
| namespace chromeos::mojo_service_manager { |
| namespace { |
| |
| // Allow others to write so others can connect to the socket. The ACLs are |
| // controlled by the policy files so we don't need to do it again by the system |
| // user / group. |
| constexpr mode_t kSocketMode = |
| S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; |
| |
| base::ScopedFD CreateUnixDomainSocket(const base::FilePath& socket_path) { |
| base::ScopedFD socket_fd{ |
| socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0)}; |
| if (!socket_fd.is_valid()) { |
| PLOG(ERROR) << "Failed to create socket."; |
| return base::ScopedFD{}; |
| } |
| |
| struct sockaddr_un unix_addr { |
| .sun_family = AF_UNIX, |
| }; |
| constexpr size_t kMaxSize = |
| sizeof(unix_addr.sun_path) - /*NULL-terminator*/ 1; |
| CHECK_LE(socket_path.value().size(), kMaxSize); |
| strncpy(unix_addr.sun_path, socket_path.value().c_str(), kMaxSize); |
| |
| if (bind(socket_fd.get(), reinterpret_cast<const sockaddr*>(&unix_addr), |
| sizeof(unix_addr)) < 0) { |
| PLOG(ERROR) << "Failed to bind: " << socket_path.value(); |
| return base::ScopedFD{}; |
| } |
| |
| if (!base::SetPosixFilePermissions(socket_path, kSocketMode)) { |
| PLOG(ERROR) << "Failed to chmod the socket: " << socket_path.value(); |
| return base::ScopedFD{}; |
| } |
| |
| if (listen(socket_fd.get(), SOMAXCONN) < 0) { |
| PLOG(ERROR) << "Failed to listen " << socket_path.value(); |
| return base::ScopedFD{}; |
| } |
| |
| return socket_fd; |
| } |
| |
| base::ScopedFD AcceptSocket(const base::ScopedFD& server_fd) { |
| return base::ScopedFD{HANDLE_EINTR(accept4(server_fd.get(), nullptr, nullptr, |
| SOCK_NONBLOCK | SOCK_CLOEXEC))}; |
| } |
| |
| mojo::PendingReceiver<mojom::ServiceManager> SendMojoInvitationAndPassReceiver( |
| base::ScopedFD peer) { |
| mojo::OutgoingInvitation invitation; |
| mojo::PendingReceiver<mojom::ServiceManager> receiver{ |
| invitation.AttachMessagePipe(kMojoInvitationPipeName)}; |
| mojo::OutgoingInvitation::Send( |
| std::move(invitation), base::kNullProcessHandle, |
| mojo::PlatformChannelEndpoint(mojo::PlatformHandle(std::move(peer)))); |
| return receiver; |
| } |
| |
| } // namespace |
| |
| std::string GetSEContextStringFromChar(const char* buf, size_t len) { |
| // The length may or may not contains the null-terminator. |
| if (len > 0 && buf[len - 1] == '\0') { |
| return std::string(buf); |
| } |
| return std::string(buf, len); |
| } |
| |
| Daemon::Delegate::Delegate() = default; |
| |
| Daemon::Delegate::~Delegate() = default; |
| |
| int Daemon::Delegate::GetSockOpt(const base::ScopedFD& socket, |
| int level, |
| int optname, |
| void* optval, |
| socklen_t* optlen) const { |
| return getsockopt(socket.get(), level, optname, optval, optlen); |
| } |
| |
| const struct passwd* Daemon::Delegate::GetPWUid(uid_t uid) const { |
| return getpwuid(uid); |
| } |
| |
| ServicePolicyMap Daemon::Delegate::LoadPolicyFiles( |
| const std::vector<base::FilePath>& policy_dir_paths) const { |
| ServicePolicyMap res; |
| LoadAllServicePolicyFileFromDirectories(policy_dir_paths, &res); |
| return res; |
| } |
| |
| Daemon::Daemon(Delegate* delegate, |
| const base::FilePath& socket_path, |
| const std::vector<base::FilePath>& policy_dir_paths, |
| Configuration configuration) |
| : ipc_support_(std::make_unique<mojo::core::ScopedIPCSupport>( |
| base::SingleThreadTaskRunner::GetCurrentDefault(), |
| // Don't block shutdown. All mojo pipes are not expected to work after |
| // the broker shutdown, so we don't need to wait them. |
| mojo::core::ScopedIPCSupport::ShutdownPolicy::FAST)), |
| delegate_(delegate), |
| socket_path_(socket_path), |
| policy_dir_paths_(std::move(policy_dir_paths)), |
| configuration_(std::move(configuration)) {} |
| |
| Daemon::~Daemon() {} |
| |
| int Daemon::OnInit() { |
| int ret = brillo::Daemon::OnInit(); |
| if (ret != EX_OK) |
| return ret; |
| |
| // Creates the socket as early as possible to reduce the time of clients that |
| // poll and wait for the socket file to be created. |
| socket_fd_ = CreateUnixDomainSocket(socket_path_); |
| if (!socket_fd_.is_valid()) { |
| LOG(ERROR) << "Failed to create socket server at path: " << socket_path_; |
| return EX_OSERR; |
| } |
| |
| service_manager_ = std::make_unique<ServiceManager>( |
| std::move(configuration_), delegate_->LoadPolicyFiles(policy_dir_paths_)); |
| |
| socket_watcher_ = base::FileDescriptorWatcher::WatchReadable( |
| socket_fd_.get(), |
| base::BindRepeating(&Daemon::SendMojoInvitationAndBindReceiver, |
| base::Unretained(this))); |
| |
| LOG(INFO) << "mojo_service_manager started."; |
| return EX_OK; |
| } |
| |
| void Daemon::OnShutdown(int* exit_code) { |
| LOG(INFO) << "mojo_service_manager is shutdowning with exit code: " |
| << *exit_code; |
| |
| // Manually reset these objects to prevent them posting tasks to the message |
| // loop during shutdowning. |
| socket_watcher_.reset(); |
| service_manager_.reset(); |
| socket_fd_.reset(); |
| // This need to be reset manually to trigger the shutdown of mojo. Otherwise, |
| // the mojo broker tasks could block the message queue during shutdowning. |
| ipc_support_.reset(); |
| } |
| |
| void Daemon::SendMojoInvitationAndBindReceiver() { |
| base::ScopedFD peer = AcceptSocket(socket_fd_); |
| if (!peer.is_valid()) { |
| LOG(ERROR) << "Failed to accept peer socket"; |
| return; |
| } |
| mojom::ProcessIdentityPtr identity = GetProcessIdentityFromPeerSocket(peer); |
| mojo::PendingReceiver<mojom::ServiceManager> receiver = |
| SendMojoInvitationAndPassReceiver(std::move(peer)); |
| if (!identity) { |
| receiver.ResetWithReason( |
| static_cast<uint32_t>(mojom::ErrorCode::kUnexpectedOsError), |
| "Cannot get identity from peer socket."); |
| return; |
| } |
| // TODO(b/234569073): Remove this log after we fully enable service manager. |
| LOG(INFO) << "Receive connection from: " << identity->security_context << ", " |
| << identity->uid << "(" |
| << identity->username.value_or("unknown user") << ")"; |
| CHECK(service_manager_); |
| service_manager_->AddReceiver(std::move(identity), std::move(receiver)); |
| } |
| |
| std::optional<std::string> Daemon::GetUsernameByUid(uint32_t uid) const { |
| const struct passwd* passwd = nullptr; |
| do { |
| static_assert(sizeof(uid_t) == sizeof(uint32_t)); |
| passwd = delegate_->GetPWUid(static_cast<uid_t>(uid)); |
| } while (passwd == nullptr && errno == EINTR); |
| if (passwd == nullptr) { |
| PLOG(ERROR) << "Failed to get username by uid " << uid; |
| return std::nullopt; |
| } |
| return std::string{passwd->pw_name}; |
| } |
| |
| mojom::ProcessIdentityPtr Daemon::GetProcessIdentityFromPeerSocket( |
| const base::ScopedFD& peer) const { |
| auto identity = mojom::ProcessIdentity::New(); |
| struct ucred ucred_data {}; |
| socklen_t len = sizeof(ucred_data); |
| if (delegate_->GetSockOpt(peer, SOL_SOCKET, SO_PEERCRED, &ucred_data, &len) < |
| 0) { |
| PLOG(ERROR) << "Failed to get SO_PEERCRED from peer socket."; |
| return nullptr; |
| } |
| static_assert(sizeof(ucred_data.pid) == 4); |
| static_assert(sizeof(ucred_data.uid) == 4); |
| static_assert(sizeof(ucred_data.gid) == 4); |
| identity->pid = static_cast<uint32_t>(ucred_data.pid); |
| identity->uid = static_cast<uint32_t>(ucred_data.uid); |
| identity->gid = static_cast<uint32_t>(ucred_data.gid); |
| identity->username = GetUsernameByUid(identity->uid); |
| |
| // TODO(b/333323875): Don't get `security_context` after migration. |
| char buf[NAME_MAX] = {}; |
| len = NAME_MAX; |
| if (delegate_->GetSockOpt(peer, SOL_SOCKET, SO_PEERSEC, &buf, &len) < 0) { |
| PLOG(ERROR) << "Failed to get SO_PEERSEC from peer socket."; |
| return nullptr; |
| } |
| identity->security_context = GetSEContextStringFromChar(buf, len); |
| if (identity->security_context.size() == 0) { |
| LOG(ERROR) << "The length of security context gotten from socket is 0."; |
| return nullptr; |
| } |
| return identity; |
| } |
| |
| } // namespace chromeos::mojo_service_manager |