Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 // | |
| 5 // This file implements a wrapper to run the virtual me2me session within a | |
| 6 // proper PAM session. It will generally be run as root and drop privileges to | |
| 7 // the specified user before running the me2me session script. | |
| 8 | |
| 9 #include <sys/types.h> | |
| 10 #include <sys/stat.h> | |
| 11 #include <sys/wait.h> | |
| 12 #include <fcntl.h> | |
| 13 #include <grp.h> | |
| 14 #include <pwd.h> | |
| 15 #include <unistd.h> | |
| 16 | |
| 17 #include <cerrno> | |
| 18 #include <cstdio> | |
| 19 #include <cstdlib> | |
| 20 #include <cstring> | |
| 21 #include <ctime> | |
| 22 | |
| 23 #include <map> | |
| 24 #include <memory> | |
| 25 #include <string> | |
| 26 #include <tuple> | |
| 27 #include <utility> | |
| 28 #include <vector> | |
| 29 | |
| 30 #include <security/pam_appl.h> | |
| 31 | |
| 32 #include "base/command_line.h" | |
| 33 #include "base/environment.h" | |
| 34 #include "base/files/file_path.h" | |
| 35 #include "base/logging.h" | |
| 36 #include "base/macros.h" | |
| 37 #include "base/optional.h" | |
| 38 #include "base/process/launch.h" | |
| 39 #include "base/strings/string_piece.h" | |
| 40 | |
| 41 namespace { | |
| 42 | |
| 43 const char kPamName[] = "chrome-remote-desktop"; | |
| 44 | |
| 45 const char kHelpSwitchName[] = "help"; | |
| 46 const char kQuestionSwitchName[] = "?"; | |
| 47 const char kUserSwitchName[] = "user"; | |
| 48 const char kScriptSwitchName[] = "me2me-script"; | |
| 49 const char kForegroundSwitchName[] = "foreground"; | |
| 50 | |
| 51 // This template will be formatted by strftime and then used by mkstemp | |
| 52 const char kLogFileTemplate[] = | |
| 53 "/tmp/chrome_remote_desktop_%Y%m%d_%H%M%S_XXXXXX"; | |
| 54 | |
| 55 const char kUsageMessage[] = | |
| 56 "Usage: %s [options]\n" | |
| 57 "\n" | |
| 58 "Options:\n" | |
| 59 " --help, -? - Print this message.\n" | |
| 60 " --user=<user> - Create session as the specified user. " | |
| 61 "(Must run as root.)\n" | |
| 62 " --me2me-script=<script> - Location of the me2me python script " | |
| 63 "(required)\n" | |
| 64 " --foreground - Don't daemonize.\n"; | |
| 65 | |
| 66 void PrintUsage(const base::FilePath& program_name) { | |
| 67 std::printf(kUsageMessage, program_name.MaybeAsASCII().c_str()); | |
| 68 } | |
| 69 | |
| 70 // Shell-escapes a single argument in a way that is compatible with various | |
| 71 // different shells. Returns nullopt when argument contains a newline, which | |
| 72 // can't be represented in a cross-shell fashion. | |
| 73 base::Optional<std::string> ShellEscapeArgument( | |
| 74 const base::StringPiece argument) { | |
| 75 std::string result; | |
| 76 for (char character : argument) { | |
| 77 // csh in particular doesn't provide a good way to handle this | |
| 78 if (character == '\n') { | |
| 79 return base::nullopt; | |
| 80 } | |
| 81 | |
| 82 // Some shells ascribe special meaning to some escape sequences such as \t, | |
| 83 // so don't escape any alphanumerics. (Also cuts down on verbosity.) This is | |
| 84 // similar to the approach sudo takes. | |
| 85 if (!((character >= '0' && character <= '9') || | |
| 86 (character >= 'A' && character <= 'Z') || | |
| 87 (character >= 'a' && character <= 'z') || | |
| 88 (character == '-' || character == '_'))) { | |
| 89 result.push_back('\\'); | |
| 90 } | |
| 91 result.push_back(character); | |
| 92 } | |
| 93 return result; | |
| 94 } | |
| 95 | |
| 96 // PAM conversation function. Since the wrapper runs in a non-interactive | |
| 97 // context, log any messages, but return an error if asked to provide user | |
| 98 // input. | |
| 99 extern "C" int Converse(int num_messages, | |
| 100 const struct pam_message** messages, | |
| 101 struct pam_response** responses, | |
| 102 void* context) { | |
| 103 bool failed = false; | |
| 104 | |
| 105 for (int i = 0; i < num_messages; ++i) { | |
| 106 // This is correct for the PAM included with Linux, OS X, and BSD. However, | |
| 107 // apparently Solaris and HP/UX require instead `&(*msg)[i]`. That is, they | |
| 108 // disagree as to which level of indirection contains the array. | |
| 109 const pam_message* message = messages[i]; | |
| 110 | |
| 111 switch (message->msg_style) { | |
| 112 case PAM_PROMPT_ECHO_OFF: | |
| 113 case PAM_PROMPT_ECHO_ON: | |
| 114 LOG(WARNING) << "PAM requested user input (unsupported): " | |
| 115 << (message->msg ? message->msg : ""); | |
| 116 failed = true; | |
| 117 break; | |
| 118 case PAM_TEXT_INFO: | |
| 119 LOG(INFO) << "[PAM] " << (message->msg ? message->msg : ""); | |
| 120 break; | |
| 121 case PAM_ERROR_MSG: | |
| 122 LOG(WARNING) << "[PAM] " << (message->msg ? message->msg : ""); | |
|
Jamie
2016/11/16 01:27:27
Add a comment about why we're not using LOG(ERROR)
rkjnsn
2016/11/18 22:46:49
So, my thought was that such messages from PAM don
| |
| 123 break; | |
| 124 default: | |
| 125 LOG(WARNING) << "Encountered unknown PAM message style"; | |
| 126 failed = true; | |
| 127 break; | |
| 128 } | |
| 129 } | |
| 130 | |
| 131 if (failed) | |
| 132 return PAM_CONV_ERR; | |
| 133 | |
| 134 pam_response* response_list = | |
| 135 static_cast<pam_response*>(calloc(num_messages, sizeof(*response_list))); | |
| 136 | |
| 137 if (response_list == nullptr) | |
|
Lambros
2016/11/17 21:19:35
I think, if you call base::EnableTerminationOnOutO
rkjnsn
2016/11/18 22:46:49
Since this is the only manual allocation we do and
| |
| 138 return PAM_BUF_ERR; | |
| 139 | |
| 140 *responses = response_list; | |
| 141 return PAM_SUCCESS; | |
| 142 } | |
| 143 | |
| 144 const struct pam_conv kPamConversation = {Converse, nullptr}; | |
| 145 | |
| 146 // Wrapper class for working with PAM and cleaning up in an RAII fashion | |
| 147 class PamHandle { | |
| 148 public: | |
| 149 // Attempts to initialize PAM transaction. Check the result with IsInitialized | |
| 150 // before calling any other member functions. | |
| 151 PamHandle(const char* service_name, | |
| 152 const char* user, | |
| 153 const struct pam_conv* pam_conversation) { | |
| 154 last_return_code_ = | |
| 155 pam_start(service_name, user, pam_conversation, &pam_handle_); | |
| 156 if (last_return_code_ != PAM_SUCCESS) { | |
| 157 pam_handle_ = nullptr; | |
| 158 } | |
| 159 } | |
| 160 | |
| 161 // Terminates PAM transaction | |
| 162 ~PamHandle() { | |
| 163 if (pam_handle_ != nullptr) { | |
| 164 pam_end(pam_handle_, last_return_code_); | |
| 165 } | |
| 166 } | |
| 167 | |
| 168 // Checks whether the PAM transaction was successfully initialized. Only call | |
| 169 // other member functions if this returns true. | |
| 170 bool IsInitialized() const { return pam_handle_ != nullptr; } | |
| 171 | |
| 172 // Performs account validation | |
| 173 int AccountManagement(int flags) { | |
| 174 return last_return_code_ = pam_acct_mgmt(pam_handle_, flags); | |
| 175 } | |
| 176 | |
| 177 // Establishes or deletes PAM user credentials | |
| 178 int SetCredentials(int flags) { | |
| 179 return last_return_code_ = pam_setcred(pam_handle_, flags); | |
| 180 } | |
| 181 | |
| 182 // Starts user session | |
| 183 int OpenSession(int flags) { | |
| 184 return last_return_code_ = pam_open_session(pam_handle_, flags); | |
| 185 } | |
| 186 | |
| 187 // Ends user session | |
| 188 int CloseSession(int flags) { | |
| 189 return last_return_code_ = pam_close_session(pam_handle_, flags); | |
| 190 } | |
| 191 | |
| 192 // Returns the current username according to PAM. It is possible for PAM | |
| 193 // modules to change this from the initial value passed to the constructor. | |
| 194 base::Optional<std::string> GetUser() { | |
| 195 const char* user; | |
| 196 last_return_code_ = pam_get_item(pam_handle_, PAM_USER, | |
| 197 reinterpret_cast<const void**>(&user)); | |
| 198 if (last_return_code_ != PAM_SUCCESS || user == nullptr) | |
| 199 return base::nullopt; | |
| 200 return std::string(user); | |
| 201 } | |
| 202 | |
| 203 // Obtains the list of environment variables provided by PAM modules. | |
| 204 base::Optional<base::EnvironmentMap> GetEnvironment() { | |
| 205 char** environment = pam_getenvlist(pam_handle_); | |
| 206 | |
| 207 if (environment == nullptr) | |
| 208 return base::nullopt; | |
| 209 | |
| 210 base::EnvironmentMap environment_map; | |
| 211 | |
| 212 for (char ** variable = environment; *variable != nullptr; ++ variable) { | |
|
Jamie
2016/11/16 01:27:27
Nit: No space before **
Lambros
2016/11/17 21:19:35
.. or after ++
rkjnsn
2016/11/18 22:46:49
Acknowledged.
| |
| 213 char* delimiter = std::strchr(*variable, '='); | |
| 214 if (delimiter != nullptr) { | |
| 215 environment_map[std::string(*variable, delimiter)] = | |
| 216 std::string(delimiter + 1); | |
| 217 } | |
| 218 std::free(*variable); | |
| 219 } | |
| 220 std::free(environment); | |
|
Jamie
2016/11/16 01:27:27
I'm surprised to see a std:: prefix on these and o
Lambros
2016/11/17 21:19:35
If you keep std:: prefix, apply it to 'calloc' (li
rkjnsn
2016/11/18 22:46:49
On C++ projects I've worked on in the past, using
| |
| 221 | |
| 222 return environment_map; | |
| 223 } | |
| 224 | |
| 225 // Returns a description of the given return code | |
| 226 const char* ErrorString(int return_code) { | |
| 227 return pam_strerror(pam_handle_, return_code); | |
| 228 } | |
| 229 | |
| 230 // Logs a fatal error if return_code isn't PAM_SUCCESS | |
| 231 void CheckReturnCode(int return_code) { | |
|
Jamie
2016/11/16 01:27:27
Add an error string to this so it's clear from the
rkjnsn
2016/11/18 22:46:49
Acknowledged.
| |
| 232 if (return_code != PAM_SUCCESS) { | |
| 233 LOG(FATAL) << "[PAM] " << ErrorString(return_code); | |
| 234 } | |
| 235 } | |
| 236 | |
| 237 private: | |
| 238 pam_handle_t* pam_handle_ = nullptr; | |
| 239 int last_return_code_ = PAM_SUCCESS; | |
| 240 | |
| 241 DISALLOW_COPY_AND_ASSIGN(PamHandle); | |
| 242 }; | |
| 243 | |
| 244 // Sets up the PAM session and runs the me2me script. Exits the program on | |
| 245 // failure. | |
| 246 | |
| 247 void ExecuteSession(base::StringPiece user, base::StringPiece script_path) { | |
| 248 // Callback to be run in child process after fork and before exec. | |
| 249 // chdir is called manually instead of using LaunchOptions.current_directory | |
| 250 // because it should take place after setuid. | |
|
Jamie
2016/11/16 01:27:27
Is this to ensure that the correct permissions are
rkjnsn
2016/11/18 22:46:49
Partially for permissions, and partially because c
| |
| 251 class PreExecDelegate : public base::LaunchOptions::PreExecDelegate { | |
| 252 public: | |
| 253 void RunAsyncSafe() override { | |
| 254 // Use RAW_CHECK to avoid allocating post-fork. | |
| 255 RAW_CHECK(setuid(pwinfo_->pw_uid) == 0); | |
| 256 RAW_CHECK(chdir(pwinfo_->pw_dir) == 0); | |
| 257 } | |
| 258 | |
| 259 PreExecDelegate(struct passwd* pwinfo) : pwinfo_(pwinfo) {} | |
| 260 | |
| 261 private: | |
| 262 struct passwd* pwinfo_; | |
| 263 }; | |
| 264 | |
| 265 // real_user may change as PAM processing progresses. | |
| 266 std::string real_user = user.as_string(); | |
| 267 | |
| 268 // First we set up the PAM session | |
| 269 | |
|
Jamie
2016/11/16 01:27:27
No newline here, please.
rkjnsn
2016/11/18 22:46:49
I added a newline to try to indicate that this com
Jamie
2016/11/18 22:56:08
In some cases we've used a multi-line comment. If
| |
| 270 PamHandle pam_handle(kPamName, real_user.c_str(), &kPamConversation); | |
| 271 | |
| 272 CHECK(pam_handle.IsInitialized()) << "Failed to initialize PAM"; | |
| 273 | |
| 274 pam_handle.CheckReturnCode(pam_handle.AccountManagement(0)); | |
| 275 | |
| 276 // PAM may remap the user at any stage | |
| 277 real_user = pam_handle.GetUser().value_or(std::move(real_user)); | |
| 278 | |
| 279 // setcred explicitly does not handle group membership, and specifies that | |
| 280 // group membership should be established before calling setcred. PAM modules | |
| 281 // may also use getpwnam, so pwinfo can only be assumed valid until the next | |
| 282 // PAM call. | |
| 283 errno = 0; | |
| 284 struct passwd* pwinfo = getpwnam(real_user.c_str()); | |
| 285 PCHECK(pwinfo != nullptr) << "getpwnam failed"; | |
| 286 PCHECK(setgid(pwinfo->pw_gid) == 0) << "setgid failed"; | |
| 287 PCHECK(initgroups(pwinfo->pw_name, pwinfo->pw_gid) == 0) | |
| 288 << "initgroups failed"; | |
| 289 | |
| 290 // The documentation states that setcred should be called before open_session, | |
| 291 // as done here, but it may be worth noting that `login` calls open_session | |
| 292 // first. | |
| 293 pam_handle.CheckReturnCode(pam_handle.SetCredentials(PAM_ESTABLISH_CRED)); | |
| 294 | |
| 295 pam_handle.CheckReturnCode(pam_handle.OpenSession(0)); | |
| 296 base::Optional<base::EnvironmentMap> pam_environment = | |
| 297 pam_handle.GetEnvironment(); | |
| 298 CHECK(pam_environment) << "Failed to get environment from PAM"; | |
| 299 | |
| 300 // The above may have remapped the user or invalidated pwinfo, so get user | |
| 301 // info again | |
| 302 real_user = pam_handle.GetUser().value_or(std::move(real_user)); | |
| 303 pwinfo = getpwnam(real_user.c_str()); | |
| 304 PCHECK(pwinfo != nullptr) << "getpwnam failed"; | |
| 305 | |
| 306 // Attempt to change log owner to the user, but don't worry if it fails. | |
| 307 fchown(STDOUT_FILENO, pwinfo->pw_uid, pwinfo->pw_gid); | |
|
rkjnsn
2016/11/15 22:52:57
If root manually runs the wrapper with the --foreg
Jamie
2016/11/16 01:27:28
I think this is fine as-is.
| |
| 308 | |
| 309 // And now we're ready to fork the child. | |
| 310 | |
| 311 PreExecDelegate pre_exec_delegate(pwinfo); | |
| 312 | |
| 313 base::LaunchOptions launch_options; | |
| 314 | |
| 315 // Required to allow suid binaries to function in the session. | |
| 316 launch_options.allow_new_privs = true; | |
| 317 | |
| 318 launch_options.kill_on_parent_death = true; | |
| 319 | |
| 320 launch_options.clear_environ = true; | |
| 321 launch_options.environ = std::move(*pam_environment); | |
| 322 launch_options.environ["USER"] = pwinfo->pw_name; | |
| 323 launch_options.environ["LOGNAME"] = pwinfo->pw_name; | |
| 324 launch_options.environ["HOME"] = pwinfo->pw_dir; | |
| 325 launch_options.environ["SHELL"] = pwinfo->pw_shell; | |
| 326 if (!launch_options.environ.count("PATH")) { | |
| 327 launch_options.environ["PATH"] = "/bin:/usr/bin"; | |
| 328 } | |
| 329 | |
| 330 launch_options.pre_exec_delegate = &pre_exec_delegate; | |
| 331 | |
| 332 // By convention, a login shell is signified by preceeding the shell name in | |
| 333 // argv[0] with a '-'. | |
| 334 base::CommandLine command_line(base::FilePath( | |
| 335 '-' + base::FilePath(pwinfo->pw_shell).BaseName().value())); | |
| 336 | |
| 337 base::Optional<std::string> escaped_script_path = | |
| 338 ShellEscapeArgument(script_path); | |
| 339 | |
| 340 CHECK(escaped_script_path) << "Could not escape script path"; | |
| 341 | |
| 342 command_line.AppendSwitch("-c"); | |
| 343 command_line.AppendArg(*escaped_script_path + | |
| 344 " --start --foreground --keep-parent-env"); | |
| 345 | |
| 346 // Tell LaunchProcess where to find the executable, since argv[0] doesn't | |
| 347 // point to it. | |
| 348 launch_options.real_path = base::FilePath(pwinfo->pw_shell); | |
| 349 | |
| 350 base::Process child = base::LaunchProcess(command_line, launch_options); | |
| 351 | |
| 352 if (child.IsValid()) { | |
| 353 int exit_code = 0; | |
| 354 // Die if wait fails so we don't close the PAM session while the child is | |
| 355 // still running. | |
| 356 CHECK(child.WaitForExit(&exit_code)) << "Failed to wait for child process"; | |
| 357 LOG_IF(WARNING, exit_code != 0) << "Child did not exit normally"; | |
| 358 } | |
| 359 | |
| 360 // Best effort PAM cleanup (ignore errors) | |
| 361 pam_handle.CloseSession(0); | |
| 362 pam_handle.SetCredentials(PAM_DELETE_CRED); | |
| 363 } | |
| 364 | |
| 365 // Opens a temp file for logging. Exits the program on failure. | |
| 366 int OpenLogFile() { | |
|
rkjnsn
2016/11/15 22:52:57
The Python script provides two ways to customize t
Lambros
2016/11/17 21:19:35
We could let the script manage the log file (and a
rkjnsn
2016/11/18 22:46:49
We would need some kind of "don't daemonize but st
| |
| 367 char logfile[265]; | |
| 368 std::time_t time = std::time(nullptr); | |
| 369 CHECK_NE(time, (std::time_t)(-1)); | |
| 370 // Safe because we're single threaded | |
| 371 std::tm* localtime = std::localtime(&time); | |
| 372 CHECK_NE(std::strftime(logfile, sizeof(logfile), kLogFileTemplate, localtime), | |
| 373 (std::size_t) 0); | |
| 374 | |
| 375 mode_t mode = umask(0177); | |
| 376 int fd = mkstemp(logfile); | |
| 377 PCHECK(fd != -1); | |
| 378 umask(mode); | |
| 379 | |
| 380 return fd; | |
| 381 } | |
| 382 | |
| 383 // Daemonizes the process. Exits the program on failure. | |
| 384 void Daemonize() { | |
|
Jamie
2016/11/16 01:27:27
It's a bit odd to have the method declaration in t
rkjnsn
2016/11/18 22:46:49
In my head, these were two distinct comments. The
| |
| 385 // This logic is mostly the same as daemonize() in linux_me2me_host.py. Log- | |
| 386 // file redirection especially should be kept in sync. Note that this does | |
| 387 // not currently wait for the host to start successfully before exiting the | |
| 388 // parent process like the Python script does, as that functionality is | |
| 389 // probably not useful at boot, where the wrapper is expected to be used. If | |
| 390 // it turns out to be desired, it can be implemented by setting up a pipe and | |
| 391 // passing a file descriptor to the Python script. | |
| 392 | |
| 393 int log_fd = OpenLogFile(); | |
| 394 int devnull_fd = open("/dev/null", O_RDONLY); | |
| 395 PCHECK(devnull_fd != -1); | |
| 396 | |
| 397 PCHECK(dup2(devnull_fd, STDIN_FILENO) != -1); | |
| 398 PCHECK(dup2(log_fd, STDOUT_FILENO) != -1); | |
| 399 PCHECK(dup2(log_fd, STDERR_FILENO) != -1); | |
| 400 | |
| 401 // Close all file descriptors except stdio, including any we may have | |
| 402 // inherited. | |
| 403 base::CloseSuperfluousFds(base::InjectiveMultimap()); | |
| 404 | |
| 405 // Allow parent to exit, and ensure we're not a session leader so setsid can | |
| 406 // succeed | |
| 407 pid_t pid = fork(); | |
| 408 PCHECK(pid != -1); | |
| 409 | |
| 410 if (pid != 0) { | |
| 411 std::exit(EXIT_SUCCESS); | |
| 412 } | |
| 413 | |
| 414 // Start a new process group and session with no controlling terminal. | |
| 415 PCHECK(setsid() != -1); | |
| 416 | |
| 417 // Fork again so we're no longer a session leader and can't get a controlling | |
| 418 // terminal. | |
| 419 pid = fork(); | |
| 420 PCHECK(pid != -1); | |
| 421 | |
| 422 if (pid != 0) { | |
| 423 std::exit(EXIT_SUCCESS); | |
| 424 } | |
| 425 | |
| 426 // We don't want to change to the target user's home directory until we've | |
| 427 // dropped privileges, so change to / to make sure we're not keeping any other | |
| 428 // directory in use. | |
| 429 PCHECK(chdir("/") == 0); | |
| 430 | |
| 431 // Done! | |
|
rkjnsn
2016/11/15 22:52:57
I could print the name of the log file here like t
Lambros
2016/11/17 21:19:35
We shouldn't pollute the init.d system output.
Log
| |
| 432 } | |
| 433 | |
| 434 } // namespace | |
| 435 | |
| 436 int main(int argc, char** argv) { | |
| 437 base::CommandLine::Init(argc, argv); | |
| 438 | |
| 439 const base::CommandLine* command_line = | |
| 440 base::CommandLine::ForCurrentProcess(); | |
| 441 if (command_line->HasSwitch(kHelpSwitchName) || | |
| 442 command_line->HasSwitch(kQuestionSwitchName)) { | |
| 443 PrintUsage(command_line->GetProgram()); | |
| 444 std::exit(EXIT_SUCCESS); | |
| 445 } | |
| 446 | |
| 447 base::FilePath script_path = | |
| 448 command_line->GetSwitchValuePath(kScriptSwitchName); | |
| 449 std::string user = command_line->GetSwitchValueNative(kUserSwitchName); | |
| 450 | |
| 451 if (script_path.empty()) { | |
| 452 std::fputs("The path to the me2me python script is required.\n", stderr); | |
| 453 std::exit(EXIT_FAILURE); | |
| 454 } | |
| 455 | |
| 456 if (user.empty()) { | |
| 457 std::fputs("The target user must be specified.\n", stderr); | |
| 458 std::exit(EXIT_FAILURE); | |
| 459 } | |
| 460 | |
| 461 if (!command_line->HasSwitch(kForegroundSwitchName)) { | |
| 462 Daemonize(); | |
| 463 } | |
| 464 | |
| 465 ExecuteSession(user, script_path.value()); | |
| 466 } | |
| OLD | NEW |