Chromium Code Reviews| Index: remoting/host/linux/remoting_pam_session.cc |
| diff --git a/remoting/host/linux/remoting_pam_session.cc b/remoting/host/linux/remoting_pam_session.cc |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..94cb2254e2926f49f5112789d3858da2f62b2b6d |
| --- /dev/null |
| +++ b/remoting/host/linux/remoting_pam_session.cc |
| @@ -0,0 +1,409 @@ |
| +// Copyright 2016 The Chromium Authors. All rights reserved. |
| +// 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. |
| + |
| +#include <sys/types.h> |
| +#include <grp.h> |
| +#include <pwd.h> |
| +#include <sys/wait.h> |
| +#include <unistd.h> |
| + |
| +#include <cerrno> |
| +#include <cstdio> |
| +#include <cstdlib> |
| +#include <cstring> |
| + |
| +#include <memory> |
| +#include <string> |
| +#include <utility> |
| +#include <vector> |
| + |
| +#include <security/pam_appl.h> |
| + |
| +#include "base/command_line.h" |
| +#include "base/files/file_path.h" |
| +#include "base/macros.h" |
| +#include "base/optional.h" |
| +#include "base/strings/string_piece.h" |
| + |
| +namespace { |
| + |
| +const char kPamName[] = "chrome-remote-desktop"; |
| + |
| +const char kHelpSwitchName[] = "help"; |
| +const char kQuestionSwitchName[] = "?"; |
| +const char kUserSwitchName[] = "user"; |
| +const char kScriptSwitchName[] = "me2me-script"; |
| + |
| +const char kUsageMessage[] = |
| + "Usage: %s [options]\n" |
| + "\n" |
| + "Options:\n" |
| + " --help, -? - Print this message.\n" |
| + " --user=<user> - Create session as the specified user." |
| + "(Must be root.)\n" |
| + " --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
|
| + "(required)\n"; |
| + |
| +void PrintUsage(const base::FilePath& program_name) { |
| + std::printf(kUsageMessage, program_name.MaybeAsASCII().c_str()); |
| +} |
| + |
| +std::string ShellEscapeArgument(const base::StringPiece argument) { |
| + std::string result; |
| + for (char character : argument) { |
| + // Some shells ascribe special meaning to some escape sequences such as \n, |
| + // 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
|
| + 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
|
| + (character >= 'A' && character <= 'Z') || |
| + (character >= 'a' && character <= 'z') || |
| + (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
|
| + result.push_back('\\'); |
| + result.push_back(character); |
| + } |
| + return result; |
| +} |
| + |
| +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
|
| + struct pam_response **resp, void *appdata_ptr) { |
| + bool failed = false; |
| + |
| + for (int i = 0; i < num_msg; ++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 = msg[i]; |
| + |
| + switch (message->msg_style) { |
| + case PAM_PROMPT_ECHO_OFF: |
| + case PAM_PROMPT_ECHO_ON: |
| + 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
|
| + failed = true; |
| + break; |
| + case PAM_TEXT_INFO: |
| + 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
|
| + 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
|
| + message->msg[std::strlen(message->msg) - 1] != '\n') |
| + std::fputc('\n', stdout); |
| + break; |
| + case PAM_ERROR_MSG: |
| + std::fprintf(stderr, "[PAM] %s", message->msg); |
| + if (message->msg[0] == '\0' || |
| + message->msg[std::strlen(message->msg) - 1] != '\n') |
| + std::fputc('\n', stderr); |
| + break; |
| + default: |
| + std::fputs("Unknown PAM conversation message style\n", stderr); |
| + failed = true; |
| + break; |
| + } |
| + } |
| + |
| + if (failed) |
| + return PAM_CONV_ERR; |
| + |
| + pam_response* response = static_cast<pam_response*>( |
| + calloc(num_msg, sizeof(*response))); |
| + |
| + if (response == nullptr) |
| + return PAM_BUF_ERR; |
| + |
| + *resp = response; |
| + return PAM_SUCCESS; |
| +} |
| + |
| +const struct pam_conv kPamConversation = {Converse, nullptr}; |
| + |
| +// Wrapper class for working with PAM |
| +class PamHandle { |
| + public: |
| + // 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.
|
| + class Env { |
| + public: |
| + 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
|
| + |
| + ~Env() { |
| + for (char* entry : env_) { |
| + free(entry); |
| + } |
| + } |
| + |
| + // Set an variable in the environment. If overwrite is true, any existing |
| + // value will be replaced. Otherwise, the old value will be kept. |
| + |
|
Jamie
2016/09/09 18:27:12
No newline after comments, please.
rkjnsn
2016/09/09 21:24:31
Acknowledged.
|
| + 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
|
| + size_t var_len = strlen(variable); |
| + size_t val_len = strlen(value); |
| + |
| + 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
|
| + if (new_entry == nullptr) { |
| + 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'
|
| + } |
| + |
| + // strcpy is safe here because the buffer size is calculated from the |
| + // source strings. |
| + |
| + strcpy(new_entry, variable); |
| + new_entry[var_len] = '='; |
| + strcpy(new_entry + var_len + 1, value); |
| + |
| + // Check if the variable already exists. |
| + |
| + 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'
|
| + if (entry != nullptr && strncmp(entry, new_entry, var_len + 1) == 0) { |
| + if (overwrite) { |
| + free(entry); |
| + entry = new_entry; |
| + } else { |
| + free(new_entry); |
| + } |
| + return; |
| + } |
| + } |
| + 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.
|
| + } |
| + |
| + operator char** () { |
|
Lambros
2016/09/09 22:48:58
Operator-overloading (and overloading in general)
rkjnsn
2016/09/10 00:06:38
Acknowledged.
|
| + return env_.data(); |
| + } |
| + private: |
| + explicit Env(char** env) { |
| + if (env) { |
| + for (char** entry = env; *entry != nullptr; ++entry) { |
| + env_.push_back(*entry); |
| + } |
| + env_.push_back(nullptr); |
| + free(env); |
| + } |
| + } |
| + |
| + std::vector<char*> env_; |
| + |
| + DISALLOW_COPY_AND_ASSIGN(Env); |
| + 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
|
| + }; |
| + |
| + // Initialize PAM transaction |
| + PamHandle(const char* service_name, const char* user, |
| + const struct pam_conv* pam_conversation) { |
| + last_retcode_ = pam_start(service_name, user, pam_conversation, &pamh_); |
| + if (last_retcode_ != PAM_SUCCESS) |
| + pamh_ = nullptr; |
| + } |
| + |
| + // Terminate PAM transaction |
| + ~PamHandle() { |
| + if (pamh_ != nullptr) |
| + pam_end(pamh_, last_retcode_); |
| + } |
| + |
| + // True if the PAM transaction was successfully initialized. |
| + 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.
|
| + return pamh_ != nullptr; |
| + } |
| + |
| + // Perform account validation |
| + int AccountManagement(int flags) { |
| + return last_retcode_ = pam_acct_mgmt(pamh_, flags); |
| + } |
| + |
| + // Establish / delete PAM user credentials |
| + int SetCredentials(int flags) { |
| + return last_retcode_ = pam_setcred(pamh_, flags); |
| + } |
| + |
| + // Start user session |
| + int OpenSession(int flags) { |
| + return last_retcode_ = pam_open_session(pamh_, flags); |
| + } |
| + |
| + // End user session |
| + int CloseSession(int flags) { |
| + return last_retcode_ = pam_close_session(pamh_, flags); |
| + } |
| + |
| + // Returns the current username according to PAM. It is possible for PAM |
| + // modules to change this from the initial value passed to the constructor. |
| + base::Optional<std::string> GetUser() { |
| + const char* user; |
| + last_retcode_ = pam_get_item(pamh_, PAM_USER, |
| + reinterpret_cast<const void**>(&user)); |
| + if (last_retcode_ != PAM_SUCCESS || user == nullptr) |
| + return base::nullopt; |
| + return std::string(user); |
| + } |
| + |
| + base::Optional<Env> GetEnv() { |
| + char** env = pam_getenvlist(pamh_); |
| + if (env == nullptr) |
| + return base::nullopt; |
| + return Env(env); |
| + } |
| + |
| + // Returns a description of the given return code |
| + const char* ErrorString(int retcode) { |
| + return pam_strerror(pamh_, retcode); |
| + } |
| + private: |
| + pam_handle_t* pamh_ = nullptr; |
|
Lambros
2016/09/09 22:48:58
Name this pam_handle_.
rkjnsn
2016/09/10 00:06:38
Acknowledged.
|
| + 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.
|
| + |
| + DISALLOW_COPY_AND_ASSIGN(PamHandle); |
| +}; |
| + |
| +#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
|
| + do { \ |
| + int my_retcode = (retcode); \ |
| + if (my_retcode != PAM_SUCCESS) { \ |
| + fprintf(stderr, "[PAM] %s\n", pam_handle.ErrorString(my_retcode)); \ |
| + return false; \ |
| + } \ |
| + } 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.
|
| + |
| +// Set up the PAM session and run the me2me script. Returns true if the session |
| +// was executed successfully. |
| + |
| +bool ExecuteSession(const char* user, const char* script_path) { |
| + |
| + // First we set up the PAM session |
| + |
| + PamHandle pam_handle(kPamName, user, &kPamConversation); |
| + |
| + if (!pam_handle) { |
| + std::fputs("Failed to initialize PAM", stderr); |
| + return false; |
| + } |
| + |
| + RETURN_ON_PAM_FAILURE(pam_handle.AccountManagement(0)); |
| + |
| + // The documentation states that setcred should be called before open_session, |
| + // 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.
|
| + // first. |
| + |
| + std::string real_user = pam_handle.GetUser().value_or(user); |
| + |
| + errno = 0; |
| + struct passwd* pwinfo = getpwnam(real_user.c_str()); |
| + if (pwinfo == nullptr) { |
| + std::perror("getpwnam"); |
| + return false; |
| + } |
| + if (setgid(pwinfo->pw_gid) == -1) { |
| + std::perror("setgid"); |
| + return false; |
| + } |
| + if (initgroups(pwinfo->pw_name, pwinfo->pw_gid) == -1) { |
| + std::perror("initgroups"); |
| + return false; |
| + } |
| + |
| + RETURN_ON_PAM_FAILURE(pam_handle.SetCredentials(PAM_ESTABLISH_CRED)); |
| + RETURN_ON_PAM_FAILURE(pam_handle.OpenSession(0)); |
| + base::Optional<PamHandle::Env> env = pam_handle.GetEnv(); |
| + if (!env) { |
| + std::fputs("Failed to get environment from PAM", stderr); |
| + return false; |
| + } |
| + |
| + // The above may have remapped the user or invalidated pwinfo, so get user |
| + // info again |
| + |
| + real_user = pam_handle.GetUser().value_or(std::move(real_user)); |
| + pwinfo = getpwnam(real_user.c_str()); |
| + if (pwinfo == nullptr) { |
| + std::perror("getpwnam"); |
| + return false; |
| + } |
| + |
| + // And now we're ready to fork the child. |
| + |
| + pid_t child = fork(); |
| + if (child < 0) { // Error |
| + std::perror("fork"); |
| + return false; |
| + } else if (child != 0) { // We're the parent |
| + int status = 0; |
| + do { |
| + wait(&status); |
| + } while (!WIFEXITED(status) && !WIFSIGNALED(status)); |
| + |
| + // Best effort cleanup (ignore errors) |
| + |
| + pam_handle.CloseSession(0); |
| + pam_handle.SetCredentials(PAM_DELETE_CRED); |
| + |
| + if (WIFSIGNALED(status) || WEXITSTATUS(status) != EXIT_SUCCESS) |
| + return false; |
| + return true; |
| + } else { // We're the child |
| + if (setuid(pwinfo->pw_uid) == -1) { // Group ids were already set above |
| + std::perror("setuid"); |
| + std::exit(EXIT_FAILURE); |
| + } |
| + if (chdir(pwinfo->pw_dir) == -1) { |
| + std::perror("chdir"); |
| + std::exit(EXIT_FAILURE); |
| + } |
| + |
| + // Set various user-related environment variables |
| + |
| + env->SetVariable("USER", pwinfo->pw_name, true); |
| + env->SetVariable("LOGNAME", pwinfo->pw_name, true); |
| + env->SetVariable("HOME", pwinfo->pw_dir, true); |
| + env->SetVariable("SHELL", pwinfo->pw_shell, true); |
| + |
| + // Set a default PATH if one hasn't been provided through PAM |
| + |
| + env->SetVariable("PATH", "/bin:/usr/bin", false); |
| + |
| + // 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.
|
| + |
| + std::string shell_command = ShellEscapeArgument(script_path) + |
| + " --start-foreground --keep-parent-env"; |
| + const char* args[4] = { "-", "-c", shell_command.c_str(), nullptr }; |
| + |
| + 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
|
| + std::perror("execve"); |
| + std::exit(EXIT_FAILURE); |
| + } |
| +} |
| + |
| +#undef RETURN_ON_PAM_FAILURE |
| + |
| +} // namespace |
| + |
| +int main(int argc, char** argv) { |
| + base::CommandLine::Init(argc, argv); |
| + |
| + const base::CommandLine* command_line = |
| + base::CommandLine::ForCurrentProcess(); |
| + if (command_line->HasSwitch(kHelpSwitchName) || |
| + command_line->HasSwitch(kQuestionSwitchName)) { |
| + PrintUsage(command_line->GetProgram()); |
| + std::exit(EXIT_SUCCESS); |
| + } |
| + |
| + base::FilePath script_path = |
| + command_line->GetSwitchValuePath(kScriptSwitchName); |
| + std::string user = command_line->GetSwitchValueNative(kUserSwitchName); |
| + |
| + if (script_path.empty()) { |
| + std::fputs("The path to the me2me python script is required.\n", stderr); |
| + std::exit(EXIT_FAILURE); |
| + } |
| + |
| + if (user.empty()) { |
| + std::fputs("Using the wrapper for the current user is not currently " |
| + "implemented.\n", stderr); |
| + std::exit(EXIT_FAILURE); |
| + } |
| + |
| + // TODO(rkjnsn): daemonize before executing session. |
| + if(!ExecuteSession(user.c_str(), script_path.value().c_str())) |
| + exit(EXIT_FAILURE); |
| + exit(EXIT_SUCCESS); |
| +} |