1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893
|
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// This file implements a wrapper to run the virtual me2me session within a
// proper PAM session. It will generally be run as root and drop privileges to
// the specified user before running the me2me session script.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
// Usage: user-session start [--foreground] [--user user] [-- SCRIPT_ARGS...]
//
// Options:
// --foreground - Don't daemonize.
// --user - Create a session for the specified user. Required when
// running as root, not allowed when running as a normal user.
// SCRIPT_ARGS - Arguments following -- are passed to the script verbatim.
#include <fcntl.h>
#include <grp.h>
#include <limits.h>
#include <pwd.h>
#include <security/pam_appl.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>
#include "base/environment.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/process/launch.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
namespace {
// This is the file descriptor used for the Python session script to pass us
// messages during startup. It must be kept in sync with USER_SESSION_MESSAGE_FD
// in linux_me2me_host.py. It should be high enough that login scripts are
// unlikely to interfere with it, but is otherwise arbitrary.
const int kMessageFd = 202;
// This is the exit code the Python session script will use to signal that the
// user-session wrapper should restart instead of exiting. It must be kept in
// sync with RELAUNCH_EXIT_CODE in linux_me2me_host.py
const int kRelaunchExitCode = 41;
const char kPamName[] = "chrome-remote-desktop";
const char kScriptName[] = "chrome-remote-desktop";
const char kStartCommand[] = "start";
const char kForegroundFlag[] = "--foreground";
const char kUserFlag[] = "--user";
const char kExeSymlink[] = "/proc/self/exe";
// This template will be formatted by strftime and then used by mkstemp
const char kLogFileTemplate[] =
"/tmp/chrome_remote_desktop_%Y%m%d_%H%M%S_XXXXXX";
// The filename for the latest log symlink.
constexpr char kLatestLogSymlink[] = "/tmp/chrome_remote_desktop.latest";
const char kUsageMessage[] =
"This program is not intended to be run by end users. To configure Chrome\n"
"Remote Desktop, please install the app from the Chrome Web Store:\n"
"https://chrome.google.com/remotedesktop\n";
// A list of variable to pass through to the child environment. Should be kept
// in sync with remoting_user_session_wrapper.sh for testing.
const char* const kPassthroughVariables[] = {
"GOOGLE_CLIENT_ID_REMOTING", "GOOGLE_CLIENT_ID_REMOTING_HOST",
"GOOGLE_CLIENT_SECRET_REMOTING", "GOOGLE_CLIENT_SECRET_REMOTING_HOST",
"CHROME_REMOTE_DESKTOP_HOST_EXTRA_PARAMS"};
// Holds the null-terminated path to this executable. This is obtained at
// startup, since it may be harder to obtain later. (E.g., Linux will append
// " (deleted)" if the file has been replaced by an update.)
char gExecutablePath[PATH_MAX] = {};
void PrintUsage() {
std::fputs(kUsageMessage, stderr);
}
// Shell-escapes a single argument in a way that is compatible with various
// different shells. Returns nullopt when argument contains a newline, which
// can't be represented in a cross-shell fashion.
std::optional<std::string> ShellEscapeArgument(std::string_view argument) {
std::string result;
for (char character : argument) {
// csh in particular doesn't provide a good way to handle this
if (character == '\n') {
return std::nullopt;
}
// Some shells ascribe special meaning to some escape sequences such as \t,
// so don't escape any alphanumerics. (Also cuts down on verbosity.) This is
// similar to the approach sudo takes.
if (!((character >= '0' && character <= '9') ||
(character >= 'A' && character <= 'Z') ||
(character >= 'a' && character <= 'z') ||
(character == '-' || character == '_'))) {
result.push_back('\\');
}
result.push_back(character);
}
return result;
}
// PAM conversation function. Since the wrapper runs in a non-interactive
// context, log any messages, but return an error if asked to provide user
// input.
extern "C" int Converse(int num_messages,
const struct pam_message** messages,
struct pam_response** responses,
void* context) {
bool failed = false;
for (int i = 0; i < num_messages; ++i) {
// This is correct for the PAM included with Linux, OS X, and BSD. However,
// apparently Solaris and HP/UX require instead `&(*msg)[i]`. That is, they
// disagree as to which level of indirection contains the array.
const pam_message* message = messages[i];
switch (message->msg_style) {
case PAM_PROMPT_ECHO_OFF:
case PAM_PROMPT_ECHO_ON:
LOG(WARNING) << "PAM requested user input (unsupported): "
<< (message->msg ? message->msg : "");
failed = true;
break;
case PAM_TEXT_INFO:
LOG(INFO) << "[PAM] " << (message->msg ? message->msg : "");
break;
case PAM_ERROR_MSG:
// Error messages from PAM are not necessarily fatal to the operation,
// as the module may be optional.
LOG(WARNING) << "[PAM] " << (message->msg ? message->msg : "");
break;
default:
LOG(WARNING) << "Encountered unknown PAM message style";
failed = true;
break;
}
}
if (failed) {
return PAM_CONV_ERR;
}
pam_response* response_list = static_cast<pam_response*>(
std::calloc(num_messages, sizeof(*response_list)));
if (response_list == nullptr) {
return PAM_BUF_ERR;
}
*responses = response_list;
return PAM_SUCCESS;
}
const struct pam_conv kPamConversation = {Converse, nullptr};
// Wrapper class for working with PAM and cleaning up in an RAII fashion
class PamHandle {
public:
// Attempts to initialize PAM transaction. Check the result with IsInitialized
// before calling any other member functions.
PamHandle(const char* service_name,
const char* user,
const struct pam_conv* pam_conversation) {
last_return_code_ = pam_start(service_name, user, pam_conversation,
&pam_handle_.AsEphemeralRawAddr());
if (last_return_code_ != PAM_SUCCESS) {
pam_handle_ = nullptr;
}
}
PamHandle(const PamHandle&) = delete;
PamHandle& operator=(const PamHandle&) = delete;
// Terminates PAM transaction
~PamHandle() {
if (pam_handle_ != nullptr) {
pam_end(pam_handle_, last_return_code_);
}
}
// Checks whether the PAM transaction was successfully initialized. Only call
// other member functions if this returns true.
bool IsInitialized() const { return pam_handle_ != nullptr; }
// Performs account validation
int AccountManagement(int flags) {
return last_return_code_ = pam_acct_mgmt(pam_handle_, flags);
}
// Establishes or deletes PAM user credentials
int SetCredentials(int flags) {
return last_return_code_ = pam_setcred(pam_handle_, flags);
}
// Starts user session
int OpenSession(int flags) {
return last_return_code_ = pam_open_session(pam_handle_, flags);
}
// Ends user session
int CloseSession(int flags) {
return last_return_code_ = pam_close_session(pam_handle_, flags);
}
int SetItem(int item_type, const char* value) {
return last_return_code_ = pam_set_item(pam_handle_, item_type, value);
}
// Returns the current username according to PAM. It is possible for PAM
// modules to change this from the initial value passed to the constructor.
std::optional<std::string> GetUser() {
const char* user;
last_return_code_ = pam_get_item(pam_handle_, PAM_USER,
reinterpret_cast<const void**>(&user));
if (last_return_code_ != PAM_SUCCESS || user == nullptr) {
return std::nullopt;
}
return std::string(user);
}
// Sets a PAM environment variable.
int PutEnv(std::string_view name, std::string_view value) {
std::string name_value = base::StrCat({name, "=", value});
return last_return_code_ = pam_putenv(pam_handle_, name_value.c_str());
}
// Obtains the list of environment variables provided by PAM modules.
std::optional<base::EnvironmentMap> GetEnvironment() {
char** environment = pam_getenvlist(pam_handle_);
if (environment == nullptr) {
return std::nullopt;
}
base::EnvironmentMap environment_map;
for (char** variable = environment; *variable != nullptr; ++variable) {
char* delimiter = std::strchr(*variable, '=');
if (delimiter != nullptr) {
environment_map[std::string(*variable, delimiter)] =
std::string(delimiter + 1);
}
std::free(*variable);
}
std::free(environment);
return environment_map;
}
// Returns a description of the given return code
const char* ErrorString(int return_code) {
return pam_strerror(pam_handle_, return_code);
}
// Logs a fatal error if return_code isn't PAM_SUCCESS
void CheckReturnCode(int return_code, std::string_view what) {
if (return_code != PAM_SUCCESS) {
LOG(FATAL) << "[PAM] " << what << ": " << ErrorString(return_code);
}
}
private:
raw_ptr<pam_handle_t> pam_handle_ = nullptr;
int last_return_code_ = PAM_SUCCESS;
};
// Initializes the gExecutablePath global to the location of the running
// executable. Should be called at program start.
void DetermineExecutablePath() {
ssize_t path_size =
readlink(kExeSymlink, gExecutablePath, std::size(gExecutablePath));
PCHECK(path_size >= 0) << "Failed to determine executable location";
CHECK(path_size < PATH_MAX) << "Executable path too long";
gExecutablePath[path_size] = '\0';
CHECK(gExecutablePath[0] == '/') << "Executable path not absolute";
}
// Returns the expected location of the session script based on the path to
// this executable.
std::string FindScriptPath() {
return base::FilePath(gExecutablePath).DirName().Append(kScriptName).value();
}
// Execs the me2me script.
// This function is called after forking and dropping privileges. It never
// returns.
[[noreturn]] void ExecMe2MeScript(base::EnvironmentMap environment,
const struct passwd* pwinfo,
const std::vector<std::string>& script_args) {
std::string login_shell = pwinfo->pw_shell;
if (login_shell.empty()) {
// According to "man 5 passwd", if the shell field is empty, it defaults to
// "/bin/sh".
login_shell = "/bin/sh";
}
// By convention, a login shell is signified by preceding the shell name in
// argv[0] with a '-'.
std::string shell_name = '-' + base::FilePath(login_shell).BaseName().value();
std::optional<std::string> escaped_script_path =
ShellEscapeArgument(FindScriptPath());
CHECK(escaped_script_path) << "Could not escape script path";
std::string shell_arg = *escaped_script_path + " --start --child-process";
for (const std::string& arg : script_args) {
std::optional<std::string> escaped_arg = ShellEscapeArgument(arg);
CHECK(escaped_arg) << "Could not escape script argument";
shell_arg += " ";
shell_arg += *escaped_arg;
}
environment["USER"] = pwinfo->pw_name;
environment["LOGNAME"] = pwinfo->pw_name;
environment["HOME"] = pwinfo->pw_dir;
environment["SHELL"] = login_shell;
if (!environment.count("PATH")) {
environment["PATH"] = "/bin:/usr/bin";
}
environment["CHROME_REMOTE_DESKTOP_SESSION"] = "1";
std::vector<std::string> env_strings;
for (const auto& env_var : environment) {
env_strings.emplace_back(env_var.first + "=" + env_var.second);
}
std::vector<const char*> arg_ptrs = {shell_name.c_str(), "-c",
shell_arg.c_str(), nullptr};
std::vector<const char*> env_ptrs;
env_ptrs.reserve(env_strings.size() + 1);
for (const auto& env_string : env_strings) {
env_ptrs.push_back(env_string.c_str());
}
env_ptrs.push_back(nullptr);
execve(login_shell.c_str(), const_cast<char* const*>(arg_ptrs.data()),
const_cast<char* const*>(env_ptrs.data()));
PLOG(FATAL) << "Failed to exec login shell " << login_shell;
}
// Either |user| must be set when running as root, xor the real user ID must be
// properly set when running as a user.
void Relaunch(const std::optional<std::string>& user,
const std::vector<std::string>& script_args) {
CHECK(user.has_value() == (getuid() == 0));
// Pass --foreground to continue using the same log file.
std::vector<const char*> arg_ptrs = {gExecutablePath, kStartCommand,
kForegroundFlag};
if (user) {
arg_ptrs.push_back(kUserFlag);
arg_ptrs.push_back(user->c_str());
}
arg_ptrs.push_back("--");
for (const std::string& arg : script_args) {
arg_ptrs.push_back(arg.c_str());
}
arg_ptrs.push_back(nullptr);
execv(gExecutablePath, const_cast<char* const*>(arg_ptrs.data()));
PCHECK(false) << "Failed to exec self";
}
// Runs the me2me script in a PAM session. Exits the program on failure.
// If chown_log is true, the owner and group of the file associated with stdout
// will be changed to the target user. If match_uid is specified, this function
// will fail if the final user id does not match the one provided. If
// script_args is not empty, the contained arguments will be passed on to the
// me2me script.
//
// Returns: whether the session should be relaunched.
bool ExecuteSession(std::string user,
bool chown_log,
std::optional<uid_t> match_uid,
const std::vector<std::string>& script_args) {
PamHandle pam_handle(kPamName, user.c_str(), &kPamConversation);
CHECK(pam_handle.IsInitialized()) << "Failed to initialize PAM";
// Since we're running setuid root, we don't want to risk any user-set
// environment variables changing the behavior of PAM modules, so copy any
// variables we explicitly want to preserve into the PAM session and then
// clear the environment.
for (const char* variable : kPassthroughVariables) {
char* value = std::getenv(variable);
if (value != nullptr) {
pam_handle.CheckReturnCode(pam_handle.PutEnv(variable, value),
"Environment passthrough");
}
}
clearenv();
// Set various session attributes.
pam_handle.CheckReturnCode(pam_handle.PutEnv("XDG_SESSION_CLASS", "user"),
"Set session class");
pam_handle.CheckReturnCode(pam_handle.PutEnv("XDG_SESSION_TYPE", "x11"),
"Set session type");
// Ideally, the TTY should be set to the X display for x11 sessions, but we
// don't yet know what display we'll be using. Apparently some PAM modules
// (the pam_systemd documentation explicitly calls out pam_time and
// pam_access) require PAM_TTY to be set, so we set it to a dummy value. There
// is some precedence for this, as SSH and cron set PAM_TTY to "ssh" and
// "cron" (respectively) for similar reasons.
// TODO(rkjnsn): This will prevent any PAM modules from, e.g., setting
// session-related X properties. It would be more correct to implement a two-
// phase session setup: first creating a "background/unspecified" session to
// run the me2me script and the X server, and then launching a "user/x11"
// session with PAM_TTY and PAM_XDISPLAY properly set to run the session
// chooser or the user's session script. This would also allow the inner
// session to be completely cleaned-up when the user selects "Logout" from
// within their chromoting session.
pam_handle.CheckReturnCode(
pam_handle.SetItem(PAM_TTY, "chrome-remote-desktop"), "Set PAM_TTY");
// Make sure the account is valid and enabled.
pam_handle.CheckReturnCode(pam_handle.AccountManagement(0), "Account check");
// PAM may remap the user at any stage.
user = pam_handle.GetUser().value_or(std::move(user));
// setcred explicitly does not handle user id or group membership, and
// specifies that they should be established before calling setcred. Only the
// real user id is set here, as we still require root privileges. PAM modules
// may use getpwnam, so pwinfo can only be assumed valid until the next PAM
// call.
errno = 0;
struct passwd* pwinfo = getpwnam(user.c_str());
PCHECK(pwinfo != nullptr) << "getpwnam failed";
PCHECK(setreuid(pwinfo->pw_uid, -1) == 0) << "setreuid failed";
PCHECK(setgid(pwinfo->pw_gid) == 0) << "setgid failed";
PCHECK(initgroups(pwinfo->pw_name, pwinfo->pw_gid) == 0)
<< "initgroups failed";
// The documentation states that setcred should be called before open_session,
// as done here, but it may be worth noting that `login` calls open_session
// first.
pam_handle.CheckReturnCode(pam_handle.SetCredentials(PAM_ESTABLISH_CRED),
"Set credentials");
pam_handle.CheckReturnCode(pam_handle.OpenSession(0), "Open session");
// The above may have remapped the user.
user = pam_handle.GetUser().value_or(std::move(user));
// Fetch pwinfo again, as it may have been invalidated or the user name might
// have been remapped.
pwinfo = getpwnam(user.c_str());
PCHECK(pwinfo != nullptr) << "getpwnam failed";
if (match_uid && pwinfo->pw_uid != *match_uid) {
LOG(FATAL) << "PAM remapped username to one with a different user ID.";
}
if (chown_log) {
int result = fchown(STDOUT_FILENO, pwinfo->pw_uid, pwinfo->pw_gid);
PLOG_IF(WARNING, result != 0) << "Failed to change log file owner";
result = lchown(kLatestLogSymlink, pwinfo->pw_uid, pwinfo->pw_gid);
PLOG_IF(WARNING, result != 0)
<< "Failed to change latest log symlink owner";
}
pid_t child_pid = fork();
PCHECK(child_pid >= 0) << "fork failed";
if (child_pid == 0) {
PCHECK(setuid(pwinfo->pw_uid) == 0) << "setuid failed";
PCHECK(chdir(pwinfo->pw_dir) == 0) << "chdir to $HOME failed";
std::optional<base::EnvironmentMap> pam_environment =
pam_handle.GetEnvironment();
CHECK(pam_environment) << "Failed to get environment from PAM";
// Never returns.
ExecMe2MeScript(std::move(*pam_environment), pwinfo, script_args);
} else {
// Close pipe write fd if it is open.
close(kMessageFd);
// waitpid will return if the child is ptraced, so loop until the process
// actually exits.
int status;
do {
pid_t wait_result = waitpid(child_pid, &status, 0);
// Die if wait fails so we don't close the PAM session while the child is
// still running.
PCHECK(wait_result >= 0) << "wait failed";
} while (!WIFEXITED(status) && !WIFSIGNALED(status));
bool relaunch = false;
if (WIFEXITED(status)) {
if (WEXITSTATUS(status) == EXIT_SUCCESS) {
LOG(INFO) << "Child exited successfully";
} else if (WEXITSTATUS(status) == kRelaunchExitCode) {
LOG(INFO) << "Restarting session";
relaunch = true;
} else {
LOG(WARNING) << "Child exited with status " << WEXITSTATUS(status);
}
} else if (WIFSIGNALED(status)) {
LOG(WARNING) << "Child terminated by signal " << WTERMSIG(status);
}
// Best effort PAM cleanup
if (pam_handle.CloseSession(0) != PAM_SUCCESS) {
LOG(WARNING) << "Failed to close PAM session";
}
std::ignore = pam_handle.SetCredentials(PAM_DELETE_CRED);
return relaunch;
}
}
struct LogFile {
int fd;
std::string path;
};
// Opens a temp file for logging. Exits the program on failure.
// Returns open file descriptor and path to log file.
LogFile OpenLogFile() {
char logfile[265];
std::time_t time = std::time(nullptr);
CHECK_NE(time, (std::time_t)(-1));
// Safe because we're single threaded
std::tm* localtime = std::localtime(&time);
CHECK_NE(std::strftime(logfile, sizeof(logfile), kLogFileTemplate, localtime),
static_cast<std::size_t>(0))
<< "Failed to format log file name";
mode_t mode = umask(0177);
int fd = mkstemp(logfile);
PCHECK(fd != -1) << "Failed to open log file";
// Creates a symlink to make the logs easier to find.
int symlink_ret = symlink(logfile, kLatestLogSymlink);
if (symlink_ret != 0 && errno == EEXIST) {
unlink(kLatestLogSymlink);
symlink_ret = symlink(logfile, kLatestLogSymlink);
}
PLOG_IF(ERROR, symlink_ret != 0)
<< "Failed to create log symlink to " << logfile;
umask(mode);
return {fd, logfile};
}
// Find the username for the current user. If either USER or LOGNAME is set to
// a user matching our real user id, we return that. Otherwise, we use getpwuid
// to attempt a reverse lookup. Note: It's possible for multiple usernames to
// share the same user id (e.g., to allow a user to have logins with different
// home directories or group membership, but be considered the same user as far
// as file permissions are concerned). Consulting USER/LOGNAME allows us to pick
// the correct entry in these circumstances.
std::string FindCurrentUsername() {
uid_t real_uid = getuid();
struct passwd* pwinfo;
for (const char* var : {"USER", "LOGNAME"}) {
const char* value = getenv(var);
if (value) {
pwinfo = getpwnam(value);
// USER and LOGNAME can be overridden, so make sure the value is valid
// and matches the UID of the invoking user.
if (pwinfo && pwinfo->pw_uid == real_uid) {
return pwinfo->pw_name;
}
}
}
errno = 0;
pwinfo = getpwuid(real_uid);
PCHECK(pwinfo) << "getpwuid failed";
return pwinfo->pw_name;
}
// Handle SIGINT and SIGTERM by printing a message and reraising the signal.
// This handler expects to be registered with the SA_RESETHAND and SA_NODEFER
// options to sigaction. (Don't register using signal.)
void HandleInterrupt(int signal) {
static const char kInterruptedMessage[] =
"Interrupted. The daemon is still running in the background.\n";
// Use write since fputs isn't async-signal-handler safe.
std::ignore = write(STDERR_FILENO, kInterruptedMessage,
std::size(kInterruptedMessage) - 1);
raise(signal);
}
// Handle SIGALRM timeout
void HandleAlarm(int) {
static const char kTimeoutMessage[] =
"Timeout waiting for session to start. It may have crashed, or may still "
"be running in the background.\n";
// Use write since fputs isn't async-signal-handler safe.
std::ignore =
write(STDERR_FILENO, kTimeoutMessage, std::size(kTimeoutMessage) - 1);
// A slow system or directory replication delay may cause the host to take
// longer than expected to start. Since it may still succeed, optimistically
// return success to prevent the host from being automatically unregistered.
std::_Exit(EXIT_SUCCESS);
}
// Relay messages from the host session and then exit.
void WaitForMessagesAndExit(int read_fd, const std::string& log_name) {
// Use initializer-list syntax to avoid trailing null
static const std::string_view kMessagePrefix = "MSG:";
static const std::string_view kReady = "READY\n";
struct sigaction action = {};
sigemptyset(&action.sa_mask);
action.sa_flags = SA_RESETHAND | SA_NODEFER;
// If Ctrl-C is pressed or TERM is received, inform the user that the daemon
// is still running before exiting.
action.sa_handler = HandleInterrupt;
sigaction(SIGINT, &action, nullptr);
sigaction(SIGTERM, &action, nullptr);
// Install a fallback timeout to end the parent process, in case the daemon
// never responds (e.g. host crash-looping, daemon killed).
//
// The value of 120s is chosen to match the heartbeat retry timeout in
// hearbeat_sender.cc.
action.sa_handler = HandleAlarm;
sigaction(SIGALRM, &action, nullptr);
alarm(120);
std::FILE* stream = fdopen(read_fd, "r");
char* buffer = nullptr;
std::size_t buffer_size = 0;
ssize_t line_size;
bool message_received = false;
bool host_ready = false;
while ((line_size = getline(&buffer, &buffer_size, stream)) >= 0) {
message_received = true;
std::string_view line(buffer, line_size);
if (base::StartsWith(line, kMessagePrefix, base::CompareCase::SENSITIVE)) {
line.remove_prefix(kMessagePrefix.size());
std::fwrite(line.data(), sizeof(char), line.size(), stderr);
} else if (line == kReady) {
host_ready = true;
} else {
std::fputs("Unrecognized command: ", stderr);
std::fwrite(line.data(), sizeof(char), line.size(), stderr);
}
}
// If we're not at EOF, it means a read error occured and we don't know if the
// host is still running or not. Similarly, if we received an EOF before any
// messages were received, it probably means the user's log-in shell closed
// the pipe before execing the python script, so again we don't know the state
// of the host. This latter behavior has only been observed in csh and tcsh.
// All other shells tested allowed the python script to inherit the pipe file
// descriptor without trouble.
if (!std::feof(stream) || !message_received) {
LOG(WARNING) << "Failed to read from message pipe. Please check log to "
"determine host status.\n";
// Assume host started so native messaging host allows flow to complete.
host_ready = true;
}
std::fprintf(stderr, "Log file: %s\n", log_name.c_str());
std::exit(host_ready ? EXIT_SUCCESS : EXIT_FAILURE);
}
// Daemonizes the process. Output is redirected to a log file. Exits the program
// on failure. Only returns in the child process.
//
// When executed by root (almost certainly via the init script), or if a pipe
// cannot be created, the parent will immediately exit. When executed by a
// user, the parent process will drop privileges and wait for the host to
// start, relaying any start-up messages to stdout.
//
// TODO(lambroslambrou): Having stdout/stderr redirected to a log file is not
// ideal - it could create a filesystem DoS if the daemon or a child process
// were to write excessive amounts to stdout/stderr. Ideally, stdout/stderr
// should be redirected to a pipe or socket, and a process at the other end
// should consume the data and write it to a logging facility which can do
// data-capping or log-rotation. The 'logger' command-line utility could be
// used for this, but it might cause too much syslog spam.
void Daemonize() {
// Open file descriptors before forking so errors can be reported.
LogFile log_file = OpenLogFile();
int devnull_fd = open("/dev/null", O_RDONLY);
PCHECK(devnull_fd != -1) << "Failed to open /dev/null";
uid_t real_uid = getuid();
// Set up message pipe
bool pipe_created = false;
int read_fd;
if (real_uid != 0) {
int pipe_fd[2];
int pipe_result = ::pipe(pipe_fd);
if (pipe_result != 0 || dup2(pipe_fd[1], kMessageFd) != kMessageFd) {
PLOG(WARNING) << "Failed to create message pipe. Please check log to "
"determine host status.\n";
} else {
pipe_created = true;
read_fd = pipe_fd[0];
close(pipe_fd[1]);
}
}
// Allow parent to exit, and ensure we're not a session leader so setsid can
// succeed
pid_t pid = fork();
PCHECK(pid != -1) << "fork failed";
if (pid != 0) {
if (!pipe_created) {
std::exit(EXIT_SUCCESS);
} else {
PCHECK(setuid(real_uid) == 0) << "setuid failed";
close(kMessageFd);
WaitForMessagesAndExit(read_fd, log_file.path);
NOTREACHED();
}
}
// Start a new process group and session with no controlling terminal.
PCHECK(setsid() != -1) << "setsid failed";
// Fork again so we're no longer a session leader and can't get a controlling
// terminal.
pid = fork();
PCHECK(pid != -1) << "fork failed";
if (pid != 0) {
std::exit(EXIT_SUCCESS);
}
LOG(INFO) << "Daemon process started in the background, logging to '"
<< log_file.path << "'";
// We don't want to change to the target user's home directory until we've
// dropped privileges, so change to / to make sure we're not keeping any other
// directory in use.
PCHECK(chdir("/") == 0) << "chdir / failed";
PCHECK(dup2(devnull_fd, STDIN_FILENO) != -1) << "dup2 failed";
PCHECK(dup2(log_file.fd, STDOUT_FILENO) != -1) << "dup2 failed";
PCHECK(dup2(log_file.fd, STDERR_FILENO) != -1) << "dup2 failed";
// Close all file descriptors except stdio and kMessageFd, including any we
// may have inherited.
if (pipe_created) {
base::CloseSuperfluousFds(
{base::InjectionArc(kMessageFd, kMessageFd, false)});
} else {
base::CloseSuperfluousFds(base::InjectiveMultimap());
}
}
} // namespace
int main(int argc, char** argv) {
// Initialize gExecutablePath
DetermineExecutablePath();
// This binary requires elevated privileges.
if (geteuid() != 0) {
std::fprintf(stderr,
"%s not installed setuid root. Host must be started by "
"administrator.\n",
gExecutablePath);
std::exit(EXIT_FAILURE);
}
if (argc < 2 || std::strcmp(argv[1], kStartCommand) != 0) {
PrintUsage();
std::exit(EXIT_FAILURE);
}
// Skip initial args
argc -= 2;
argv += 2;
bool foreground = false;
std::optional<std::string> user;
std::vector<std::string> script_args;
while (argc > 0) {
if (std::strcmp(argv[0], kForegroundFlag) == 0) {
foreground = true;
argc -= 1;
argv += 1;
} else if (std::strcmp(argv[0], kUserFlag) == 0 && argc >= 2) {
user = std::string(argv[1]);
argc -= 2;
argv += 2;
} else if (std::strcmp(argv[0], "--") == 0) {
argc -= 1;
argv += 1;
// Remaining args get forwarded to python script.
while (argc > 0) {
script_args.emplace_back(argv[0]);
argc -= 1;
argv += 1;
}
} else {
PrintUsage();
std::exit(EXIT_FAILURE);
}
}
uid_t real_uid = getuid();
// Note: This logic is security sensitive. It is imperative that a non-root
// user is not allowed to specify an arbitrary target user.
if (real_uid != 0) {
if (user) {
std::fputs("Target user may not be specified by non-root users.\n",
stderr);
std::exit(EXIT_FAILURE);
}
user = FindCurrentUsername();
} else {
if (!user) {
std::fputs("Target user must be specified when run as root.\n", stderr);
std::exit(EXIT_FAILURE);
}
}
if (!foreground) {
Daemonize();
}
// Daemonizing redirects stdout to a log file, which we want to be owned by
// the target user.
bool chown_stdout = !foreground;
std::optional<uid_t> match_uid =
real_uid != 0 ? std::make_optional(real_uid) : std::nullopt;
// Fork before opening PAM session so relaunches don't descend from the closed
// PAM session.
pid_t child_pid = fork();
PCHECK(child_pid >= 0) << "fork failed";
if (child_pid == 0) {
bool relaunch = ExecuteSession(std::move(*user), chown_stdout, match_uid,
std::move(script_args));
std::exit(relaunch ? kRelaunchExitCode : EXIT_SUCCESS);
} else {
// Close pipe write fd if it is open.
close(kMessageFd);
// waitpid will return if the child is ptraced, so loop until the process
// actually exits.
int status;
do {
pid_t wait_result = waitpid(child_pid, &status, 0);
// If we fail to wait on our child process, something has gone wrong and
// there's not much we can do. Note that this means if the user later logs
// out, the session won't restart.
PCHECK(wait_result >= 0) << "wait failed";
} while (!WIFEXITED(status) && !WIFSIGNALED(status));
if (WIFEXITED(status) && WEXITSTATUS(status) == kRelaunchExitCode) {
// If running as root, forward the username argument to the relaunched
// process. Otherwise, it should be inferred from the user id and
// environment.
Relaunch(real_uid == 0 ? user : std::nullopt, script_args);
}
}
return EXIT_SUCCESS;
}
|