| // Copyright 2019 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "kerberos/krb5_jail_wrapper.h" |
| |
| #include <vector> |
| |
| #include <sys/wait.h> |
| |
| #include <base/files/file_util.h> |
| #include <base/files/scoped_file.h> |
| #include <base/logging.h> |
| #include <base/process/process.h> |
| |
| namespace kerberos { |
| namespace { |
| |
| constexpr char kerberosd_exec[] = "kerberosd-exec"; |
| |
| // Timeout for child processes. |
| constexpr base::TimeDelta kProcessExitTimeout = base::TimeDelta::FromMinutes(3); |
| |
| bool g_change_user_disabled_for_testing = false; |
| |
| // Read/write for owner, read for group. |
| constexpr int kFileMode_rw_r = base::FILE_PERMISSION_READ_BY_USER | |
| base::FILE_PERMISSION_WRITE_BY_USER | |
| base::FILE_PERMISSION_READ_BY_GROUP; |
| |
| // Forks a process to a parent and a jailed child process and manages a pipe for |
| // data transfer from the child to the parent. |
| class MinijailForker { |
| public: |
| // Forks the process into a parent and a child process and puts the child in a |
| // jail because it was naughty. Also sets up a pipe to send data from the |
| // child to the parent. |
| MinijailForker(); |
| ~MinijailForker(); |
| |
| // Returns true if the current process is the child process. |
| // Returns false for the parent process. |
| bool IsChild() const { return child_pid_ == 0; } |
| |
| // |
| // Child interface. DCHECKs IsChild(). |
| // |
| |
| // Writes the |error| status to the data pipe. |
| void Child_WriteError(ErrorType error); |
| |
| // Writes the |tgt_status| code to the data pipe. |
| void Child_WriteTgtStatus(const Krb5Interface::TgtStatus& tgt_status); |
| |
| // Writes the config validation |error_info| to the data pipe. |
| void Child_WriteErrorInfo(const ConfigErrorInfo& error_info); |
| |
| // Exits the process with code 0 if no error occurred and 1 otherwise. |
| void Child_Exit(); |
| |
| // |
| // Parent interface. DCHECKs !IsChild(). |
| // |
| |
| // Waits for the child process to exit. Sets |error_| to true in case the |
| // child didn't exit with status 0. |
| void Parent_Wait(); |
| |
| // Reads an error status from the data pipe. |
| ErrorType Parent_ReadError(); |
| |
| // Reads a TGT status from the data pipe. |
| Krb5Interface::TgtStatus Parent_ReadTgtStatus(); |
| |
| // Reads config validation error info from the data pipe. |
| ConfigErrorInfo Parent_ReadErrorInfo(); |
| |
| private: |
| // Writes |data| of size |data_size| to the data pipe. Sets |error_| on error. |
| // Note: Assumes that all data fits into the pipe buffer. If the pipe buffer |
| // is exceeded, |error_| is set to true. |
| void Child_Write(const void* data, size_t data_size); |
| |
| // Reads |data| of size |data_size| from the data pipe. |data| must be big |
| // enough to hold |data_size| bytes. Sets |error_| on error. |
| void Parent_Read(void* data, size_t data_size); |
| |
| ScopedMinijail jail_; |
| base::ScopedFD pipe_read_end_; |
| base::ScopedFD pipe_write_end_; |
| pid_t child_pid_ = -1; |
| bool error_ = false; |
| }; |
| |
| MinijailForker::MinijailForker() : jail_(minijail_new()) { |
| // Create pipes for data transfer from child to parent. |
| int pipe_fd[2]; |
| if (!base::CreateLocalNonBlockingPipe(pipe_fd)) { |
| LOG(ERROR) << "Failed to create pipe"; |
| error_ = true; |
| return; |
| } |
| pipe_read_end_.reset(pipe_fd[0]); |
| pipe_write_end_.reset(pipe_fd[1]); |
| |
| // Change uid to kerberosd-exec. |
| if (!g_change_user_disabled_for_testing) |
| CHECK_EQ(0, minijail_change_user(jail_.get(), kerberosd_exec)); |
| |
| // Required since we don't have the caps to wipe supplementary groups. |
| minijail_keep_supplementary_gids(jail_.get()); |
| |
| // Fork the process. |
| child_pid_ = minijail_fork(jail_.get()); |
| if (child_pid_ < 0) { |
| PLOG(ERROR) << "Failed to fork process and enter jail"; |
| error_ = true; |
| return; |
| } |
| } |
| |
| MinijailForker::~MinijailForker() = default; |
| |
| void MinijailForker::Child_WriteError(ErrorType error) { |
| Child_Write(&error, sizeof(error)); |
| } |
| |
| void MinijailForker::Child_WriteTgtStatus( |
| const Krb5Interface::TgtStatus& tgt_status) { |
| Child_Write(&tgt_status.validity_seconds, |
| sizeof(tgt_status.validity_seconds)); |
| Child_Write(&tgt_status.renewal_seconds, sizeof(tgt_status.renewal_seconds)); |
| } |
| |
| void MinijailForker::Child_WriteErrorInfo(const ConfigErrorInfo& error_info) { |
| std::vector<uint8_t> buffer(error_info.ByteSizeLong()); |
| CHECK(error_info.SerializeToArray(buffer.data(), buffer.size())); |
| int buffer_size = static_cast<int>(buffer.size()); |
| Child_Write(&buffer_size, sizeof(buffer_size)); |
| Child_Write(buffer.data(), buffer.size()); |
| } |
| |
| void MinijailForker::Child_Exit() { |
| DCHECK(IsChild()); |
| exit(error_ ? 1 : 0); |
| } |
| |
| void MinijailForker::Parent_Wait() { |
| DCHECK(!IsChild()); |
| |
| auto process = base::Process::Open(child_pid_); |
| int exit_code = -1; |
| if (!process.WaitForExitWithTimeout(kProcessExitTimeout, &exit_code)) { |
| LOG(ERROR) << "Child process timed out"; |
| process.Terminate(-1 /* exit_code */, false /* wait */); |
| error_ = true; |
| return; |
| } |
| |
| if (exit_code != 0) { |
| LOG(ERROR) << "Child process exited with code " << exit_code; |
| error_ = true; |
| } |
| } |
| |
| ErrorType MinijailForker::Parent_ReadError() { |
| // Handle internal errors, don't try to read ErrorType, it might block. |
| ErrorType error = ERROR_JAIL_FAILURE; |
| if (error_) |
| return error; |
| |
| Parent_Read(&error, sizeof(error)); |
| return error_ ? ERROR_JAIL_FAILURE : error; |
| } |
| |
| Krb5Interface::TgtStatus MinijailForker::Parent_ReadTgtStatus() { |
| // Handle internal errors, don't try to read the TGT status, it might block. |
| Krb5Interface::TgtStatus tgt_status; |
| if (error_) |
| return tgt_status; |
| |
| Parent_Read(&tgt_status.validity_seconds, |
| sizeof(tgt_status.validity_seconds)); |
| Parent_Read(&tgt_status.renewal_seconds, sizeof(tgt_status.renewal_seconds)); |
| return tgt_status; |
| } |
| |
| ConfigErrorInfo MinijailForker::Parent_ReadErrorInfo() { |
| // Handle internal errors, don't try to read the error info, it might block. |
| ConfigErrorInfo error_info; |
| if (error_) |
| return error_info; |
| |
| int buffer_size = 0; |
| Parent_Read(&buffer_size, sizeof(buffer_size)); |
| if (buffer_size == 0) |
| return error_info; |
| |
| std::vector<uint8_t> buffer; |
| buffer.resize(buffer_size); |
| Parent_Read(buffer.data(), buffer_size); |
| error_info.ParseFromArray(buffer.data(), buffer_size); |
| return error_info; |
| } |
| |
| void MinijailForker::Child_Write(const void* data, size_t data_size) { |
| DCHECK(IsChild()); |
| if (!base::WriteFileDescriptor(pipe_write_end_.get(), |
| static_cast<const char*>(data), data_size)) { |
| LOG(ERROR) << "Failed to write " << data_size << " bytes"; |
| error_ = true; |
| } |
| } |
| |
| void MinijailForker::Parent_Read(void* data, size_t data_size) { |
| DCHECK(!IsChild()); |
| if (HANDLE_EINTR(read(pipe_read_end_.get(), data, data_size)) != |
| static_cast<int>(data_size)) { |
| LOG(ERROR) << "Failed to read " << data_size << " bytes"; |
| error_ = true; |
| } |
| } |
| |
| // If |error| is ERROR_NONE, gives TGT at |krb5cc_path| group read permission |
| // (for the kerberosd group), so that the kerberosd user can read it. |
| void SetTgtFilePermissions(const base::FilePath& krb5cc_path, ErrorType error) { |
| if (error == ERROR_NONE) |
| CHECK(base::SetPosixFilePermissions(krb5cc_path, kFileMode_rw_r)); |
| } |
| |
| } // namespace |
| |
| Krb5JailWrapper::Krb5JailWrapper(std::unique_ptr<Krb5Interface> krb5) |
| : krb5_(std::move(krb5)) {} |
| |
| Krb5JailWrapper::~Krb5JailWrapper() = default; |
| |
| ErrorType Krb5JailWrapper::AcquireTgt(const std::string& principal_name, |
| const std::string& password, |
| const base::FilePath& krb5cc_path, |
| const base::FilePath& krb5conf_path) { |
| MinijailForker forker; |
| |
| if (forker.IsChild()) { |
| ErrorType error = |
| krb5_->AcquireTgt(principal_name, password, krb5cc_path, krb5conf_path); |
| SetTgtFilePermissions(krb5cc_path, error); |
| forker.Child_WriteError(error); |
| forker.Child_Exit(); |
| } |
| |
| forker.Parent_Wait(); |
| return forker.Parent_ReadError(); |
| } |
| |
| ErrorType Krb5JailWrapper::RenewTgt(const std::string& principal_name, |
| const base::FilePath& krb5cc_path, |
| const base::FilePath& krb5conf_path) { |
| MinijailForker forker; |
| |
| if (forker.IsChild()) { |
| ErrorType error = |
| krb5_->RenewTgt(principal_name, krb5cc_path, krb5conf_path); |
| SetTgtFilePermissions(krb5cc_path, error); |
| forker.Child_WriteError(error); |
| forker.Child_Exit(); |
| } |
| |
| forker.Parent_Wait(); |
| return forker.Parent_ReadError(); |
| } |
| |
| ErrorType Krb5JailWrapper::GetTgtStatus(const base::FilePath& krb5cc_path, |
| Krb5Interface::TgtStatus* status) { |
| MinijailForker forker; |
| |
| if (forker.IsChild()) { |
| ErrorType error = krb5_->GetTgtStatus(krb5cc_path, status); |
| forker.Child_WriteTgtStatus(*status); |
| forker.Child_WriteError(error); |
| forker.Child_Exit(); |
| } |
| |
| forker.Parent_Wait(); |
| *status = forker.Parent_ReadTgtStatus(); |
| return forker.Parent_ReadError(); |
| } |
| |
| ErrorType Krb5JailWrapper::ValidateConfig(const std::string& krb5conf, |
| ConfigErrorInfo* error_info) { |
| MinijailForker forker; |
| |
| if (forker.IsChild()) { |
| ErrorType error = krb5_->ValidateConfig(krb5conf, error_info); |
| forker.Child_WriteErrorInfo(*error_info); |
| forker.Child_WriteError(error); |
| forker.Child_Exit(); |
| } |
| |
| forker.Parent_Wait(); |
| *error_info = forker.Parent_ReadErrorInfo(); |
| return forker.Parent_ReadError(); |
| } |
| |
| // static |
| void Krb5JailWrapper::DisableChangeUserForTesting(bool disabled) { |
| g_change_user_disabled_for_testing = disabled; |
| } |
| |
| } // namespace kerberos |