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

Side by Side 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 unified diff | Download patch
« remoting/host/BUILD.gn ('K') | « remoting/host/BUILD.gn ('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 <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 }
OLDNEW
« 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