Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(951)

Unified Diff: remoting/host/linux/remoting_pam_session.cc

Issue 2323153002: Add PAM session wrapper (Closed)
Patch Set: Add PAM session wrapper Created 4 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« remoting/host/BUILD.gn ('K') | « remoting/host/BUILD.gn ('k') | no next file » | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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);
+}
« remoting/host/BUILD.gn ('K') | « remoting/host/BUILD.gn ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698