blob: ca5152045487625f793354953caa11437ade63fd [file] [log] [blame]
// Copyright 2014 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 "login_manager/x_server_runner.h"
#include <arpa/inet.h>
#include <grp.h>
#include <signal.h>
#include <sys/resource.h>
#include <sys/signalfd.h>
#include <sys/wait.h>
#include <unistd.h>
#include <base/bind.h>
#include <base/command_line.h>
#include <base/file_util.h>
#include <base/files/file.h>
#include <base/logging.h>
#include <base/macros.h>
#include <base/process/launch.h>
#include <base/rand_util.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/stringprintf.h>
#include "login_manager/util.h"
namespace {
// Path to the X server binary.
const char kXServerCommand[] = "/usr/bin/X";
// Writes |data| to |file|, returning true on success.
bool WriteString(base::File* file, const std::string& data) {
return file->WriteAtCurrentPos(data.data(), data.size()) ==
static_cast<int>(data.size());
}
// Writes |value| to |file| in big-endian order, returning true on success.
bool WriteUint16(base::File* file, uint16 value) {
value = htons(value);
return file->WriteAtCurrentPos(
reinterpret_cast<const char*>(&value), sizeof(value)) ==
static_cast<int>(sizeof(value));
}
// Creates a new X authority file at |path|, returning true on success.
bool CreateXauthFile(const base::FilePath& path, uid_t uid, gid_t gid) {
base::File file(path,
base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
if (!file.IsValid()) {
PLOG(ERROR) << "Couldn't open " << path.value();
return false;
}
if (!util::SetPermissions(path, uid, gid, 0600))
return false;
const int kCookieSize = 16;
// TODO(derat): base/rand_util.h says not to use RandBytesAsString() for
// security-related purposes, but crypto::RandBytes() (which we don't package)
// just wraps RandBytes(). The base implementation uses /dev/urandom, which is
// fine for our purposes (see e.g. http://www.2uo.de/myths-about-urandom/),
// but to make this code self-documenting, this call should be changed to
// crypto::RandBytes() if/when that gets packaged for Chrome OS.
const std::string kCookie(base::RandBytesAsString(kCookieSize));
const uint16 kFamily = 0x100;
const std::string kAddress = "localhost";
const std::string kNumber = "0";
const std::string kName = "MIT-MAGIC-COOKIE-1";
if (!WriteUint16(&file, kFamily) ||
!WriteUint16(&file, kAddress.size()) ||
!WriteString(&file, kAddress) ||
!WriteUint16(&file, kNumber.size()) ||
!WriteString(&file, kNumber) ||
!WriteUint16(&file, kName.size()) ||
!WriteString(&file, kName) ||
!WriteUint16(&file, kCookie.size()) ||
!WriteString(&file, kCookie)) {
PLOG(ERROR) << "Couldn't write to " << path.value();
return false;
}
return true;
}
// Runs the X server, replacing the current process.
void ExecServer(int vt,
int max_vt,
const base::FilePath& xauth_file,
const base::FilePath& log_file) {
std::vector<std::string> args;
args.push_back(kXServerCommand);
args.push_back("-nohwaccess");
args.push_back("-noreset");
args.push_back("-maxvt");
args.push_back(base::IntToString(max_vt));
args.push_back("-nolisten");
args.push_back("tcp");
args.push_back(base::StringPrintf("vt%d", vt));
args.push_back("-auth");
args.push_back(xauth_file.value());
args.push_back("-logfile");
args.push_back(log_file.value());
const size_t kMaxArgs = 32;
char* argv[kMaxArgs + 1];
CHECK_LE(args.size(), kMaxArgs);
for (size_t i = 0; i < args.size(); ++i)
argv[i] = const_cast<char*>(args[i].c_str());
argv[args.size()] = NULL;
// This call doesn't return on success.
PCHECK(execv(argv[0], argv) == 0) << "execv() failed";
}
// Drops privileges, forks-and-execs the X server, waits for it to emit SIGUSR1
// to indicate that it's ready for connections, and returns true on success.
bool ExecAndWaitForServer(const std::string& user,
uid_t uid,
gid_t gid,
const base::Closure& closure) {
// Avoid some syscalls when not running as root in tests.
if (getuid() == 0) {
if (setpriority(PRIO_PROCESS, 0, -20) != 0)
PLOG(WARNING) << "setpriority() failed";
PCHECK(initgroups(user.c_str(), gid) == 0);
PCHECK(setgid(gid) == 0);
PCHECK(setuid(uid) == 0);
}
sigset_t mask;
PCHECK(sigemptyset(&mask) == 0);
PCHECK(sigaddset(&mask, SIGUSR1) == 0);
PCHECK(sigaddset(&mask, SIGCHLD) == 0);
const int fd = signalfd(-1, &mask, 0);
PCHECK(fd != -1) << "signalfd() failed";
PCHECK(sigprocmask(SIG_BLOCK, &mask, NULL) == 0);
switch (pid_t pid = fork()) {
case -1:
PLOG(ERROR) << "fork() failed";
return false;
case 0:
// Forked process: exec the X server.
base::CloseSuperfluousFds(base::InjectiveMultimap());
// Set SIGUSR1's disposition to SIG_IGN before exec-ing so that X will
// emit SIGUSR1 once it's ready to accept connections.
PCHECK(signal(SIGUSR1, SIG_IGN) != SIG_ERR);
PCHECK(signal(SIGCHLD, SIG_DFL) != SIG_ERR);
closure.Run();
break;
default: {
// Original process: wait for the forked process to become ready or exit.
LOG(INFO) << "X server started with PID " << pid;
struct signalfd_siginfo siginfo;
while (true) {
int bytes_read = HANDLE_EINTR(read(fd, &siginfo, sizeof(siginfo)));
PCHECK(bytes_read >= 0);
if (bytes_read != sizeof(siginfo)) {
LOG(ERROR) << "Read " << bytes_read << " byte(s); expected "
<< sizeof(siginfo);
close(fd);
return false;
}
switch (siginfo.ssi_signo) {
case SIGUSR1:
LOG(INFO) << "X server is ready for connections";
close(fd);
return true;
case SIGCHLD: {
int status = 0;
int result = waitpid(pid, &status, WNOHANG);
if (result != 0) {
PCHECK(result == pid) << "waitpid() returned " << result;
if (WIFEXITED(status)) {
LOG(ERROR) << "X server exited with " << WEXITSTATUS(status)
<< " before sending SIGUSR1";
close(fd);
return false;
} else if (WIFSIGNALED(status)) {
LOG(ERROR) << "X server was terminated with signal "
<< WTERMSIG(status) << " before sending SIGUSR1";
close(fd);
return false;
}
}
// Ignore non-exit SIGCHLDs.
break;
}
default:
CHECK(false) << "Unexpected signal " << siginfo.ssi_signo;
}
}
}
}
return true;
}
} // namespace
const char XServerRunner::kSocketDir[] = "/tmp/.X11-unix";
const char XServerRunner::kIceDir[] = "/tmp/.ICE-unix";
const char XServerRunner::kLogFile[] = "/var/log/xorg/Xorg.0.log";
const char XServerRunner::kXkbDir[] = "/var/lib/xkb";
XServerRunner::XServerRunner() : child_pid_(0) {}
XServerRunner::~XServerRunner() {}
bool XServerRunner::StartServer(const std::string& user,
int vt,
bool allow_vt_switching,
const base::FilePath& xauth_file) {
uid_t uid = 0;
gid_t gid = 0;
if (!util::GetUserInfo(user, &uid, &gid))
return false;
if (!CreateXauthFile(xauth_file, uid, gid))
return false;
if (!util::EnsureDirectoryExists(GetPath(kSocketDir), 0, 0, 01777) ||
!util::EnsureDirectoryExists(GetPath(kIceDir), 0, 0, 01777))
return false;
const base::FilePath log_file(GetPath(kLogFile));
if (!util::EnsureDirectoryExists(log_file.DirName(), uid, gid, 0755) ||
!util::EnsureDirectoryExists(GetPath(kXkbDir), uid, gid, 0755))
return false;
// Create a relative symlink from one directory above |log_file| to the file
// itself (e.g. /var/log/Xorg.0.log -> xorg/Xorg.0.log).
base::CreateSymbolicLink(
log_file.DirName().BaseName().Append(log_file.BaseName()),
log_file.DirName().DirName().Append(log_file.BaseName()));
// Disable all the Ctrl-Alt-Fn shortcuts for switching between virtual
// terminals if requested. Otherwise, disable only Fn (n>=3) keys.
int max_vt = allow_vt_switching ? 2 : 0;
switch (child_pid_ = fork()) {
case -1:
PLOG(ERROR) << "fork() failed";
return false;
case 0: {
base::Closure closure = !callback_for_testing_.is_null() ?
callback_for_testing_ :
base::Bind(&ExecServer, vt, max_vt, xauth_file, log_file);
// The child process waits for the server to start and exits with 0.
exit(ExecAndWaitForServer(user, uid, gid, closure) ? 0 : 1);
}
default:
LOG(INFO) << "Child process " << child_pid_
<< " starting X server in background";
}
return true;
}
bool XServerRunner::WaitForServer() {
CHECK_GT(child_pid_, 0);
int status = 0;
if (waitpid(child_pid_, &status, 0) != child_pid_) {
PLOG(ERROR) << "waitpid() on " << child_pid_ << " failed";
return false;
}
if (!WIFEXITED(status)) {
LOG(ERROR) << "Child process " << child_pid_ << " didn't exit normally";
return false;
}
if (WEXITSTATUS(status) != 0) {
LOG(ERROR) << "Child process " << child_pid_ << " exited with "
<< WEXITSTATUS(status);
return false;
}
return true;
}
base::FilePath XServerRunner::GetPath(const std::string& path) const {
return util::GetReparentedPath(path, base_path_for_testing_);
}