]> git.proxmox.com Git - mirror_lxcfs.git/blame - pam/pam_cgfs.c
release 2.0.0.rc5
[mirror_lxcfs.git] / pam / pam_cgfs.c
CommitLineData
df54106a
SH
1/* pam-cgfs
2 *
3 * Copyright © 2016 Canonical, Inc
4 * Author: Serge Hallyn <serge.hallyn@ubuntu.com>
5 *
edd25678
SH
6 * When a user logs in, this pam module will create cgroups which the user
7 * may administer, either for all controllers or for any controllers listed
8 * on the command line (if any are listed).
df54106a
SH
9 *
10 * The cgroup created will be "user/$user/0" for the first session,
11 * "user/$user/1" for the second, etc.
12 *
78a2a9f3
SH
13 * name=systemd is handled specially. If the host is an upstart system
14 * or the login is noninteractive, then the logged in user does not get
15 * a cgroup created. On a systemd interactive login, one is created but
16 * not chowned to the user. In the former case, we create one as usual,
17 * in the latter case we simply chown whatever cgroup the user is in.
edd25678 18 *
df54106a
SH
19 * All requested cgroups must be mounted under /sys/fs/cgroup/$controller,
20 * no messing around with finding mountpoints.
21 *
22 * See COPYING file for details.
23 */
24
25#include <stdio.h>
26#include <stdlib.h>
27#include <unistd.h>
28#include <syslog.h>
29#include <stdarg.h>
30#include <errno.h>
31#include <sys/mount.h>
32#include <sys/types.h>
33#include <sys/stat.h>
34#include <sys/param.h>
35#include <pwd.h>
36#include <stdbool.h>
37#include <dirent.h>
38
39#define PAM_SM_SESSION
40#include <security/_pam_macros.h>
41#include <security/pam_modules.h>
42
43#include <linux/unistd.h>
44
45static bool initialized;
46
47static void mysyslog(int err, const char *format, ...)
48{
49 va_list args;
50
51 va_start(args, format);
52 openlog("PAM-CGFS", LOG_CONS|LOG_PID, LOG_AUTH);
53 vsyslog(err, format, args);
54 va_end(args);
55 closelog();
56}
57
58static char *must_strcat(const char *first, ...) __attribute__((sentinel));
59
60static char *must_strcat(const char *first, ...)
61{
62 va_list args;
63 char *dest, *cur, *new;
64 size_t len;
65
66 do {
67 dest = strdup(first);
68 } while (!dest);
69 len = strlen(dest);
70
71 va_start(args, first);
72
73 while ((cur = va_arg(args, char *)) != NULL) {
74 size_t newlen = len + strlen(cur);
75 do {
76 new = realloc(dest, newlen + 1);
77 } while (!new);
78 dest = new;
79 strcat(dest, cur);
80 len = newlen;
81 }
82 va_end(args);
83
84 return dest;
85}
86
87static bool exists(const char *path)
88{
89 struct stat sb;
90 int ret;
91
92 ret = stat(path, &sb);
93 return ret == 0;
94}
95
96static bool is_dir(const char *path)
97{
98 struct stat sb;
99
100 if (stat(path, &sb) < 0)
101 return false;
102 if (S_ISDIR(sb.st_mode))
103 return true;
104 return false;
105}
106
107static bool mkdir_p(const char *root, char *path)
108{
109 char *b, orig, *e;
110
111 if (strlen(path) < strlen(root))
112 return false;
113 if (strlen(path) == strlen(root))
114 return true;
115
116 b = path + strlen(root) + 1;
117 while (1) {
118 while (*b && *b == '/')
119 b++;
120 if (!*b)
121 return true;
122 e = b + 1;
123 while (*e && *e != '/')
124 e++;
125 orig = *e;
126 if (orig)
127 *e = '\0';
128 if (exists(path))
129 goto next;
130 if (mkdir(path, 0755) < 0) {
131#if DEBUG
132 fprintf(stderr, "Failed to create %s: %m\n", path);
133#endif
134 return false;
135 }
136next:
137 if (!orig)
138 return true;
139 *e = orig;
140 b = e + 1;
141 }
78a2a9f3 142
df54106a
SH
143}
144
145struct controller {
146 struct controller *next;
147 int id;
edd25678 148 bool systemd_created;
df54106a
SH
149 char *name;
150 char *mount_path;
151 char *init_path;
edd25678 152 char *cur_path;
df54106a
SH
153};
154
155#define MAXCONTROLLERS 20
156static struct controller *controllers[MAXCONTROLLERS];
157
2be80971
SH
158/*
159 * if cpu and cpuacct are comounted, it's possible a mount
160 * exists for only one. Find it.
161 */
162static char *find_controller_path(struct controller *c)
163{
164 while (c) {
165 char *path = must_strcat("/sys/fs/cgroup/", c->name, NULL);
166 if (exists(path))
167 return path;
168 free(path);
edd25678
SH
169 if (strncmp(c->name, "name=", 5) == 0) {
170 path = must_strcat("/sys/fs/cgroup/", c->name + 5, NULL);
171 if (exists(path))
172 return path;
173 free(path);
174 }
2be80971
SH
175 c = c->next;
176 }
177 return NULL;
178}
179
df54106a
SH
180/* Find the path at which each controller is mounted. */
181static void get_mounted_paths(void)
182{
183 int i;
184 struct controller *c;
185 char *path;
186
187 for (i = 0; i < MAXCONTROLLERS; i++) {
188 c = controllers[i];
189 if (!c || c->mount_path)
190 continue;
2be80971
SH
191 path = find_controller_path(c);
192 if (!path)
df54106a 193 continue;
df54106a
SH
194 while (c) {
195 c->mount_path = path;
196 c = c->next;
197 }
198 }
199}
200
edd25678 201static void add_controller(int id, char *tok, char *cur_path)
df54106a
SH
202{
203 struct controller *c;
78a2a9f3 204
215cfad6
SH
205 do {
206 c = malloc(sizeof(struct controller));
207 } while (!c);
208 do {
209 c->name = strdup(tok);
210 } while (!c->name);
edd25678
SH
211 do {
212 c->cur_path = strdup(cur_path);
213 } while (!c->cur_path);
df54106a 214 c->id = id;
df54106a
SH
215 c->next = controllers[id];
216 c->mount_path = NULL;
217 c->init_path = NULL;
218 controllers[id] = c;
df54106a
SH
219}
220
221static void drop_controller(int which)
222{
223 struct controller *c = controllers[which];
224
225 if (c) {
226 free(c->init_path); // all comounts share this
227 free(c->mount_path);
228 }
229 while (c) {
230 struct controller *tmp = c->next;
231 free(c->name);
edd25678 232 free(c->cur_path);
df54106a
SH
233 free(c);
234 c = tmp;
235 }
236 controllers[which] = NULL;
237}
238
239static bool single_in_filter(char *c, const char *filter)
240{
241 char *dup = strdupa(filter), *tok;
242 for (tok = strtok(dup, ","); tok; tok = strtok(NULL, ",")) {
243 if (strcmp(c, tok) == 0)
244 return true;
245 }
246 return false;
247}
248
249static bool controller_in_filter(struct controller *controller, const char *filter)
250{
251 struct controller *c;
252
253 for (c = controller; c; c = c->next) {
254 if (single_in_filter(c->name, filter))
255 return true;
256 }
257 return false;
258}
259
260/*
261 * Passed a comma-delimited list of requested controllers.
262 * Pulls any controllers not in the list out of the
263 * list of controllers
264 */
265static void filter_controllers(const char *filter)
266{
267 int i;
268 for (i = 0; i < MAXCONTROLLERS; i++) {
269 if (!controllers[i])
270 continue;
271 if (filter && !controller_in_filter(controllers[i], filter))
272 drop_controller(i);
273 }
274}
275
276#define INIT_SCOPE "/init.scope"
277static void prune_init_scope(char *cg)
278{
279 char *point;
826297d7 280 size_t cg_len, initscope_len;
df54106a
SH
281
282 if (!cg)
283 return;
284
826297d7
SF
285 cg_len = strlen(cg);
286 initscope_len = strlen(INIT_SCOPE);
287 if (cg_len < initscope_len)
df54106a 288 return;
826297d7
SF
289
290 point = cg + cg_len - initscope_len;
df54106a
SH
291 if (strcmp(point, INIT_SCOPE) == 0) {
292 if (point == cg)
293 *(point+1) = '\0';
294 else
295 *point = '\0';
296 }
297}
298
299static bool fill_in_init_paths(void)
300{
301 FILE *f;
302 char *line = NULL;
303 size_t len = 0;
304 struct controller *c;
c65c5956 305 bool ret = false;
df54106a
SH
306
307 f = fopen("/proc/1/cgroup", "r");
308 if (!f)
309 return false;
310 while (getline(&line, &len, f) != -1) {
311 int id;
312 char *subsystems, *ip;
313 if (sscanf(line, "%d:%m[^:]:%ms", &id, &subsystems, &ip) != 3) {
314 mysyslog(LOG_ERR, "Corrupt /proc/1/cgroup\n");
c65c5956 315 goto out;
df54106a
SH
316 }
317 free(subsystems);
318 if (id < 0 || id > 20) {
319 mysyslog(LOG_ERR, "Too many subsystems\n");
320 free(ip);
c65c5956 321 goto out;
df54106a
SH
322 }
323 if (ip[0] != '/') {
324 free(ip);
325 mysyslog(LOG_ERR, "ERROR: init cgroup path is not absolute!\n");
c65c5956 326 goto out;
df54106a
SH
327 }
328 prune_init_scope(ip);
edd25678
SH
329 for (c = controllers[id]; c; c = c->next) {
330 if (strcmp(c->name, "name=systemd") == 0)
331 c->systemd_created = strcmp(ip, c->cur_path) != 0;
df54106a 332 c->init_path = ip;
edd25678 333 }
df54106a 334 }
c65c5956
SH
335 ret = true;
336out:
df54106a 337 fclose(f);
c65c5956
SH
338 free(line);
339 return ret;
df54106a
SH
340}
341
342#if DEBUG
343static void print_found_controllers(void) {
344 struct controller *c;
345 int i;
346
347 for (i = 0; i < MAXCONTROLLERS; i++) {
348 c = controllers[i];
349 if (!c) {
350 fprintf(stderr, "Nothing in controller %d\n", i);
351 continue;
352 }
353 fprintf(stderr, "Controller %d:\n", i);
354 while (c) {
355 fprintf(stderr, " Next mount: index %d name %s\n", c->id, c->name);
356 fprintf(stderr, " mount path %s\n", c->mount_path ? c->mount_path : "(none)");
357 fprintf(stderr, " init task path %s\n", c->init_path);
edd25678 358 fprintf(stderr, " login task path %s\n", c->cur_path);
df54106a
SH
359 c = c->next;
360 }
361 }
362}
363#else
364static inline void print_found_controllers(void) { };
365#endif
366/*
367 * Get the list of cgroup controllers currently mounted.
368 * This includes both kernel and named subsystems, so get the list from
369 * /proc/self/cgroup rather than /proc/cgroups.
370 */
371static bool get_active_controllers(void)
372{
373 FILE *f;
edd25678 374 char *line = NULL, *tok, *cur_path;
df54106a
SH
375 size_t len = 0;
376
377 f = fopen("/proc/self/cgroup", "r");
378 if (!f)
379 return false;
380 while (getline(&line, &len, f) != -1) {
381 int id;
382 char *subsystems;
edd25678 383 if (sscanf(line, "%d:%m[^:]:%ms", &id, &subsystems, &cur_path) != 3) {
df54106a
SH
384 mysyslog(LOG_ERR, "Corrupt /proc/self/cgroup\n");
385 fclose(f);
c65c5956 386 free(line);
df54106a
SH
387 return false;
388 }
389 if (id < 0 || id > 20) {
390 mysyslog(LOG_ERR, "Too many subsystems\n");
391 free(subsystems);
edd25678 392 free(cur_path);
df54106a 393 fclose(f);
c65c5956 394 free(line);
df54106a
SH
395 return false;
396 }
df54106a 397 for (tok = strtok(subsystems, ","); tok; tok = strtok(NULL, ","))
edd25678 398 add_controller(id, tok, cur_path);
df54106a 399 free(subsystems);
edd25678 400 free(cur_path);
df54106a
SH
401 }
402 fclose(f);
c65c5956 403 free(line);
df54106a
SH
404
405 get_mounted_paths();
406
407 if (!fill_in_init_paths()) {
408 mysyslog(LOG_ERR, "Failed finding cgroups for init task\n");
409 return false;
410 }
411
412 print_found_controllers();
413
414 initialized = true;
415
416 return true;
417}
418
78a2a9f3
SH
419/*
420 * the systemd-created path is: user-$uid.slice/session-c$session.scope
421 * If that is not the end of our systemd path, then we're not part of
422 * the PAM call that created that path.
423 *
424 * The last piece is chowned to $uid, the user- part not.
425 * Note - if the user creates paths that look like what we're looking for
426 * to 'fool' us, either
427 * . they fool us, we create new cgroups, and they get auto-logged-out.
428 * . they fool a root sudo, systemd cgroup is not changed but chowned,
429 * and they lose ownership of their cgroups
430 */
431static bool systemd_created_slice_for_us(struct controller *c, const char *in, uid_t uid)
432{
433 char *p, *copy = strdupa(in);
434 size_t len;
435 int id;
436
437 if (!copy || strlen(copy) < strlen("/user-0.slice/session-0.scope"))
438 return false;
439 p = copy + strlen(copy) - 1;
440 /* skip any trailing '/' (shouldn't be any, but be sure) */
441 while (p >= copy && *p == '/')
442 *(p--) = '\0';
443 if (p < copy)
444 return false;
445
446 /* Get last path element */
447 while (p >= copy && *p != '/')
448 p--;
449 if (p < copy)
450 return false;
451 /* make sure it is session-something.scope */
452 len = strlen(p+1);
453 if (strncmp(p+1, "session-", strlen("session-")) != 0 ||
454 strncmp(p+1 + len - 6, ".scope", 6) != 0)
455 return false;
456
457 /* ok last path piece checks out, now check the second to last */
458 *(p+1) = '\0';
459 while (p >= copy && *(--p) != '/');
460 if (sscanf(p+1, "user-%d.slice/", &id) != 1)
461 return false;
462
463 if (id != (int)uid)
464 return false;
465
466 return true;
467}
edd25678
SH
468/*
469 * Handle systemd creation. Return true if all's done. Returns false if
470 * the caller needs to create=chown a cgroup
471 */
78a2a9f3 472static bool handle_systemd_create(struct controller *c, uid_t uid, gid_t gid)
edd25678
SH
473{
474 char *user_path;
475
476 if (!c->systemd_created)
477 return false;
478
479 user_path = must_strcat(c->mount_path, c->cur_path, NULL);
480
78a2a9f3
SH
481 // Is this actually our cgroup, or was it created for someone
482 // else?
483 if (!systemd_created_slice_for_us(c, user_path, uid)) {
484 c->systemd_created = false;
485 free(user_path);
486 return false;
487 }
488
edd25678
SH
489 // a name=systemd cgroup has already been created, just chown it
490 if (chown(user_path, uid, gid) < 0)
491 mysyslog(LOG_WARNING, "Failed to chown %s to %d:%d: %m\n",
492 user_path, (int)uid, (int)gid);
493 free(user_path);
494 return true;
495}
496
78a2a9f3 497static bool cgfs_create_forone(struct controller *c, uid_t uid, gid_t gid, const char *cg, bool *existed)
df54106a
SH
498{
499 while (c) {
500 if (!c->mount_path || !c->init_path)
501 goto next;
edd25678
SH
502
503 if (strcmp(c->name, "name=systemd") == 0 && handle_systemd_create(c, uid, gid))
504 return true;
505
df54106a
SH
506 char *path = must_strcat(c->mount_path, c->init_path, cg, NULL);
507#if DEBUG
508 fprintf(stderr, "Creating %s for %s\n", path, c->name);
509#endif
510 if (exists(path)) {
511 free(path);
512 *existed = true;
513#if DEBUG
edd25678 514 fprintf(stderr, "%s existed\n", path);
df54106a
SH
515#endif
516 return true;
517 }
edd25678 518
df54106a
SH
519 bool pass = mkdir_p(c->mount_path, path);
520#if DEBUG
521 fprintf(stderr, "Creating %s %s\n", path, pass ? "succeeded" : "failed");
522#endif
79ab1116
SH
523 if (pass) {
524 if (chown(path, uid, gid) < 0)
525 mysyslog(LOG_WARNING, "Failed to chown %s to %d:%d: %m\n",
526 path, (int)uid, (int)gid);
527 }
df54106a
SH
528 free(path);
529 if (pass)
530 return true;
531next:
532 c = c->next;
533 }
534 return false;
535}
536
537static void recursive_rmdir(const char *path)
538{
539 struct dirent *direntp;
540 DIR *dir;
541
542 dir = opendir(path);
543 if (!dir)
544 return;
545 while ((direntp = readdir(dir))!= NULL) {
546 if (!strcmp(direntp->d_name, ".") ||
547 !strcmp(direntp->d_name, ".."))
548 continue;
549
550 char *dpath = must_strcat(path, "/", direntp->d_name, NULL);
551 if (is_dir(dpath)) {
552 recursive_rmdir(dpath);
553#if DEBUG
554 fprintf(stderr, "attempting to remove %s\n", dpath);
555#endif
556 if (rmdir(dpath) < 0) {
557#if DEBUG
558 fprintf(stderr, "Failed removing %s: %m\n", dpath);
559#endif
560 }
561 }
562 free(dpath);
563 }
564
565 closedir(dir);
566}
567
568/*
569 * Try to remove a cgroup in a controller to cleanup during failure.
570 * All mounts of comounted controllers are the same, so we just look
571 * for the first mount which exists, try to remove the directory, and
572 * return.
573 */
574static void cgfs_remove_forone(int idx, const char *cg)
575{
576 struct controller *c = controllers[idx];
577 char *path;
578
579 while (c) {
580 if (c->mount_path) {
581 path = must_strcat(c->mount_path, cg, NULL);
582 recursive_rmdir(path);
583 free(path);
584 }
585 c = c->next;
586 }
587}
588
79ab1116 589static bool cgfs_create(const char *cg, uid_t uid, gid_t gid, bool *existed)
df54106a
SH
590{
591 *existed = false;
592 int i, j;
593
594#if DEBUG
595 fprintf(stderr, "creating %s\n", cg);
596#endif
597 for (i = 0; i < MAXCONTROLLERS; i++) {
598 struct controller *c = controllers[i];
599
600 if (!c)
601 continue;
602
79ab1116 603 if (!cgfs_create_forone(c, uid, gid, cg, existed)) {
df54106a
SH
604 for (j = 0; j < i; j++)
605 cgfs_remove_forone(j, cg);
606 return false;
607 }
608 }
609
610 return true;
611}
612
df54106a
SH
613static bool write_int(char *path, int v)
614{
615 FILE *f = fopen(path, "w");
e9597a70
SH
616 bool ret = true;
617
df54106a
SH
618 if (!f)
619 return false;
e9597a70
SH
620 if (fprintf(f, "%d\n", v) < 0)
621 ret = false;
622 if (fclose(f) != 0)
623 ret = false;
624 return ret;
df54106a
SH
625}
626
627static bool do_enter(struct controller *c, const char *cg)
628{
629 char *path;
630 bool pass;
631
632 while (c) {
633 if (!c->mount_path || !c->init_path)
1e5fe374 634 goto next;
df54106a
SH
635 path = must_strcat(c->mount_path, c->init_path, cg, "/cgroup.procs", NULL);
636 if (!exists(path)) {
637 free(path);
638 path = must_strcat(c->mount_path, c->init_path, cg, "/tasks", NULL);
639 }
640#if DEBUG
641 fprintf(stderr, "Attempting to enter %s:%s using %s\n", c->name, cg, path);
642#endif
643 pass = write_int(path, (int)getpid());
644 free(path);
645 if (pass) /* only have to enter one of the comounts */
646 return true;
647#if DEBUG
648 if (!pass)
649 fprintf(stderr, "Failed to enter %s:%s\n", c->name, cg);
650#endif
1e5fe374 651next:
df54106a
SH
652 c = c->next;
653 }
654
655 return false;
656}
657
edd25678 658static bool cgfs_enter(const char *cg, bool skip_systemd)
df54106a
SH
659{
660 int i;
661
662 for (i = 0; i < MAXCONTROLLERS; i++) {
663 struct controller *c = controllers[i];
664
665 if (!c)
666 continue;
667
edd25678
SH
668 if (strcmp(c->name, "name=systemd") == 0) {
669 if (skip_systemd)
670 continue;
671 if (c->systemd_created)
672 continue;
673 }
674
df54106a
SH
675 if (!do_enter(c, cg))
676 return false;
677 }
678
679 return true;
680}
681
682static void cgfs_escape(void)
683{
edd25678 684 if (!cgfs_enter("/", true)) {
df54106a
SH
685 mysyslog(LOG_WARNING, "Failed to escape to init's cgroup\n");
686 }
687}
688
689static bool get_uid_gid(const char *user, uid_t *uid, gid_t *gid)
690{
691 struct passwd *pwent;
692
693 pwent = getpwnam(user);
694 if (!pwent)
695 return false;
696 *uid = pwent->pw_uid;
697 *gid = pwent->pw_gid;
698
699 return true;
700}
701
702#define DIRNAMSZ 200
703static int handle_login(const char *user)
704{
705 int idx = 0, ret;
706 bool existed;
707 uid_t uid = 0;
708 gid_t gid = 0;
709 char cg[MAXPATHLEN];
78a2a9f3 710
df54106a
SH
711 if (!get_uid_gid(user, &uid, &gid)) {
712 mysyslog(LOG_ERR, "Failed to get uid and gid for %s\n", user);
713 return PAM_SESSION_ERR;
714 }
715
716 cgfs_escape();
717
718 while (idx >= 0) {
719 ret = snprintf(cg, MAXPATHLEN, "/user/%s/%d", user, idx);
720 if (ret < 0 || ret >= MAXPATHLEN) {
721 mysyslog(LOG_ERR, "username too long\n");
722 return PAM_SESSION_ERR;
723 }
724
79ab1116 725 if (!cgfs_create(cg, uid, gid, &existed)) {
df54106a
SH
726 mysyslog(LOG_ERR, "Failed to create a cgroup for user %s\n", user);
727 return PAM_SESSION_ERR;
728 }
729
730 if (existed == 1) {
731 idx++;
732 continue;
733 }
734
edd25678 735 if (!cgfs_enter(cg, false)) {
df54106a
SH
736 mysyslog(LOG_ERR, "Failed to enter user cgroup %s for user %s\n", cg, user);
737 return PAM_SESSION_ERR;
738 }
739 break;
740 }
741
742 return PAM_SUCCESS;
743}
744
745int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc,
746 const char **argv)
747{
748 const char *PAM_user = NULL;
749 int ret;
750
751 if (!get_active_controllers()) {
752 mysyslog(LOG_ERR, "Failed to get list of controllers\n");
753 return PAM_SESSION_ERR;
754 }
755
756 if (argc > 1 && strcmp(argv[0], "-c") == 0)
757 filter_controllers(argv[1]);
758
759 ret = pam_get_user(pamh, &PAM_user, NULL);
760 if (ret != PAM_SUCCESS) {
761 mysyslog(LOG_ERR, "PAM-CGFS: couldn't get user\n");
762 return PAM_SESSION_ERR;
763 }
764
765 ret = handle_login(PAM_user);
766 return ret;
767}
768
769static void prune_empty_cgroups(struct controller *c, const char *user)
770{
771 while (c) {
772 if (!c->mount_path || !c->init_path)
773 goto next;
774 char *path = must_strcat(c->mount_path, c->init_path, "user/", user, NULL);
775#if DEBUG
776 fprintf(stderr, "Pruning %s\n", path);
777#endif
778 recursive_rmdir(path);
dec08471 779 free(path);
df54106a
SH
780next:
781 c = c->next;
782 }
783}
784
785/*
786 * Since we can't rely on kernel's autoremove, remove stale cgroups
787 * any time the user logs out.
788 */
789static void prune_user_cgs(const char *user)
790{
791 int i;
792
793 for (i = 0; i < MAXCONTROLLERS; i++)
794 prune_empty_cgroups(controllers[i], user);
795}
796
797int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc,
798 const char **argv)
799{
800 const char *PAM_user = NULL;
801 int ret = pam_get_user(pamh, &PAM_user, NULL);
802
803 if (ret != PAM_SUCCESS) {
804 mysyslog(LOG_ERR, "PAM-CGFS: couldn't get user\n");
805 return PAM_SESSION_ERR;
806 }
807
808 if (!initialized) {
809 get_active_controllers();
810 if (argc > 1 && strcmp(argv[0], "-c") == 0)
811 filter_controllers(argv[1]);
812 }
813
814 prune_user_cgs(PAM_user);
815 return PAM_SUCCESS;
816}