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 <grp.h> | |
| 11 #include <pwd.h> | |
| 12 #include <sys/wait.h> | |
| 13 #include <unistd.h> | |
| 14 | |
| 15 #include <cerrno> | |
| 16 #include <cstdio> | |
| 17 #include <cstdlib> | |
| 18 #include <cstring> | |
| 19 | |
| 20 #include <memory> | |
| 21 #include <string> | |
| 22 #include <utility> | |
| 23 #include <vector> | |
| 24 | |
| 25 #include <security/pam_appl.h> | |
| 26 | |
| 27 #include "base/command_line.h" | |
| 28 #include "base/files/file_path.h" | |
| 29 #include "base/macros.h" | |
| 30 #include "base/optional.h" | |
| 31 #include "base/strings/string_piece.h" | |
| 32 | |
| 33 namespace { | |
| 34 | |
| 35 const char kPamName[] = "chrome-remote-desktop"; | |
| 36 | |
| 37 const char kHelpSwitchName[] = "help"; | |
| 38 const char kQuestionSwitchName[] = "?"; | |
| 39 const char kUserSwitchName[] = "user"; | |
| 40 const char kScriptSwitchName[] = "me2me-script"; | |
| 41 | |
| 42 const char kUsageMessage[] = | |
| 43 "Usage: %s [options]\n" | |
| 44 "\n" | |
| 45 "Options:\n" | |
| 46 " --help, -? - Print this message.\n" | |
| 47 " --user=<user> - Create session as the specified user." | |
| 48 "(Must be root.)\n" | |
| 49 " --me2me-script=<script> - Location of the me2me python script " | |
|
Sergey Ulanov
2016/09/10 00:23:16
Do we need this parameter? Can this wrapper discov
rkjnsn
2016/09/12 19:04:27
Using lstat and readlink on /proc/self/exe, along
| |
| 50 "(required)\n"; | |
| 51 | |
| 52 void PrintUsage(const base::FilePath& program_name) { | |
| 53 std::printf(kUsageMessage, program_name.MaybeAsASCII().c_str()); | |
| 54 } | |
| 55 | |
| 56 std::string ShellEscapeArgument(const base::StringPiece argument) { | |
| 57 std::string result; | |
| 58 for (char character : argument) { | |
| 59 // Some shells ascribe special meaning to some escape sequences such as \n, | |
| 60 // so don't escape any alphanumerics. (Also cuts down on verbosity.) | |
|
Jamie
2016/09/09 18:27:12
This approach (exempting characters for which a pr
rkjnsn
2016/09/09 21:24:31
This is indeed called separately for each argument
Sergey Ulanov
2016/09/10 00:23:16
Why do we need to support any shell other than bas
rkjnsn
2016/09/10 00:40:56
We need to invoke the user's login shell to pick u
rkjnsn
2016/09/12 19:33:05
In code, this would look something like this:
Jamie
2016/09/12 20:36:51
If there's precedent for this approach, then I thi
| |
| 61 if (!((character >= '0' && character <= '9') || | |
|
Lambros
2016/09/09 22:48:58
I'm not sure if it's better to use isalnum() from
rkjnsn
2016/09/10 00:06:38
I was wondering the same. Sudo uses isalnum, but i
| |
| 62 (character >= 'A' && character <= 'Z') || | |
| 63 (character >= 'a' && character <= 'z') || | |
| 64 (character == '-' || character == '_'))) | |
|
Jamie
2016/09/09 18:27:11
It's not mandated by the style guide (and we're no
rkjnsn
2016/09/09 21:24:30
Acknowledged.
Lambros
2016/09/09 22:48:58
Yep, I think we prefer braces when the 'if' condit
| |
| 65 result.push_back('\\'); | |
| 66 result.push_back(character); | |
| 67 } | |
| 68 return result; | |
| 69 } | |
| 70 | |
| 71 extern "C" int Converse(int num_msg, const struct pam_message **msg, | |
|
Lambros
2016/09/09 22:48:58
nit: Name these parameters consistently with remot
rkjnsn
2016/09/10 00:06:38
Yeah, it's basically the same purpose as PamAuthor
| |
| 72 struct pam_response **resp, void *appdata_ptr) { | |
| 73 bool failed = false; | |
| 74 | |
| 75 for (int i = 0; i < num_msg; ++i) { | |
| 76 // This is correct for the PAM included with Linux, OS X, and BSD. However, | |
| 77 // apparently Solaris and HP/UX require instead `&(*msg)[i]`. That is, they | |
| 78 // disagree as to which level of indirection contains the array. | |
| 79 const pam_message* message = msg[i]; | |
| 80 | |
| 81 switch (message->msg_style) { | |
| 82 case PAM_PROMPT_ECHO_OFF: | |
| 83 case PAM_PROMPT_ECHO_ON: | |
| 84 std::fputs("Unsupported request for user input from PAM\n", stderr); | |
|
Jamie
2016/09/09 18:27:11
LOG(ERROR) would be more appropriate here, unless
rkjnsn
2016/09/09 21:24:31
Lambros mentioned that pulling in logging would al
Jamie
2016/09/12 20:36:51
It might be worth seeing how much it increases the
| |
| 85 failed = true; | |
| 86 break; | |
| 87 case PAM_TEXT_INFO: | |
| 88 std::fprintf(stdout, "[PAM] %s", message->msg); | |
|
Jamie
2016/09/09 18:27:11
LOG(INFO), or maybe even VLOG if most users don't
| |
| 89 if (message->msg[0] == '\0' || | |
|
Lambros
2016/09/09 22:48:58
Maybe:
!StringPiece(message->msg).ends_with('\n')
rkjnsn
2016/09/10 00:06:38
I didn't notice that StringPiece also handles gett
| |
| 90 message->msg[std::strlen(message->msg) - 1] != '\n') | |
| 91 std::fputc('\n', stdout); | |
| 92 break; | |
| 93 case PAM_ERROR_MSG: | |
| 94 std::fprintf(stderr, "[PAM] %s", message->msg); | |
| 95 if (message->msg[0] == '\0' || | |
| 96 message->msg[std::strlen(message->msg) - 1] != '\n') | |
| 97 std::fputc('\n', stderr); | |
| 98 break; | |
| 99 default: | |
| 100 std::fputs("Unknown PAM conversation message style\n", stderr); | |
| 101 failed = true; | |
| 102 break; | |
| 103 } | |
| 104 } | |
| 105 | |
| 106 if (failed) | |
| 107 return PAM_CONV_ERR; | |
| 108 | |
| 109 pam_response* response = static_cast<pam_response*>( | |
| 110 calloc(num_msg, sizeof(*response))); | |
| 111 | |
| 112 if (response == nullptr) | |
| 113 return PAM_BUF_ERR; | |
| 114 | |
| 115 *resp = response; | |
| 116 return PAM_SUCCESS; | |
| 117 } | |
| 118 | |
| 119 const struct pam_conv kPamConversation = {Converse, nullptr}; | |
| 120 | |
| 121 // Wrapper class for working with PAM | |
| 122 class PamHandle { | |
| 123 public: | |
| 124 // Wrapper for environment list to properly free it on scope exit | |
|
Jamie
2016/09/09 18:27:12
This shouldn't be a nested class--Env is not an su
rkjnsn
2016/09/09 21:24:31
Acknowledged.
| |
| 125 class Env { | |
| 126 public: | |
| 127 Env(Env&& other) = default; | |
|
Lambros
2016/09/09 22:48:58
If you define a move-ctor, I think it's standard t
rkjnsn
2016/09/10 00:06:38
It is. On the other hand, this class is private to
| |
| 128 | |
| 129 ~Env() { | |
| 130 for (char* entry : env_) { | |
| 131 free(entry); | |
| 132 } | |
| 133 } | |
| 134 | |
| 135 // Set an variable in the environment. If overwrite is true, any existing | |
| 136 // value will be replaced. Otherwise, the old value will be kept. | |
| 137 | |
|
Jamie
2016/09/09 18:27:12
No newline after comments, please.
rkjnsn
2016/09/09 21:24:31
Acknowledged.
| |
| 138 void SetVariable(const char* variable, const char* value, bool overwrite) { | |
|
Jamie
2016/09/09 18:27:11
Rather than an overwrite flag, it might be better
rkjnsn
2016/09/09 21:24:31
There should only be one entry for each variable,
Jamie
2016/09/12 20:36:51
The problem is that the API contract is confusing
| |
| 139 size_t var_len = strlen(variable); | |
| 140 size_t val_len = strlen(value); | |
| 141 | |
| 142 char* new_entry = static_cast<char*>(malloc(var_len + 1 + val_len + 1)); | |
|
Lambros
2016/09/09 22:48:58
It looks like you're trying to directly operate on
rkjnsn
2016/09/10 00:06:38
So PAM returns a bunch of malloc'd c strings. The
| |
| 143 if (new_entry == nullptr) { | |
| 144 return; // Should never happen in practice, but don't crash if it does. | |
|
Lambros
2016/09/09 22:48:58
A graceful "crash" is fine. _exit(1) is a reasonab
rkjnsn
2016/09/10 00:06:38
I'm using malloc/free because PAM gives us malloc'
| |
| 145 } | |
| 146 | |
| 147 // strcpy is safe here because the buffer size is calculated from the | |
| 148 // source strings. | |
| 149 | |
| 150 strcpy(new_entry, variable); | |
| 151 new_entry[var_len] = '='; | |
| 152 strcpy(new_entry + var_len + 1, value); | |
| 153 | |
| 154 // Check if the variable already exists. | |
| 155 | |
| 156 for (char*& entry : env_) { | |
|
Jamie
2016/09/09 18:27:12
We don't use non-const references much. They obfus
rkjnsn
2016/09/09 21:24:31
I personally find this formulation clearer, but I'
| |
| 157 if (entry != nullptr && strncmp(entry, new_entry, var_len + 1) == 0) { | |
| 158 if (overwrite) { | |
| 159 free(entry); | |
| 160 entry = new_entry; | |
| 161 } else { | |
| 162 free(new_entry); | |
| 163 } | |
| 164 return; | |
| 165 } | |
| 166 } | |
| 167 env_.insert(env_.end() - 1, new_entry); // Insert before terminating null | |
|
Jamie
2016/09/09 18:27:11
push_back() would be clearer.
rkjnsn
2016/09/09 21:24:31
push_back would insert the entry after the termina
Jamie
2016/09/12 20:36:51
Ack.
| |
| 168 } | |
| 169 | |
| 170 operator char** () { | |
|
Lambros
2016/09/09 22:48:58
Operator-overloading (and overloading in general)
rkjnsn
2016/09/10 00:06:38
Acknowledged.
| |
| 171 return env_.data(); | |
| 172 } | |
| 173 private: | |
| 174 explicit Env(char** env) { | |
| 175 if (env) { | |
| 176 for (char** entry = env; *entry != nullptr; ++entry) { | |
| 177 env_.push_back(*entry); | |
| 178 } | |
| 179 env_.push_back(nullptr); | |
| 180 free(env); | |
| 181 } | |
| 182 } | |
| 183 | |
| 184 std::vector<char*> env_; | |
| 185 | |
| 186 DISALLOW_COPY_AND_ASSIGN(Env); | |
| 187 friend class PamHandle; | |
|
Lambros
2016/09/09 22:48:58
Why does PamHandle need special access? Maybe beca
rkjnsn
2016/09/10 00:06:38
The char** ctor expects to receive specifically th
| |
| 188 }; | |
| 189 | |
| 190 // Initialize PAM transaction | |
| 191 PamHandle(const char* service_name, const char* user, | |
| 192 const struct pam_conv* pam_conversation) { | |
| 193 last_retcode_ = pam_start(service_name, user, pam_conversation, &pamh_); | |
| 194 if (last_retcode_ != PAM_SUCCESS) | |
| 195 pamh_ = nullptr; | |
| 196 } | |
| 197 | |
| 198 // Terminate PAM transaction | |
| 199 ~PamHandle() { | |
| 200 if (pamh_ != nullptr) | |
| 201 pam_end(pamh_, last_retcode_); | |
| 202 } | |
| 203 | |
| 204 // True if the PAM transaction was successfully initialized. | |
| 205 operator bool() const { | |
|
Lambros
2016/09/09 22:48:58
Same comment about overloading. Maybe call it IsIn
rkjnsn
2016/09/10 00:06:38
Acknowledged.
| |
| 206 return pamh_ != nullptr; | |
| 207 } | |
| 208 | |
| 209 // Perform account validation | |
| 210 int AccountManagement(int flags) { | |
| 211 return last_retcode_ = pam_acct_mgmt(pamh_, flags); | |
| 212 } | |
| 213 | |
| 214 // Establish / delete PAM user credentials | |
| 215 int SetCredentials(int flags) { | |
| 216 return last_retcode_ = pam_setcred(pamh_, flags); | |
| 217 } | |
| 218 | |
| 219 // Start user session | |
| 220 int OpenSession(int flags) { | |
| 221 return last_retcode_ = pam_open_session(pamh_, flags); | |
| 222 } | |
| 223 | |
| 224 // End user session | |
| 225 int CloseSession(int flags) { | |
| 226 return last_retcode_ = pam_close_session(pamh_, flags); | |
| 227 } | |
| 228 | |
| 229 // Returns the current username according to PAM. It is possible for PAM | |
| 230 // modules to change this from the initial value passed to the constructor. | |
| 231 base::Optional<std::string> GetUser() { | |
| 232 const char* user; | |
| 233 last_retcode_ = pam_get_item(pamh_, PAM_USER, | |
| 234 reinterpret_cast<const void**>(&user)); | |
| 235 if (last_retcode_ != PAM_SUCCESS || user == nullptr) | |
| 236 return base::nullopt; | |
| 237 return std::string(user); | |
| 238 } | |
| 239 | |
| 240 base::Optional<Env> GetEnv() { | |
| 241 char** env = pam_getenvlist(pamh_); | |
| 242 if (env == nullptr) | |
| 243 return base::nullopt; | |
| 244 return Env(env); | |
| 245 } | |
| 246 | |
| 247 // Returns a description of the given return code | |
| 248 const char* ErrorString(int retcode) { | |
| 249 return pam_strerror(pamh_, retcode); | |
| 250 } | |
| 251 private: | |
| 252 pam_handle_t* pamh_ = nullptr; | |
|
Lambros
2016/09/09 22:48:58
Name this pam_handle_.
rkjnsn
2016/09/10 00:06:38
Acknowledged.
| |
| 253 int last_retcode_ = PAM_SUCCESS; | |
|
Lambros
2016/09/09 22:48:58
Avoid 'last', because it could mean 'final' or 'pr
rkjnsn
2016/09/10 00:06:38
Acknowledged.
| |
| 254 | |
| 255 DISALLOW_COPY_AND_ASSIGN(PamHandle); | |
| 256 }; | |
| 257 | |
| 258 #define RETURN_ON_PAM_FAILURE(retcode) \ | |
|
Jamie
2016/09/09 18:27:12
Please don't use macros if you can avoid it. In th
rkjnsn
2016/09/09 21:24:30
I figured this would make the non-error case easie
| |
| 259 do { \ | |
| 260 int my_retcode = (retcode); \ | |
| 261 if (my_retcode != PAM_SUCCESS) { \ | |
| 262 fprintf(stderr, "[PAM] %s\n", pam_handle.ErrorString(my_retcode)); \ | |
| 263 return false; \ | |
| 264 } \ | |
| 265 } while (false) | |
|
Jamie
2016/09/09 18:27:12
I've never seen do...while(false) used elsewhere;
rkjnsn
2016/09/09 21:24:30
It's a trick often used for function-style macros.
| |
| 266 | |
| 267 // Set up the PAM session and run the me2me script. Returns true if the session | |
| 268 // was executed successfully. | |
| 269 | |
| 270 bool ExecuteSession(const char* user, const char* script_path) { | |
| 271 | |
| 272 // First we set up the PAM session | |
| 273 | |
| 274 PamHandle pam_handle(kPamName, user, &kPamConversation); | |
| 275 | |
| 276 if (!pam_handle) { | |
| 277 std::fputs("Failed to initialize PAM", stderr); | |
| 278 return false; | |
| 279 } | |
| 280 | |
| 281 RETURN_ON_PAM_FAILURE(pam_handle.AccountManagement(0)); | |
| 282 | |
| 283 // The documentation states that setcred should be called before open_session, | |
| 284 // as done here, but it may me worth noting that `login` calls open_session | |
|
Jamie
2016/09/09 18:27:12
s/me/be/
Also, move this comment down to the code
rkjnsn
2016/09/09 21:24:31
Acknowledged.
| |
| 285 // first. | |
| 286 | |
| 287 std::string real_user = pam_handle.GetUser().value_or(user); | |
| 288 | |
| 289 errno = 0; | |
| 290 struct passwd* pwinfo = getpwnam(real_user.c_str()); | |
| 291 if (pwinfo == nullptr) { | |
| 292 std::perror("getpwnam"); | |
| 293 return false; | |
| 294 } | |
| 295 if (setgid(pwinfo->pw_gid) == -1) { | |
| 296 std::perror("setgid"); | |
| 297 return false; | |
| 298 } | |
| 299 if (initgroups(pwinfo->pw_name, pwinfo->pw_gid) == -1) { | |
| 300 std::perror("initgroups"); | |
| 301 return false; | |
| 302 } | |
| 303 | |
| 304 RETURN_ON_PAM_FAILURE(pam_handle.SetCredentials(PAM_ESTABLISH_CRED)); | |
| 305 RETURN_ON_PAM_FAILURE(pam_handle.OpenSession(0)); | |
| 306 base::Optional<PamHandle::Env> env = pam_handle.GetEnv(); | |
| 307 if (!env) { | |
| 308 std::fputs("Failed to get environment from PAM", stderr); | |
| 309 return false; | |
| 310 } | |
| 311 | |
| 312 // The above may have remapped the user or invalidated pwinfo, so get user | |
| 313 // info again | |
| 314 | |
| 315 real_user = pam_handle.GetUser().value_or(std::move(real_user)); | |
| 316 pwinfo = getpwnam(real_user.c_str()); | |
| 317 if (pwinfo == nullptr) { | |
| 318 std::perror("getpwnam"); | |
| 319 return false; | |
| 320 } | |
| 321 | |
| 322 // And now we're ready to fork the child. | |
| 323 | |
| 324 pid_t child = fork(); | |
| 325 if (child < 0) { // Error | |
| 326 std::perror("fork"); | |
| 327 return false; | |
| 328 } else if (child != 0) { // We're the parent | |
| 329 int status = 0; | |
| 330 do { | |
| 331 wait(&status); | |
| 332 } while (!WIFEXITED(status) && !WIFSIGNALED(status)); | |
| 333 | |
| 334 // Best effort cleanup (ignore errors) | |
| 335 | |
| 336 pam_handle.CloseSession(0); | |
| 337 pam_handle.SetCredentials(PAM_DELETE_CRED); | |
| 338 | |
| 339 if (WIFSIGNALED(status) || WEXITSTATUS(status) != EXIT_SUCCESS) | |
| 340 return false; | |
| 341 return true; | |
| 342 } else { // We're the child | |
| 343 if (setuid(pwinfo->pw_uid) == -1) { // Group ids were already set above | |
| 344 std::perror("setuid"); | |
| 345 std::exit(EXIT_FAILURE); | |
| 346 } | |
| 347 if (chdir(pwinfo->pw_dir) == -1) { | |
| 348 std::perror("chdir"); | |
| 349 std::exit(EXIT_FAILURE); | |
| 350 } | |
| 351 | |
| 352 // Set various user-related environment variables | |
| 353 | |
| 354 env->SetVariable("USER", pwinfo->pw_name, true); | |
| 355 env->SetVariable("LOGNAME", pwinfo->pw_name, true); | |
| 356 env->SetVariable("HOME", pwinfo->pw_dir, true); | |
| 357 env->SetVariable("SHELL", pwinfo->pw_shell, true); | |
| 358 | |
| 359 // Set a default PATH if one hasn't been provided through PAM | |
| 360 | |
| 361 env->SetVariable("PATH", "/bin:/usr/bin", false); | |
| 362 | |
| 363 // Finally, exec the me2me script | |
|
Jamie
2016/09/09 18:27:12
Explain why we're setting argv[0] to '-' (with a r
rkjnsn
2016/09/09 21:24:31
Acknowledged.
| |
| 364 | |
| 365 std::string shell_command = ShellEscapeArgument(script_path) + | |
| 366 " --start-foreground --keep-parent-env"; | |
| 367 const char* args[4] = { "-", "-c", shell_command.c_str(), nullptr }; | |
| 368 | |
| 369 execve(pwinfo->pw_shell, const_cast<char * const *>(args), *env); | |
|
Sergey Ulanov
2016/09/10 00:23:16
Would it make sense to use base::LaunchProcess() h
Sergey Ulanov
2016/09/10 00:23:17
Why do we need to run $SHELL here instead of /bin/
rkjnsn
2016/09/10 00:40:56
I'll take a look at LaunchProcess. See above for w
| |
| 370 std::perror("execve"); | |
| 371 std::exit(EXIT_FAILURE); | |
| 372 } | |
| 373 } | |
| 374 | |
| 375 #undef RETURN_ON_PAM_FAILURE | |
| 376 | |
| 377 } // namespace | |
| 378 | |
| 379 int main(int argc, char** argv) { | |
| 380 base::CommandLine::Init(argc, argv); | |
| 381 | |
| 382 const base::CommandLine* command_line = | |
| 383 base::CommandLine::ForCurrentProcess(); | |
| 384 if (command_line->HasSwitch(kHelpSwitchName) || | |
| 385 command_line->HasSwitch(kQuestionSwitchName)) { | |
| 386 PrintUsage(command_line->GetProgram()); | |
| 387 std::exit(EXIT_SUCCESS); | |
| 388 } | |
| 389 | |
| 390 base::FilePath script_path = | |
| 391 command_line->GetSwitchValuePath(kScriptSwitchName); | |
| 392 std::string user = command_line->GetSwitchValueNative(kUserSwitchName); | |
| 393 | |
| 394 if (script_path.empty()) { | |
| 395 std::fputs("The path to the me2me python script is required.\n", stderr); | |
| 396 std::exit(EXIT_FAILURE); | |
| 397 } | |
| 398 | |
| 399 if (user.empty()) { | |
| 400 std::fputs("Using the wrapper for the current user is not currently " | |
| 401 "implemented.\n", stderr); | |
| 402 std::exit(EXIT_FAILURE); | |
| 403 } | |
| 404 | |
| 405 // TODO(rkjnsn): daemonize before executing session. | |
| 406 if(!ExecuteSession(user.c_str(), script_path.value().c_str())) | |
| 407 exit(EXIT_FAILURE); | |
| 408 exit(EXIT_SUCCESS); | |
| 409 } | |
| OLD | NEW |