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

Side by Side Diff: remoting/host/linux/remoting_user_session.cc

Issue 2323153002: Add PAM session wrapper (Closed)
Patch Set: Option A: Use wrapper for boot only Created 4 years, 1 month 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 unified diff | Download patch
OLDNEW
(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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698