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

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

Issue 2323153002: Add PAM session wrapper (Closed)
Patch Set: Address feedback Created 4 years 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
« no previous file with comments | « remoting/host/linux/linux_me2me_host.py ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 // Error messages from PAM are not necessarily fatal to the operation,
123 // as the module may be optional.
124 LOG(WARNING) << "[PAM] " << (message->msg ? message->msg : "");
125 break;
126 default:
127 LOG(WARNING) << "Encountered unknown PAM message style";
128 failed = true;
129 break;
130 }
131 }
132
133 if (failed)
134 return PAM_CONV_ERR;
135
136 pam_response* response_list = static_cast<pam_response*>(
137 std::calloc(num_messages, sizeof(*response_list)));
138
139 if (response_list == nullptr)
140 return PAM_BUF_ERR;
141
142 *responses = response_list;
143 return PAM_SUCCESS;
144 }
145
146 const struct pam_conv kPamConversation = {Converse, nullptr};
147
148 // Wrapper class for working with PAM and cleaning up in an RAII fashion
149 class PamHandle {
150 public:
151 // Attempts to initialize PAM transaction. Check the result with IsInitialized
152 // before calling any other member functions.
153 PamHandle(const char* service_name,
154 const char* user,
155 const struct pam_conv* pam_conversation) {
156 last_return_code_ =
157 pam_start(service_name, user, pam_conversation, &pam_handle_);
158 if (last_return_code_ != PAM_SUCCESS) {
159 pam_handle_ = nullptr;
160 }
161 }
162
163 // Terminates PAM transaction
164 ~PamHandle() {
165 if (pam_handle_ != nullptr) {
166 pam_end(pam_handle_, last_return_code_);
167 }
168 }
169
170 // Checks whether the PAM transaction was successfully initialized. Only call
171 // other member functions if this returns true.
172 bool IsInitialized() const { return pam_handle_ != nullptr; }
173
174 // Performs account validation
175 int AccountManagement(int flags) {
176 return last_return_code_ = pam_acct_mgmt(pam_handle_, flags);
177 }
178
179 // Establishes or deletes PAM user credentials
180 int SetCredentials(int flags) {
181 return last_return_code_ = pam_setcred(pam_handle_, flags);
182 }
183
184 // Starts user session
185 int OpenSession(int flags) {
186 return last_return_code_ = pam_open_session(pam_handle_, flags);
187 }
188
189 // Ends user session
190 int CloseSession(int flags) {
191 return last_return_code_ = pam_close_session(pam_handle_, flags);
192 }
193
194 // Returns the current username according to PAM. It is possible for PAM
195 // modules to change this from the initial value passed to the constructor.
196 base::Optional<std::string> GetUser() {
197 const char* user;
198 last_return_code_ = pam_get_item(pam_handle_, PAM_USER,
199 reinterpret_cast<const void**>(&user));
200 if (last_return_code_ != PAM_SUCCESS || user == nullptr)
201 return base::nullopt;
202 return std::string(user);
203 }
204
205 // Obtains the list of environment variables provided by PAM modules.
206 base::Optional<base::EnvironmentMap> GetEnvironment() {
207 char** environment = pam_getenvlist(pam_handle_);
208
209 if (environment == nullptr)
210 return base::nullopt;
211
212 base::EnvironmentMap environment_map;
213
214 for (char** variable = environment; *variable != nullptr; ++variable) {
215 char* delimiter = std::strchr(*variable, '=');
216 if (delimiter != nullptr) {
217 environment_map[std::string(*variable, delimiter)] =
218 std::string(delimiter + 1);
219 }
220 std::free(*variable);
221 }
222 std::free(environment);
223
224 return environment_map;
225 }
226
227 // Returns a description of the given return code
228 const char* ErrorString(int return_code) {
229 return pam_strerror(pam_handle_, return_code);
230 }
231
232 // Logs a fatal error if return_code isn't PAM_SUCCESS
233 void CheckReturnCode(int return_code, base::StringPiece what) {
234 if (return_code != PAM_SUCCESS) {
235 LOG(FATAL) << "[PAM] " << what << ": " << ErrorString(return_code);
236 }
237 }
238
239 private:
240 pam_handle_t* pam_handle_ = nullptr;
241 int last_return_code_ = PAM_SUCCESS;
242
243 DISALLOW_COPY_AND_ASSIGN(PamHandle);
244 };
245
246 // Runs the me2me script in a PAM session. Exits the program on failure.
247 // If chown_log is true, the owner and group of the file associated with stdout
248 // will be changed to the target user.
249 void ExecuteSession(std::string user, base::StringPiece script_path,
250 bool chown_log) {
251 //////////////////////////////////////////////////////////////////////////////
252 // Set up the PAM session
253 //////////////////////////////////////////////////////////////////////////////
254
255 PamHandle pam_handle(kPamName, user.c_str(), &kPamConversation);
256 CHECK(pam_handle.IsInitialized()) << "Failed to initialize PAM";
257
258 // Make sure the account is valid and enabled.
259 pam_handle.CheckReturnCode(pam_handle.AccountManagement(0), "Account check");
260
261 // PAM may remap the user at any stage.
262 user = pam_handle.GetUser().value_or(std::move(user));
263
264 // setcred explicitly does not handle group membership, and specifies that
265 // group membership should be established before calling setcred. PAM modules
266 // may also use getpwnam, so pwinfo can only be assumed valid until the next
267 // PAM call.
268 errno = 0;
269 struct passwd* pwinfo = getpwnam(user.c_str());
270 PCHECK(pwinfo != nullptr) << "getpwnam failed";
271 PCHECK(setgid(pwinfo->pw_gid) == 0) << "setgid failed";
272 PCHECK(initgroups(pwinfo->pw_name, pwinfo->pw_gid) == 0)
273 << "initgroups failed";
274
275 // The documentation states that setcred should be called before open_session,
276 // as done here, but it may be worth noting that `login` calls open_session
277 // first.
278 pam_handle.CheckReturnCode(pam_handle.SetCredentials(PAM_ESTABLISH_CRED),
279 "Set credentials");
280
281 pam_handle.CheckReturnCode(pam_handle.OpenSession(0), "Open session");
282
283 // The above may have remapped the user.
284 user = pam_handle.GetUser().value_or(std::move(user));
285
286 base::Optional<base::EnvironmentMap> pam_environment =
287 pam_handle.GetEnvironment();
288 CHECK(pam_environment) << "Failed to get environment from PAM";
289
290 //////////////////////////////////////////////////////////////////////////////
291 // Prepare to execute remoting session process
292 //////////////////////////////////////////////////////////////////////////////
293
294 // Callback to be run in child process after fork and before exec.
295 // chdir is called manually instead of using LaunchOptions.current_directory
296 // because it should take place after setuid. (This both makes sure the user
297 // has the proper permissions and also apparently avoids some obscure errors
298 // that can occur when accessing some network filesystems as the wrong user.)
299 class PreExecDelegate : public base::LaunchOptions::PreExecDelegate {
300 public:
301 void RunAsyncSafe() override {
302 // Use RAW_CHECK to avoid allocating post-fork.
303 RAW_CHECK(setuid(pwinfo_->pw_uid) == 0);
304 RAW_CHECK(chdir(pwinfo_->pw_dir) == 0);
305 }
306
307 PreExecDelegate(struct passwd* pwinfo) : pwinfo_(pwinfo) {}
308
309 private:
310 struct passwd* pwinfo_;
311 };
312
313 // Fetch pwinfo again, as it may have been invalidated or the user name might
314 // have been remapped.
315 pwinfo = getpwnam(user.c_str());
316 PCHECK(pwinfo != nullptr) << "getpwnam failed";
317
318 if (chown_log) {
319 int result = fchown(STDOUT_FILENO, pwinfo->pw_uid, pwinfo->pw_gid);
320 PLOG_IF(WARNING, result != 0) << "Failed to change log file owner";
321 }
322
323 PreExecDelegate pre_exec_delegate(pwinfo);
324
325 base::LaunchOptions launch_options;
326
327 // Required to allow suid binaries to function in the session.
328 launch_options.allow_new_privs = true;
329
330 launch_options.kill_on_parent_death = true;
331
332 launch_options.clear_environ = true;
333 launch_options.environ = std::move(*pam_environment);
334 launch_options.environ["USER"] = pwinfo->pw_name;
335 launch_options.environ["LOGNAME"] = pwinfo->pw_name;
336 launch_options.environ["HOME"] = pwinfo->pw_dir;
337 launch_options.environ["SHELL"] = pwinfo->pw_shell;
338 if (!launch_options.environ.count("PATH")) {
339 launch_options.environ["PATH"] = "/bin:/usr/bin";
340 }
341
342 launch_options.pre_exec_delegate = &pre_exec_delegate;
343
344 // By convention, a login shell is signified by preceeding the shell name in
345 // argv[0] with a '-'.
346 base::CommandLine command_line(base::FilePath(
347 '-' + base::FilePath(pwinfo->pw_shell).BaseName().value()));
348
349 base::Optional<std::string> escaped_script_path =
350 ShellEscapeArgument(script_path);
351
352 CHECK(escaped_script_path) << "Could not escape script path";
353
354 command_line.AppendSwitch("-c");
355 command_line.AppendArg(*escaped_script_path +
356 " --start --foreground --keep-parent-env");
357
358 // Tell LaunchProcess where to find the executable, since argv[0] doesn't
359 // point to it.
360 launch_options.real_path = base::FilePath(pwinfo->pw_shell);
361
362 //////////////////////////////////////////////////////////////////////////////
363 // We're ready to execute the remoting session
364 //////////////////////////////////////////////////////////////////////////////
365
366 base::Process child = base::LaunchProcess(command_line, launch_options);
367
368 if (child.IsValid()) {
369 int exit_code = 0;
370 // Die if wait fails so we don't close the PAM session while the child is
371 // still running.
372 CHECK(child.WaitForExit(&exit_code)) << "Failed to wait for child process";
373 LOG_IF(WARNING, exit_code != 0) << "Child did not exit normally";
374 }
375
376 // Best effort PAM cleanup
377 if (pam_handle.CloseSession(0) != PAM_SUCCESS) {
378 LOG(WARNING) << "Failed to close PAM session";
379 }
380 ignore_result(pam_handle.SetCredentials(PAM_DELETE_CRED));
381 }
382
383 // Opens a temp file for logging. Exits the program on failure.
384 int OpenLogFile() {
385 char logfile[265];
386 std::time_t time = std::time(nullptr);
387 CHECK_NE(time, (std::time_t)(-1));
388 // Safe because we're single threaded
389 std::tm* localtime = std::localtime(&time);
390 CHECK_NE(std::strftime(logfile, sizeof(logfile), kLogFileTemplate, localtime),
391 (std::size_t) 0);
392
393 mode_t mode = umask(0177);
394 int fd = mkstemp(logfile);
395 PCHECK(fd != -1);
396 umask(mode);
397
398 return fd;
399 }
400
401 // Daemonizes the process. Output is redirected to a file. Exits the program on
402 // failure.
403 //
404 // This logic is mostly the same as daemonize() in linux_me2me_host.py. Log-
405 // file redirection especially should be kept in sync. Note that this does
406 // not currently wait for the host to start successfully before exiting the
407 // parent process like the Python script does, as that functionality is
408 // probably not useful at boot, where the wrapper is expected to be used. If
409 // it turns out to be desired, it can be implemented by setting up a pipe and
410 // passing a file descriptor to the Python script.
411 void Daemonize() {
412
413 int log_fd = OpenLogFile();
414 int devnull_fd = open("/dev/null", O_RDONLY);
415 PCHECK(devnull_fd != -1);
416
417 PCHECK(dup2(devnull_fd, STDIN_FILENO) != -1);
418 PCHECK(dup2(log_fd, STDOUT_FILENO) != -1);
419 PCHECK(dup2(log_fd, STDERR_FILENO) != -1);
420
421 // Close all file descriptors except stdio, including any we may have
422 // inherited.
423 base::CloseSuperfluousFds(base::InjectiveMultimap());
424
425 // Allow parent to exit, and ensure we're not a session leader so setsid can
426 // succeed
427 pid_t pid = fork();
428 PCHECK(pid != -1);
429
430 if (pid != 0) {
431 std::exit(EXIT_SUCCESS);
432 }
433
434 // Start a new process group and session with no controlling terminal.
435 PCHECK(setsid() != -1);
436
437 // Fork again so we're no longer a session leader and can't get a controlling
438 // terminal.
439 pid = fork();
440 PCHECK(pid != -1);
441
442 if (pid != 0) {
443 std::exit(EXIT_SUCCESS);
444 }
445
446 // We don't want to change to the target user's home directory until we've
447 // dropped privileges, so change to / to make sure we're not keeping any other
448 // directory in use.
449 PCHECK(chdir("/") == 0);
450
451 // Done!
452 }
453
454 } // namespace
455
456 int main(int argc, char** argv) {
457 base::CommandLine::Init(argc, argv);
458
459 const base::CommandLine* command_line =
460 base::CommandLine::ForCurrentProcess();
461 if (command_line->HasSwitch(kHelpSwitchName) ||
462 command_line->HasSwitch(kQuestionSwitchName)) {
463 PrintUsage(command_line->GetProgram());
464 std::exit(EXIT_SUCCESS);
465 }
466
467 base::FilePath script_path =
468 command_line->GetSwitchValuePath(kScriptSwitchName);
469 std::string user = command_line->GetSwitchValueNative(kUserSwitchName);
470 bool foreground = command_line->HasSwitch(kForegroundSwitchName);
471
472 if (script_path.empty()) {
473 std::fputs("The path to the me2me python script is required.\n", stderr);
474 std::exit(EXIT_FAILURE);
475 }
476
477 if (user.empty()) {
478 std::fputs("The target user must be specified.\n", stderr);
479 std::exit(EXIT_FAILURE);
480 }
481
482 if (!foreground) {
483 Daemonize();
484 }
485
486 ExecuteSession(std::move(user), script_path.value(), !foreground);
487 }
OLDNEW
« no previous file with comments | « remoting/host/linux/linux_me2me_host.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698