Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
2646 lines (2163 sloc) 65 KB
/* pam-cgfs
*
* Copyright © 2016 Canonical, Inc
* Author: Serge Hallyn <serge.hallyn@ubuntu.com>
* Author: Christian Brauner <christian.brauner@ubuntu.com>
*
* When a user logs in, this pam module will create cgroups which the user may
* administer. It handles both pure cgroupfs v1 and pure cgroupfs v2, as well as
* mixed mounts, where some controllers are mounted in a standard cgroupfs v1
* hierarchy location (/sys/fs/cgroup/<controller>) and others are in the
* cgroupfs v2 hierarchy.
* Writeable cgroups are either created for all controllers or, if specified,
* for any controllers listed on the command line.
* The cgroup created will be "user/$user/0" for the first session,
* "user/$user/1" for the second, etc.
*
* Systems with a systemd init system are treated specially, both with respect
* to cgroupfs v1 and cgroupfs v2. For both, cgroupfs v1 and cgroupfs v2, We
* check whether systemd already placed us in a cgroup it created:
*
* user.slice/user-uid.slice/session-n.scope
*
* by checking whether uid == our uid. If it did, we simply chown the last
* part (session-n.scope). If it did not we create a cgroup as outlined above
* (user/$user/n) and chown it to our uid.
* The same holds for cgroupfs v2 where this assumptions becomes crucial:
* We __have to__ be placed in our under the cgroup systemd created for us on
* login, otherwise things like starting an xserver or similar will not work.
*
* All requested cgroups must be mounted under /sys/fs/cgroup/$controller,
* no messing around with finding mountpoints.
*
* See COPYING file for details.
*/
#ifndef _GNU_SOURCE
#define _GNU_SOURCE 1
#endif
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/unistd.h>
#include <pwd.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/vfs.h>
#include <syslog.h>
#include <unistd.h>
#include "config.h"
#include "file_utils.h"
#include "macro.h"
#include "memory_utils.h"
#include "string_utils.h"
#define PAM_SM_SESSION
#include <security/_pam_macros.h>
#include <security/pam_modules.h>
#ifndef HAVE_STRLCPY
#include "include/strlcpy.h"
#endif
#ifndef HAVE_STRLCAT
#include "include/strlcat.h"
#endif
#define pam_cgfs_debug_stream(stream, format, ...) \
do { \
fprintf(stream, "%s: %d: %s: " format, __FILE__, __LINE__, \
__func__, __VA_ARGS__); \
} while (false)
#define pam_cgfs_error(format, ...) pam_cgfs_debug_stream(stderr, format, __VA_ARGS__)
#ifdef DEBUG
#define pam_cgfs_debug(format, ...) pam_cgfs_error(format, __VA_ARGS__)
#else
#define pam_cgfs_debug(format, ...)
#endif /* DEBUG */
/* Taken over modified from the kernel sources. */
#define NBITS 32 /* bits in uint32_t */
#define DIV_ROUND_UP(n, d) (((n) + (d)-1) / (d))
#define BITS_TO_LONGS(nr) DIV_ROUND_UP(nr, NBITS)
static enum cg_mount_mode {
CGROUP_UNKNOWN = -1,
CGROUP_MIXED = 0,
CGROUP_PURE_V1 = 1,
CGROUP_PURE_V2 = 2,
CGROUP_UNINITIALIZED = 3,
} cg_mount_mode = CGROUP_UNINITIALIZED;
/* Common helper functions. Most of these have been taken from LXC. */
static void append_line(char **dest, size_t oldlen, char *new, size_t newlen);
static int append_null_to_list(void ***list);
static void batch_realloc(char **mem, size_t oldlen, size_t newlen);
static inline void clear_bit(unsigned bit, uint32_t *bitarr)
{
bitarr[bit / NBITS] &= ~(1 << (bit % NBITS));
}
static char *copy_to_eol(char *s);
static void free_string_list(char **list);
static char *get_mountpoint(char *line);
static bool get_uid_gid(const char *user, uid_t *uid, gid_t *gid);
static int handle_login(const char *user, uid_t uid, gid_t gid);
static inline bool is_set(unsigned bit, uint32_t *bitarr)
{
return (bitarr[bit / NBITS] & (1 << (bit % NBITS))) != 0;
}
static bool is_lxcfs(const char *line);
static bool is_cgv1(char *line);
static bool is_cgv2(char *line);
static void must_add_to_list(char ***clist, char *entry);
static void must_append_controller(char **klist, char **nlist, char ***clist,
char *entry);
static void must_append_string(char ***list, char *entry);
static void mysyslog(int err, const char *format, ...) __attribute__((sentinel));
static char *read_file(char *fnam);
static int recursive_rmdir(char *dirname);
static inline void set_bit(unsigned bit, uint32_t *bitarr)
{
bitarr[bit / NBITS] |= (1 << (bit % NBITS));
}
static bool string_in_list(char **list, const char *entry);
static char *string_join(const char *sep, const char **parts, bool use_as_prefix);
static void trim(char *s);
static bool write_int(char *path, int v);
/* cgroupfs prototypes. */
static bool cg_belongs_to_uid_gid(const char *path, uid_t uid, gid_t gid);
static uint32_t *cg_cpumask(char *buf, size_t nbits);
static bool cg_copy_parent_file(char *path, char *file);
static char *cg_cpumask_to_cpulist(uint32_t *bitarr, size_t nbits);
static bool cg_enter(const char *cgroup);
static void cg_escape(void);
static bool cg_filter_and_set_cpus(char *path, bool am_initialized);
static ssize_t cg_get_max_cpus(char *cpulist);
static int cg_get_version_of_mntpt(const char *path);
static bool cg_init(uid_t uid, gid_t gid);
static void cg_mark_to_make_rw(char **list);
static void cg_prune_empty_cgroups(const char *user);
static bool cg_systemd_created_user_slice(const char *base_cgroup,
const char *init_cgroup,
const char *in, uid_t uid);
static bool cg_systemd_chown_existing_cgroup(const char *mountpoint,
const char *base_cgroup, uid_t uid,
gid_t gid,
bool systemd_user_slice);
static bool cg_systemd_under_user_slice_1(const char *in, uid_t uid);
static bool cg_systemd_under_user_slice_2(const char *base_cgroup,
const char *init_cgroup, uid_t uid);
static void cg_systemd_prune_init_scope(char *cg);
static bool is_lxcfs(const char *line);
/* cgroupfs v1 prototypes. */
struct cgv1_hierarchy {
char **controllers;
char *mountpoint;
char *base_cgroup;
char *fullcgpath;
char *init_cgroup;
bool create_rw_cgroup;
bool systemd_user_slice;
};
static struct cgv1_hierarchy **cgv1_hierarchies;
static void cgv1_add_controller(char **clist, char *mountpoint,
char *base_cgroup, char *init_cgroup);
static bool cgv1_controller_in_clist(char *cgline, char *c);
static bool cgv1_controller_lists_intersect(char **l1, char **l2);
static bool cgv1_controller_list_is_dup(struct cgv1_hierarchy **hlist,
char **clist);
static bool cgv1_create(const char *cgroup, uid_t uid, gid_t gid,
bool *existed);
static bool cgv1_create_one(struct cgv1_hierarchy *h, const char *cgroup,
uid_t uid, gid_t gid, bool *existed);
static bool cgv1_enter(const char *cgroup);
static void cgv1_escape(void);
static bool cgv1_get_controllers(char ***klist, char ***nlist);
static char *cgv1_get_current_cgroup(char *basecginfo, char *controller);
static char **cgv1_get_proc_mountinfo_controllers(char **klist, char **nlist,
char *line);
static bool cgv1_handle_cpuset_hierarchy(struct cgv1_hierarchy *h,
const char *cgroup);
static bool cgv1_handle_root_cpuset_hierarchy(struct cgv1_hierarchy *h);
static bool cgv1_init(uid_t uid, gid_t gid);
static void cgv1_mark_to_make_rw(char **clist);
static char *cgv1_must_prefix_named(char *entry);
static bool cgv1_prune_empty_cgroups(const char *user);
static bool cgv1_remove_one(struct cgv1_hierarchy *h, const char *cgroup);
static bool is_cgv1(char *line);
/* cgroupfs v2 prototypes. */
struct cgv2_hierarchy {
char **controllers;
char *mountpoint;
char *base_cgroup;
char *fullcgpath;
char *init_cgroup;
bool create_rw_cgroup;
bool systemd_user_slice;
};
/* Actually this should only be a single hierarchy. But for the sake of
* parallelism and because the layout of the cgroupfs v2 is still somewhat
* changing, we'll leave it as an array of structs.
*/
static struct cgv2_hierarchy **cgv2_hierarchies;
static void cgv2_add_controller(char **clist, char *mountpoint,
char *base_cgroup, char *init_cgroup,
bool systemd_user_slice);
static bool cgv2_create(const char *cgroup, uid_t uid, gid_t gid,
bool *existed);
static bool cgv2_enter(const char *cgroup);
static void cgv2_escape(void);
static char *cgv2_get_current_cgroup(int pid);
static bool cgv2_init(uid_t uid, gid_t gid);
static void cgv2_mark_to_make_rw(char **clist);
static bool cgv2_prune_empty_cgroups(const char *user);
static bool cgv2_remove(const char *cgroup);
static bool is_cgv2(char *line);
static int do_mkdir(const char *path, mode_t mode)
{
int saved_errno;
mode_t mask;
int r;
mask = umask(0);
r = mkdir(path, mode);
saved_errno = errno;
umask(mask);
errno = saved_errno;
return (r);
}
/* Create directory and (if necessary) its parents. */
static bool mkdir_parent(const char *root, char *path)
{
char *b, orig, *e;
if (strlen(path) < strlen(root))
return false;
if (strlen(path) == strlen(root))
return true;
b = path + strlen(root) + 1;
for (;;) {
while (*b && (*b == '/'))
b++;
if (!*b)
return true;
e = b + 1;
while (*e && *e != '/')
e++;
orig = *e;
if (orig)
*e = '\0';
if (file_exists(path))
goto next;
if (do_mkdir(path, 0755) < 0) {
pam_cgfs_debug("Failed to create %s: %s\n", path, strerror(errno));
return false;
}
next:
if (!orig)
return true;
*e = orig;
b = e + 1;
}
return false;
}
/* Common helper functions. Most of these have been taken from LXC. */
static void mysyslog(int err, const char *format, ...)
{
va_list args;
va_start(args, format);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
openlog("PAM-CGFS", LOG_CONS | LOG_PID, LOG_AUTH);
vsyslog(err, format, args);
#pragma GCC diagnostic pop
va_end(args);
closelog();
}
/* realloc() pointer in batch sizes; do not fail. */
#define BATCH_SIZE 50
static void batch_realloc(char **mem, size_t oldlen, size_t newlen)
{
int newbatches = (newlen / BATCH_SIZE) + 1;
int oldbatches = (oldlen / BATCH_SIZE) + 1;
if (!*mem || newbatches > oldbatches)
*mem = must_realloc(*mem, newbatches * BATCH_SIZE);
}
/* Append lines as is to pointer; do not fail. */
static void append_line(char **dest, size_t oldlen, char *new, size_t newlen)
{
size_t full = oldlen + newlen;
batch_realloc(dest, oldlen, full + 1);
memcpy(*dest + oldlen, new, newlen + 1);
}
/* Read in whole file and return allocated pointer. */
static char *read_file(char *fnam)
{
FILE *f;
int linelen;
char *line = NULL, *buf = NULL;
size_t len = 0, fulllen = 0;
f = fopen(fnam, "r");
if (!f)
return NULL;
while ((linelen = getline(&line, &len, f)) != -1) {
append_line(&buf, fulllen, line, linelen);
fulllen += linelen;
}
fclose(f);
free(line);
return buf;
}
/* Given a pointer to a null-terminated array of pointers, realloc to add one
* entry, and point the new entry to NULL. Do not fail. Return the index to the
* second-to-last entry - that is, the one which is now available for use
* (keeping the list null-terminated).
*/
static int append_null_to_list(void ***list)
{
int newentry = 0;
if (*list)
for (; (*list)[newentry]; newentry++)
;
*list = must_realloc(*list, (newentry + 2) * sizeof(void **));
(*list)[newentry + 1] = NULL;
return newentry;
}
/* Append new entry to null-terminated array of pointer; make sure that array of
* pointers will still be null-terminated.
*/
static void must_append_string(char ***list, char *entry)
{
int newentry;
char *copy;
newentry = append_null_to_list((void ***)list);
copy = must_copy_string(entry);
(*list)[newentry] = copy;
}
/* Remove newlines from string. */
static void trim(char *s)
{
size_t len = strlen(s);
while ((len > 0) && s[len - 1] == '\n')
s[--len] = '\0';
}
/* Make allocated copy of string. End of string is taken to be '\n'. */
static char *copy_to_eol(char *s)
{
char *newline, *sret;
size_t len;
newline = strchr(s, '\n');
if (!newline)
return NULL;
len = newline - s;
sret = must_realloc(NULL, len + 1);
memcpy(sret, s, len);
sret[len] = '\0';
return sret;
}
/* Check if given entry under /proc/<pid>/mountinfo is a fuse.lxcfs mount. */
static bool is_lxcfs(const char *line)
{
char *p = strstr(line, " - ");
if (!p)
return false;
return strncmp(p, " - fuse.lxcfs ", 14) == 0;
}
/* Check if given entry under /proc/<pid>/mountinfo is a cgroupfs v1 mount. */
static bool is_cgv1(char *line)
{
char *p = strstr(line, " - ");
if (!p)
return false;
return strncmp(p, " - cgroup ", 10) == 0;
}
/* Check if given entry under /proc/<pid>/mountinfo is a cgroupfs v2 mount. */
static bool is_cgv2(char *line)
{
char *p = strstr(line, " - ");
if (!p)
return false;
return strncmp(p, " - cgroup2 ", 11) == 0;
}
/* Given a null-terminated array of strings, check whether @entry is one of the
* strings
*/
static bool string_in_list(char **list, const char *entry)
{
char **it;
for (it = list; it && *it; it++)
if (strcmp(*it, entry) == 0)
return true;
return false;
}
/*
* Creates a null-terminated array of strings, made by splitting the entries in
* @str on each @sep. Caller is responsible for calling free_string_list.
*/
static char **make_string_list(const char *str, const char *sep)
{
char *copy, *tok;
char *saveptr = NULL;
char **clist = NULL;
copy = must_copy_string(str);
for (tok = strtok_r(copy, sep, &saveptr); tok;
tok = strtok_r(NULL, sep, &saveptr))
must_add_to_list(&clist, tok);
free(copy);
return clist;
}
/* Gets the length of a null-terminated array of strings. */
static size_t string_list_length(char **list)
{
size_t len = 0;
char **it;
for (it = list; it && *it; it++)
len++;
return len;
}
/* Free null-terminated array of strings. */
static void free_string_list(char **list)
{
char **it;
for (it = list; it && *it; it++)
free(*it);
free(list);
}
/* Write single integer to file. */
static bool write_int(char *path, int v)
{
FILE *f;
bool ret = true;
f = fopen(path, "w");
if (!f)
return false;
if (fprintf(f, "%d\n", v) < 0)
ret = false;
if (fclose(f) != 0)
ret = false;
return ret;
}
/* Recursively remove directory and its parents. */
static int recursive_rmdir(char *dirname)
{
struct dirent *direntp;
DIR *dir;
int r = 0;
dir = opendir(dirname);
if (!dir)
return -ENOENT;
while ((direntp = readdir(dir))) {
struct stat st;
char *pathname;
if (!strcmp(direntp->d_name, ".") ||
!strcmp(direntp->d_name, ".."))
continue;
pathname = must_make_path(dirname, direntp->d_name, NULL);
if (lstat(pathname, &st)) {
if (!r)
pam_cgfs_debug("Failed to stat %s\n", pathname);
r = -1;
goto next;
}
if (!S_ISDIR(st.st_mode))
goto next;
if (recursive_rmdir(pathname) < 0)
r = -1;
next:
free(pathname);
}
if (rmdir(dirname) < 0) {
if (!r)
pam_cgfs_debug("Failed to delete %s: %s\n", dirname, strerror(errno));
r = -1;
}
if (closedir(dir) < 0) {
if (!r)
pam_cgfs_debug("Failed to delete %s: %s\n", dirname, strerror(errno));
r = -1;
}
return r;
}
/* Add new entry to null-terminated array of pointers. Make sure array is still
* null-terminated.
*/
static void must_add_to_list(char ***clist, char *entry)
{
int newentry;
newentry = append_null_to_list((void ***)clist);
(*clist)[newentry] = must_copy_string(entry);
}
/* Get mountpoint from a /proc/<pid>/mountinfo line. */
static char *get_mountpoint(char *line)
{
int i;
char *p, *sret, *p2;
size_t len;
p = line;
for (i = 0; i < 4; i++) {
p = strchr(p, ' ');
if (!p)
return NULL;
p++;
}
p2 = strchr(p, ' ');
if (p2)
*p2 = '\0';
len = strlen(p);
sret = must_realloc(NULL, len + 1);
memcpy(sret, p, len);
sret[len] = '\0';
return sret;
}
/* Create list of cgroupfs v1 controller found under /proc/self/cgroup. Skips
* the 0::/some/path cgroupfs v2 hierarchy listed. Splits controllers into
* kernel controllers (@klist) and named controllers (@nlist).
*/
static bool cgv1_get_controllers(char ***klist, char ***nlist)
{
FILE *f;
char *line = NULL;
size_t len = 0;
f = fopen("/proc/self/cgroup", "r");
if (!f)
return false;
while (getline(&line, &len, f) != -1) {
char *p, *p2, *tok;
char *saveptr = NULL;
p = strchr(line, ':');
if (!p)
continue;
p++;
p2 = strchr(p, ':');
if (!p2)
continue;
*p2 = '\0';
/* Skip the v2 hierarchy. */
if ((p2 - p) == 0)
continue;
for (tok = strtok_r(p, ",", &saveptr); tok;
tok = strtok_r(NULL, ",", &saveptr)) {
if (strncmp(tok, "name=", 5) == 0)
must_append_string(nlist, tok);
else
must_append_string(klist, tok);
}
}
free(line);
fclose(f);
return true;
}
/* Get list of controllers for cgroupfs v2 hierarchy by looking at
* cgroup.controllers and/or cgroup.subtree_control of a given (parent) cgroup.
static bool cgv2_get_controllers(char ***klist)
{
return -ENOSYS;
}
*/
/* Get current cgroup from /proc/self/cgroup for the cgroupfs v2 hierarchy. */
static char *cgv2_get_current_cgroup(int pid)
{
int ret;
char *cgroups_v2;
char *current_cgroup;
char *copy = NULL;
/* The largest integer that can fit into long int is 2^64. This is a
* 20-digit number. */
#define __PIDLEN /* /proc */ 5 + /* /pid-to-str */ 21 + /* /cgroup */ 7 + /* \0 */ 1
char path[__PIDLEN];
ret = snprintf(path, __PIDLEN, "/proc/%d/cgroup", pid);
if (ret < 0 || ret >= __PIDLEN)
return NULL;
cgroups_v2 = read_file(path);
if (!cgroups_v2)
return NULL;
current_cgroup = strstr(cgroups_v2, "0::/");
if (!current_cgroup)
goto cleanup_on_err;
current_cgroup = current_cgroup + 3;
copy = copy_to_eol(current_cgroup);
if (!copy)
goto cleanup_on_err;
cleanup_on_err:
free(cgroups_v2);
if (copy)
trim(copy);
return copy;
}
/* Given two null-terminated lists of strings, return true if any string is in
* both.
*/
static bool cgv1_controller_lists_intersect(char **l1, char **l2)
{
char **it;
if (!l2)
return false;
for (it = l1; it && *it; it++)
if (string_in_list(l2, *it))
return true;
return false;
}
/* For a null-terminated list of controllers @clist, return true if any of those
* controllers is already listed the null-terminated list of hierarchies @hlist.
* Realistically, if one is present, all must be present.
*/
static bool cgv1_controller_list_is_dup(struct cgv1_hierarchy **hlist, char **clist)
{
struct cgv1_hierarchy **it;
for (it = hlist; it && *it; it++)
if ((*it)->controllers)
if (cgv1_controller_lists_intersect((*it)->controllers, clist))
return true;
return false;
}
/* Set boolean to mark controllers under which we are supposed create a
* writeable cgroup.
*/
static void cgv1_mark_to_make_rw(char **clist)
{
struct cgv1_hierarchy **it;
for (it = cgv1_hierarchies; it && *it; it++)
if ((*it)->controllers)
if (cgv1_controller_lists_intersect((*it)->controllers, clist) ||
string_in_list(clist, "all"))
(*it)->create_rw_cgroup = true;
}
/* Set boolean to mark whether we are supposed to create a writeable cgroup in
* the cgroupfs v2 hierarchy.
*/
static void cgv2_mark_to_make_rw(char **clist)
{
if (string_in_list(clist, "unified") || string_in_list(clist, "all"))
if (cgv2_hierarchies)
(*cgv2_hierarchies)->create_rw_cgroup = true;
}
/* Wrapper around cgv{1,2}_mark_to_make_rw(). */
static void cg_mark_to_make_rw(char **clist)
{
cgv1_mark_to_make_rw(clist);
cgv2_mark_to_make_rw(clist);
}
/* Prefix any named controllers with "name=", e.g. "name=systemd". */
static char *cgv1_must_prefix_named(char *entry)
{
char *s;
int ret;
size_t len;
len = strlen(entry);
s = must_realloc(NULL, len + 6);
ret = snprintf(s, len + 6, "name=%s", entry);
if (ret < 0 || (size_t)ret >= (len + 6)) {
free(s);
return NULL;
}
return s;
}
/* Append kernel controller in @klist or named controller in @nlist to @clist */
static void must_append_controller(char **klist, char **nlist, char ***clist, char *entry)
{
int newentry;
char *copy;
if (string_in_list(klist, entry) && string_in_list(nlist, entry))
return;
newentry = append_null_to_list((void ***)clist);
if (strncmp(entry, "name=", 5) == 0)
copy = must_copy_string(entry);
else if (string_in_list(klist, entry))
copy = must_copy_string(entry);
else
copy = cgv1_must_prefix_named(entry);
(*clist)[newentry] = copy;
}
/* Get the controllers from a mountinfo line. There are other ways we could get
* this info. For lxcfs, field 3 is /cgroup/controller-list. For cgroupfs, we
* could parse the mount options. But we simply assume that the mountpoint must
* be /sys/fs/cgroup/controller-list
*/
static char **cgv1_get_proc_mountinfo_controllers(char **klist, char **nlist, char *line)
{
int i;
char *p, *p2, *tok;
char *saveptr = NULL;
char **aret = NULL;
p = line;
for (i = 0; i < 4; i++) {
p = strchr(p, ' ');
if (!p)
return NULL;
p++;
}
if (strncmp(p, "/sys/fs/cgroup/", 15) != 0)
return NULL;
p += 15;
p2 = strchr(p, ' ');
if (!p2)
return NULL;
*p2 = '\0';
for (tok = strtok_r(p, ",", &saveptr); tok;
tok = strtok_r(NULL, ",", &saveptr))
must_append_controller(klist, nlist, &aret, tok);
return aret;
}
/* Check if a cgroupfs v2 controller is present in the string @cgline. */
static bool cgv1_controller_in_clist(char *cgline, char *c)
{
__do_free char *tmp = NULL;
size_t len;
char *tok, *eol;
char *saveptr = NULL;
eol = strchr(cgline, ':');
if (!eol)
return false;
len = eol - cgline;
tmp = must_realloc(NULL, len + 1);
memcpy(tmp, cgline, len);
tmp[len] = '\0';
for (tok = strtok_r(tmp, ",", &saveptr); tok;
tok = strtok_r(NULL, ",", &saveptr)) {
if (strcmp(tok, c) == 0)
return true;
}
return false;
}
/* Get current cgroup from the /proc/<pid>/cgroup file passed in via @basecginfo
* of a given cgv1 controller passed in via @controller.
*/
static char *cgv1_get_current_cgroup(char *basecginfo, char *controller)
{
char *p;
p = basecginfo;
for (;;) {
p = strchr(p, ':');
if (!p)
return NULL;
p++;
if (cgv1_controller_in_clist(p, controller)) {
p = strchr(p, ':');
if (!p)
return NULL;
p++;
return copy_to_eol(p);
}
p = strchr(p, '\n');
if (!p)
return NULL;
p++;
}
return NULL;
}
/* Remove /init.scope from string @cg. This will mostly affect systemd-based
* systems.
*/
#define INIT_SCOPE "/init.scope"
static void cg_systemd_prune_init_scope(char *cg)
{
char *point;
if (!cg)
return;
point = cg + strlen(cg) - strlen(INIT_SCOPE);
if (point < cg)
return;
if (strcmp(point, INIT_SCOPE) == 0) {
if (point == cg)
*(point + 1) = '\0';
else
*point = '\0';
}
}
/* Add new info about a mounted cgroupfs v1 hierarchy. Includes the controllers
* mounted into that hierarchy (e.g. cpu,cpuacct), the mountpoint of that
* hierarchy (/sys/fs/cgroup/<controller>, the base cgroup of the current
* process gathered from /proc/self/cgroup, and the init cgroup of PID1 gathered
* from /proc/1/cgroup.
*/
static void cgv1_add_controller(char **clist, char *mountpoint, char *base_cgroup, char *init_cgroup)
{
struct cgv1_hierarchy *new;
int newentry;
new = must_realloc(NULL, sizeof(*new));
new->controllers = clist;
new->mountpoint = mountpoint;
new->base_cgroup = base_cgroup;
new->fullcgpath = NULL;
new->create_rw_cgroup = false;
new->init_cgroup = init_cgroup;
new->systemd_user_slice = false;
newentry = append_null_to_list((void ***)&cgv1_hierarchies);
cgv1_hierarchies[newentry] = new;
}
/* Add new info about the mounted cgroupfs v2 hierarchy. Can (but doesn't
* currently) include the controllers mounted into the hierarchy (e.g. memory,
* pids, blkio), the mountpoint of that hierarchy (Should usually be
* /sys/fs/cgroup but some init systems seems to think it might be a good idea
* to also mount empty cgroupfs v2 hierarchies at /sys/fs/cgroup/systemd.), the
* base cgroup of the current process gathered from /proc/self/cgroup, and the
* init cgroup of PID1 gathered from /proc/1/cgroup.
*/
static void cgv2_add_controller(char **clist, char *mountpoint, char *base_cgroup, char *init_cgroup, bool systemd_user_slice)
{
struct cgv2_hierarchy *new;
int newentry;
new = must_realloc(NULL, sizeof(*new));
new->controllers = clist;
new->mountpoint = mountpoint;
new->base_cgroup = base_cgroup;
new->fullcgpath = NULL;
new->create_rw_cgroup = false;
new->init_cgroup = init_cgroup;
new->systemd_user_slice = systemd_user_slice;
newentry = append_null_to_list((void ***)&cgv2_hierarchies);
cgv2_hierarchies[newentry] = new;
}
/* In Ubuntu 14.04, the paths created for us were
* '/user/$uid.user/$something.session' This can be merged better with
* systemd_created_slice_for_us(), but keeping it separate makes it easier to
* reason about the correctness.
*/
static bool cg_systemd_under_user_slice_1(const char *in, uid_t uid)
{
char *p;
size_t len;
int id;
char *copy = NULL;
bool bret = false;
copy = must_copy_string(in);
if (strlen(copy) < strlen("/user/1.user/1.session"))
goto cleanup;
p = copy + strlen(copy) - 1;
/* skip any trailing '/' (shouldn't be any, but be sure) */
while (p >= copy && *p == '/')
*(p--) = '\0';
if (p < copy)
goto cleanup;
/* Get last path element */
while (p >= copy && *p != '/')
p--;
if (p < copy)
goto cleanup;
/* make sure it is something.session */
len = strlen(p + 1);
if (len < strlen("1.session") ||
strncmp(p + 1 + len - 8, ".session", 8) != 0)
goto cleanup;
/* ok last path piece checks out, now check the second to last */
*(p + 1) = '\0';
while (p >= copy && *(--p) != '/')
;
if (sscanf(p + 1, "%d.user/", &id) != 1)
goto cleanup;
if (id != (int)uid)
goto cleanup;
bret = true;
cleanup:
free(copy);
return bret;
}
/* So long as our path relative to init starts with /user.slice/user-$uid.slice,
* assume it belongs to $uid and chown it
*/
static bool cg_systemd_under_user_slice_2(const char *base_cgroup,
const char *init_cgroup, uid_t uid)
{
int ret;
char buf[100];
size_t curlen, initlen;
curlen = strlen(base_cgroup);
initlen = strlen(init_cgroup);
if (curlen <= initlen)
return false;
if (strncmp(base_cgroup, init_cgroup, initlen) != 0)
return false;
ret = snprintf(buf, 100, "/user.slice/user-%d.slice/", (int)uid);
if (ret < 0 || ret >= 100)
return false;
if (initlen == 1)
initlen = 0; // skip the '/'
return strncmp(base_cgroup + initlen, buf, strlen(buf)) == 0;
}
/* The systemd-created path is: user-$uid.slice/session-c$session.scope. If that
* is not the end of our systemd path, then we're not part of the PAM call that
* created that path.
*
* The last piece is chowned to $uid, the user- part not.
* Note: If the user creates paths that look like what we're looking for to
* 'fool' us, either
* - they fool us, we create new cgroups, and they get auto-logged-out.
* - they fool a root sudo, systemd cgroup is not changed but chowned, and they
* lose ownership of their cgroups
*/
static bool cg_systemd_created_user_slice(const char *base_cgroup,
const char *init_cgroup,
const char *in, uid_t uid)
{
char *p;
size_t len;
int id;
char *copy = NULL;
bool bret = false;
copy = must_copy_string(in);
/* An old version of systemd has already created a cgroup for us. */
if (cg_systemd_under_user_slice_1(in, uid))
goto succeed;
/* A new version of systemd has already created a cgroup for us. */
if (cg_systemd_under_user_slice_2(base_cgroup, init_cgroup, uid))
goto succeed;
if (strlen(copy) < strlen("/user-0.slice/session-0.scope"))
goto cleanup;
p = copy + strlen(copy) - 1;
/* Skip any trailing '/' (shouldn't be any, but be sure). */
while (p >= copy && *p == '/')
*(p--) = '\0';
if (p < copy)
goto cleanup;
/* Get last path element */
while (p >= copy && *p != '/')
p--;
if (p < copy)
goto cleanup;
/* Make sure it is session-something.scope. */
len = strlen(p + 1);
if (strncmp(p + 1, "session-", strlen("session-")) != 0 ||
strncmp(p + 1 + len - 6, ".scope", 6) != 0)
goto cleanup;
/* Ok last path piece checks out, now check the second to last. */
*(p + 1) = '\0';
while (p >= copy && *(--p) != '/')
;
if (sscanf(p + 1, "user-%d.slice/", &id) != 1)
goto cleanup;
if (id != (int)uid)
goto cleanup;
succeed:
bret = true;
cleanup:
free(copy);
return bret;
}
/* Chown existing cgroup that systemd has already created for us. */
static bool cg_systemd_chown_existing_cgroup(const char *mountpoint,
const char *base_cgroup, uid_t uid,
gid_t gid, bool systemd_user_slice)
{
char *path;
if (!systemd_user_slice)
return false;
path = must_make_path(mountpoint, base_cgroup, NULL);
/* A cgroup within name=systemd has already been created. So we only
* need to chown it.
*/
if (chown(path, uid, gid) < 0)
mysyslog(LOG_WARNING, "Failed to chown %s to %d:%d: %s\n",
path, (int)uid, (int)gid, strerror(errno), NULL);
pam_cgfs_debug("Chowned %s to %d:%d\n", path, (int)uid, (int)gid);
free(path);
return true;
}
/* Detect and store information about cgroupfs v1 hierarchies. */
static bool cgv1_init(uid_t uid, gid_t gid)
{
FILE *f;
struct cgv1_hierarchy **it;
char *basecginfo;
char *line = NULL;
char **klist = NULL, **nlist = NULL;
size_t len = 0;
basecginfo = read_file("/proc/self/cgroup");
if (!basecginfo)
return false;
f = fopen("/proc/self/mountinfo", "r");
if (!f) {
free(basecginfo);
return false;
}
cgv1_get_controllers(&klist, &nlist);
while (getline(&line, &len, f) != -1) {
char **controller_list = NULL;
char *mountpoint, *base_cgroup;
if (is_lxcfs(line) || !is_cgv1(line))
continue;
controller_list = cgv1_get_proc_mountinfo_controllers(klist, nlist, line);
if (!controller_list)
continue;
if (cgv1_controller_list_is_dup(cgv1_hierarchies, controller_list)) {
free(controller_list);
continue;
}
mountpoint = get_mountpoint(line);
if (!mountpoint) {
free_string_list(controller_list);
continue;
}
base_cgroup = cgv1_get_current_cgroup(basecginfo, controller_list[0]);
if (!base_cgroup) {
free_string_list(controller_list);
free(mountpoint);
continue;
}
trim(base_cgroup);
pam_cgfs_debug("Detected cgroupfs v1 controller \"%s\" with "
"mountpoint \"%s\" and cgroup \"%s\"\n",
controller_list[0], mountpoint, base_cgroup);
cgv1_add_controller(controller_list, mountpoint, base_cgroup, NULL);
}
free_string_list(klist);
free_string_list(nlist);
free(basecginfo);
fclose(f);
free(line);
/* Retrieve init cgroup path for all controllers. */
basecginfo = read_file("/proc/1/cgroup");
if (!basecginfo)
return false;
for (it = cgv1_hierarchies; it && *it; it++) {
if ((*it)->controllers) {
char *init_cgroup, *user_slice;
/* We've already stored the controller and received its
* current cgroup. If we now fail to retrieve its init
* cgroup, we should probably fail.
*/
init_cgroup = cgv1_get_current_cgroup(basecginfo, (*it)->controllers[0]);
if (!init_cgroup) {
free(basecginfo);
return false;
}
cg_systemd_prune_init_scope(init_cgroup);
(*it)->init_cgroup = init_cgroup;
pam_cgfs_debug("cgroupfs v1 controller \"%s\" has init "
"cgroup \"%s\"\n",
(*(*it)->controllers), init_cgroup);
/* Check whether systemd has already created a cgroup
* for us.
*/
user_slice = must_make_path((*it)->mountpoint, (*it)->base_cgroup, NULL);
if (cg_systemd_created_user_slice((*it)->base_cgroup, (*it)->init_cgroup, user_slice, uid))
(*it)->systemd_user_slice = true;
free(user_slice);
}
}
free(basecginfo);
return true;
}
/* Check whether @path is a cgroupfs v1 or cgroupfs v2 mount. Returns -1 if
* statfs fails. If @path is null /sys/fs/cgroup is checked.
*/
static inline int cg_get_version_of_mntpt(const char *path)
{
if (has_fs_type(path, CGROUP_SUPER_MAGIC))
return 1;
if (has_fs_type(path, CGROUP2_SUPER_MAGIC))
return 2;
return 0;
}
/* Detect and store information about the cgroupfs v2 hierarchy. Currently only
* deals with the empty v2 hierarchy as we do not retrieve enabled controllers.
*/
static bool cgv2_init(uid_t uid, gid_t gid)
{
char *mountpoint;
FILE *f = NULL;
char *current_cgroup = NULL, *init_cgroup = NULL;
char * line = NULL;
size_t len = 0;
int ret = false;
current_cgroup = cgv2_get_current_cgroup(getpid());
if (!current_cgroup) {
/* No v2 hierarchy present. We're done. */
ret = true;
goto cleanup;
}
init_cgroup = cgv2_get_current_cgroup(1);
if (!init_cgroup) {
/* If we're here and didn't fail already above, then something's
* certainly wrong, so error this time.
*/
goto cleanup;
}
cg_systemd_prune_init_scope(init_cgroup);
/* Check if the v2 hierarchy is mounted at its standard location.
* If so we can skip the rest of the work here. Although the unified
* hierarchy can be mounted multiple times, each of those mountpoints
* will expose identical information.
*/
if (cg_get_version_of_mntpt("/sys/fs/cgroup") == 2) {
char *user_slice;
bool has_user_slice = false;
mountpoint = must_copy_string("/sys/fs/cgroup");
if (!mountpoint)
goto cleanup;
user_slice = must_make_path(mountpoint, current_cgroup, NULL);
if (cg_systemd_created_user_slice(current_cgroup, init_cgroup, user_slice, uid))
has_user_slice = true;
free(user_slice);
cgv2_add_controller(NULL, mountpoint, current_cgroup, init_cgroup, has_user_slice);
ret = true;
goto cleanup;
}
f = fopen("/proc/self/mountinfo", "r");
if (!f)
goto cleanup;
/* we support simple cgroup mounts and lxcfs mounts */
while (getline(&line, &len, f) != -1) {
char *user_slice;
bool has_user_slice = false;
if (!is_cgv2(line))
continue;
mountpoint = get_mountpoint(line);
if (!mountpoint)
continue;
user_slice = must_make_path(mountpoint, current_cgroup, NULL);
if (cg_systemd_created_user_slice(current_cgroup, init_cgroup, user_slice, uid))
has_user_slice = true;
free(user_slice);
cgv2_add_controller(NULL, mountpoint, current_cgroup, init_cgroup, has_user_slice);
/* Although the unified hierarchy can be mounted multiple times,
* each of those mountpoints will expose identical information.
* So let the first mountpoint we find, win.
*/
ret = true;
break;
}
pam_cgfs_debug("Detected cgroupfs v2 hierarchy at mountpoint \"%s\" with "
"current cgroup \"%s\" and init cgroup \"%s\"\n",
mountpoint, current_cgroup, init_cgroup);
cleanup:
if (f)
fclose(f);
free(line);
if (!ret) {
free(init_cgroup);
free(current_cgroup);
}
return ret;
}
/* Detect and store information about mounted cgroupfs v1 hierarchies and the
* cgroupfs v2 hierarchy.
* Detect whether we are on a pure cgroupfs v1, cgroupfs v2, or mixed system,
* where some controllers are mounted into their standard cgroupfs v1 locations
* (/sys/fs/cgroup/<controller>) and others are mounted into the cgroupfs v2
* hierarchy (/sys/fs/cgroup).
*/
static bool cg_init(uid_t uid, gid_t gid)
{
if (!cgv1_init(uid, gid))
return false;
if (!cgv2_init(uid, gid))
return false;
if (cgv1_hierarchies && cgv2_hierarchies) {
cg_mount_mode = CGROUP_MIXED;
pam_cgfs_debug("%s\n", "Detected cgroupfs v1 and v2 hierarchies");
} else if (cgv1_hierarchies && !cgv2_hierarchies) {
cg_mount_mode = CGROUP_PURE_V1;
pam_cgfs_debug("%s\n", "Detected cgroupfs v1 hierarchies");
} else if (cgv2_hierarchies && !cgv1_hierarchies) {
cg_mount_mode = CGROUP_PURE_V2;
pam_cgfs_debug("%s\n", "Detected cgroupfs v2 hierarchies");
} else {
cg_mount_mode = CGROUP_UNKNOWN;
mysyslog(LOG_ERR, "Could not detect cgroupfs hierarchy\n", NULL);
}
if (cg_mount_mode == CGROUP_UNKNOWN)
return false;
return true;
}
/* Try to move/migrate us into @cgroup in a cgroupfs v1 hierarchy. */
static bool cgv1_enter(const char *cgroup)
{
struct cgv1_hierarchy **it;
for (it = cgv1_hierarchies; it && *it; it++) {
char **controller;
bool entered = false;
if (!(*it)->controllers || !(*it)->mountpoint ||
!(*it)->init_cgroup || !(*it)->create_rw_cgroup)
continue;
for (controller = (*it)->controllers; controller && *controller;
controller++) {
char *path;
/* We've already been placed in a user slice, so we
* don't need to enter the cgroup again.
*/
if ((*it)->systemd_user_slice) {
entered = true;
break;
}
path = must_make_path((*it)->mountpoint,
(*it)->init_cgroup,
cgroup,
"/cgroup.procs",
NULL);
if (!file_exists(path)) {
free(path);
path = must_make_path((*it)->mountpoint,
(*it)->init_cgroup,
cgroup,
"/tasks",
NULL);
}
pam_cgfs_debug("Attempting to enter cgroupfs v1 hierarchy in \"%s\" cgroup\n", path);
entered = write_int(path, (int)getpid());
if (entered) {
free(path);
break;
}
pam_cgfs_debug("Failed to enter cgroupfs v1 hierarchy in \"%s\" cgroup\n", path);
free(path);
}
if (!entered)
return false;
}
return true;
}
/* Try to move/migrate us into @cgroup in the cgroupfs v2 hierarchy. */
static bool cgv2_enter(const char *cgroup)
{
struct cgv2_hierarchy *v2;
char *path;
bool entered = false;
if (!cgv2_hierarchies)
return true;
v2 = *cgv2_hierarchies;
if (!v2->mountpoint || !v2->base_cgroup)
return false;
if (!v2->create_rw_cgroup || v2->systemd_user_slice)
return true;
path = must_make_path(v2->mountpoint, v2->base_cgroup, cgroup, "/cgroup.procs", NULL);
pam_cgfs_debug("Attempting to enter cgroupfs v2 hierarchy in cgroup \"%s\"\n", path);
entered = write_int(path, (int)getpid());
if (!entered) {
pam_cgfs_debug("Failed to enter cgroupfs v2 hierarchy in cgroup \"%s\"\n", path);
free(path);
return false;
}
free(path);
return true;
}
/* Wrapper around cgv{1,2}_enter(). */
static bool cg_enter(const char *cgroup)
{
if (!cgv1_enter(cgroup)) {
mysyslog(LOG_WARNING, "cgroupfs v1: Failed to enter cgroups\n", NULL);
return false;
}
if (!cgv2_enter(cgroup)) {
mysyslog(LOG_WARNING, "cgroupfs v2: Failed to enter cgroups\n", NULL);
return false;
}
return true;
}
/* Escape to root cgroup in all detected cgroupfs v1 hierarchies. */
static void cgv1_escape(void)
{
struct cgv1_hierarchy **it;
/* In case systemd hasn't already placed us in a user slice for the
* cpuset v1 controller we will reside in the root cgroup. This means
* that cgroup.clone_children will not have been initialized for us so
* we need to do it.
*/
for (it = cgv1_hierarchies; it && *it; it++)
if (!cgv1_handle_root_cpuset_hierarchy(*it))
mysyslog(LOG_WARNING, "cgroupfs v1: Failed to initialize cpuset\n", NULL);
if (!cgv1_enter("/"))
mysyslog(LOG_WARNING, "cgroupfs v1: Failed to escape to init's cgroup\n", NULL);
}
/* Escape to root cgroup in the cgroupfs v2 hierarchy. */
static void cgv2_escape(void)
{
if (!cgv2_enter("/"))
mysyslog(LOG_WARNING, "cgroupfs v2: Failed to escape to init's cgroup\n", NULL);
}
/* Wrapper around cgv{1,2}_escape(). */
static void cg_escape(void)
{
cgv1_escape();
cgv2_escape();
}
/* Get uid and gid for @user. */
static bool get_uid_gid(const char *user, uid_t *uid, gid_t *gid)
{
struct passwd pwent;
struct passwd *pwentp = NULL;
char *buf;
size_t bufsize;
int ret;
bufsize = sysconf(_SC_GETPW_R_SIZE_MAX);
if (bufsize == -1)
bufsize = 1024;
buf = malloc(bufsize);
if (!buf)
return false;
ret = getpwnam_r(user, &pwent, buf, bufsize, &pwentp);
if (!pwentp) {
if (ret == 0)
mysyslog(LOG_ERR,
"Could not find matched password record\n", NULL);
free(buf);
return false;
}
*uid = pwent.pw_uid;
*gid = pwent.pw_gid;
free(buf);
return true;
}
/* Check if cgroup belongs to our uid and gid. If so, reuse it. */
static bool cg_belongs_to_uid_gid(const char *path, uid_t uid, gid_t gid)
{
struct stat statbuf;
if (stat(path, &statbuf) < 0)
return false;
if (!(statbuf.st_uid == uid) || !(statbuf.st_gid == gid))
return false;
return true;
}
/* Create cpumask from cpulist aka turn:
*
* 0,2-3
*
* into bit array
*
* 1 0 1 1
*/
static uint32_t *cg_cpumask(char *buf, size_t nbits)
{
char *token;
char *saveptr = NULL;
size_t arrlen = BITS_TO_LONGS(nbits);
uint32_t *bitarr = calloc(arrlen, sizeof(uint32_t));
if (!bitarr)
return NULL;
for (; (token = strtok_r(buf, ",", &saveptr)); buf = NULL) {
errno = 0;
unsigned start = strtoul(token, NULL, 0);
unsigned end = start;
char *range = strchr(token, '-');
if (range)
end = strtoul(range + 1, NULL, 0);
if (!(start <= end)) {
free(bitarr);
return NULL;
}
if (end >= nbits) {
free(bitarr);
return NULL;
}
while (start <= end)
set_bit(start++, bitarr);
}
return bitarr;
}
static char *string_join(const char *sep, const char **parts, bool use_as_prefix)
{
char *result;
char **p;
size_t sep_len = strlen(sep);
size_t result_len = use_as_prefix * sep_len;
size_t buf_len;
if (!parts)
return NULL;
/* calculate new string length */
for (p = (char **)parts; *p; p++)
result_len += (p > (char **)parts) * sep_len + strlen(*p);
buf_len = result_len + 1;
result = calloc(buf_len, sizeof(char));
if (!result)
return NULL;
if (use_as_prefix)
(void)strlcpy(result, sep, buf_len * sizeof(char));
for (p = (char **)parts; *p; p++) {
if (p > (char **)parts)
(void)strlcat(result, sep, buf_len * sizeof(char));
(void)strlcat(result, *p, buf_len * sizeof(char));
}
return result;
}
/* The largest integer that can fit into long int is 2^64. This is a
* 20-digit number.
*/
#define __IN_TO_STR_LEN 21
/* Turn cpumask into simple, comma-separated cpulist. */
static char *cg_cpumask_to_cpulist(uint32_t *bitarr, size_t nbits)
{
size_t i;
int ret;
char numstr[__IN_TO_STR_LEN] = {0};
char **cpulist = NULL;
for (i = 0; i <= nbits; i++) {
if (is_set(i, bitarr)) {
ret = snprintf(numstr, __IN_TO_STR_LEN, "%zu", i);
if (ret < 0 || (size_t)ret >= __IN_TO_STR_LEN) {
free_string_list(cpulist);
return NULL;
}
must_append_string(&cpulist, numstr);
}
}
return string_join(",", (const char **)cpulist, false);
}
static ssize_t cg_get_max_cpus(char *cpulist)
{
char *c1, *c2;
char *maxcpus = cpulist;
size_t cpus = 0;
c1 = strrchr(maxcpus, ',');
if (c1)
c1++;
c2 = strrchr(maxcpus, '-');
if (c2)
c2++;
if (!c1 && !c2)
c1 = maxcpus;
else if (c1 < c2)
c1 = c2;
if (!c1)
return -1;
/* If the above logic is correct, c1 should always hold a valid string
* here.
*/
errno = 0;
cpus = strtoul(c1, NULL, 0);
if (errno != 0)
return -1;
return cpus;
}
#define __ISOL_CPUS "/sys/devices/system/cpu/isolated"
static bool cg_filter_and_set_cpus(char *path, bool am_initialized)
{
char *lastslash, *fpath, oldv;
int ret;
ssize_t i;
ssize_t maxposs = 0, maxisol = 0;
char *cpulist = NULL, *posscpus = NULL, *isolcpus = NULL;
uint32_t *possmask = NULL, *isolmask = NULL;
bool bret = false, flipped_bit = false;
lastslash = strrchr(path, '/');
if (!lastslash) { // bug... this shouldn't be possible
pam_cgfs_debug("Invalid path: %s\n", path);
return bret;
}
oldv = *lastslash;
*lastslash = '\0';
fpath = must_make_path(path, "cpuset.cpus", NULL);
posscpus = read_file(fpath);
if (!posscpus) {
pam_cgfs_debug("Could not read file: %s\n", fpath);
goto on_error;
}
/* Get maximum number of cpus found in possible cpuset. */
maxposs = cg_get_max_cpus(posscpus);
if (maxposs < 0 || maxposs >= INT_MAX - 1)
goto on_error;
if (!file_exists(__ISOL_CPUS)) {
/* This system doesn't expose isolated cpus. */
pam_cgfs_debug("%s", "Path: "__ISOL_CPUS" to read isolated cpus from does not exist\n");
cpulist = posscpus;
/* No isolated cpus but we weren't already initialized by
* someone. We should simply copy the parents cpuset.cpus
* values.
*/
if (!am_initialized) {
pam_cgfs_debug("%s", "Copying cpuset of parent cgroup\n");
goto copy_parent;
}
/* No isolated cpus but we were already initialized by someone.
* Nothing more to do for us.
*/
goto on_success;
}
isolcpus = read_file(__ISOL_CPUS);
if (!isolcpus) {
pam_cgfs_debug("%s", "Could not read file "__ISOL_CPUS"\n");
goto on_error;
}
if (!isdigit(isolcpus[0])) {
pam_cgfs_debug("%s", "No isolated cpus detected\n");
cpulist = posscpus;
/* No isolated cpus but we weren't already initialized by
* someone. We should simply copy the parents cpuset.cpus
* values.
*/
if (!am_initialized) {
pam_cgfs_debug("%s", "Copying cpuset of parent cgroup\n");
goto copy_parent;
}
/* No isolated cpus but we were already initialized by someone.
* Nothing more to do for us.
*/
goto on_success;
}
/* Get maximum number of cpus found in isolated cpuset. */
maxisol = cg_get_max_cpus(isolcpus);
if (maxisol < 0 || maxisol >= INT_MAX - 1)
goto on_error;
if (maxposs < maxisol)
maxposs = maxisol;
maxposs++;
possmask = cg_cpumask(posscpus, maxposs);
if (!possmask) {
pam_cgfs_debug("%s", "Could not create cpumask for all possible cpus\n");
goto on_error;
}
isolmask = cg_cpumask(isolcpus, maxposs);
if (!isolmask) {
pam_cgfs_debug("%s", "Could not create cpumask for all isolated cpus\n");
goto on_error;
}
for (i = 0; i <= maxposs; i++) {
if (is_set(i, isolmask) && is_set(i, possmask)) {
flipped_bit = true;
clear_bit(i, possmask);
}
}
if (!flipped_bit) {
pam_cgfs_debug("%s", "No isolated cpus present in cpuset\n");
goto on_success;
}
pam_cgfs_debug("%s", "Removed isolated cpus from cpuset\n");
cpulist = cg_cpumask_to_cpulist(possmask, maxposs);
if (!cpulist) {
pam_cgfs_debug("%s", "Could not create cpu list\n");
goto on_error;
}
copy_parent:
*lastslash = oldv;
free(fpath);
fpath = must_make_path(path, "cpuset.cpus", NULL);
ret = lxc_write_to_file(fpath, cpulist, strlen(cpulist), false, 0660);
if (ret < 0) {
pam_cgfs_debug("Could not write cpu list to: %s\n", fpath);
goto on_error;
}
on_success:
bret = true;
on_error:
*lastslash = oldv;
free(fpath);
free(isolcpus);
free(isolmask);
if (posscpus != cpulist)
free(posscpus);
free(possmask);
free(cpulist);
return bret;
}
/* Copy contents of parent(@path)/@file to @path/@file */
static bool cg_copy_parent_file(char *path, char *file)
{
char *lastslash, *value = NULL, *fpath, oldv;
int len = 0;
int ret;
lastslash = strrchr(path, '/');
if (!lastslash) { // bug... this shouldn't be possible
pam_cgfs_debug("cgfsng:copy_parent_file: bad path %s", path);
return false;
}
oldv = *lastslash;
*lastslash = '\0';
fpath = must_make_path(path, file, NULL);
len = lxc_read_from_file(fpath, NULL, 0);
if (len <= 0) {
pam_cgfs_debug("Failed to read %s: %s", fpath, strerror(errno));
goto bad;
}
value = must_realloc(NULL, len + 1);
if (lxc_read_from_file(fpath, value, len) != len) {
pam_cgfs_debug("Failed to read %s: %s", fpath, strerror(errno));
goto bad;
}
free(fpath);
*lastslash = oldv;
fpath = must_make_path(path, file, NULL);
ret = lxc_write_to_file(fpath, value, len, false, 0660);
if (ret < 0)
pam_cgfs_debug("Unable to write %s to %s", value, fpath);
free(fpath);
free(value);
return ret >= 0;
bad:
pam_cgfs_debug("Error reading '%s'", fpath);
free(fpath);
free(value);
return false;
}
/* In case systemd hasn't already placed us in a user slice for the cpuset v1
* controller we will reside in the root cgroup. This means that
* cgroup.clone_children will not have been initialized for us so we need to do
* it.
*/
static bool cgv1_handle_root_cpuset_hierarchy(struct cgv1_hierarchy *h)
{
char *clonechildrenpath, v;
if (!string_in_list(h->controllers, "cpuset"))
return true;
clonechildrenpath = must_make_path(h->mountpoint, "cgroup.clone_children", NULL);
if (lxc_read_from_file(clonechildrenpath, &v, 1) < 0) {
pam_cgfs_debug("Failed to read %s: %s", clonechildrenpath, strerror(errno));
free(clonechildrenpath);
return false;
}
if (v == '1') { /* already set for us by someone else */
free(clonechildrenpath);
return true;
}
if (lxc_write_to_file(clonechildrenpath, "1", 1, false, 0660) < 0) {
/* Set clone_children so children inherit our settings */
pam_cgfs_debug("Failed to write 1 to %s", clonechildrenpath);
free(clonechildrenpath);
return false;
}
free(clonechildrenpath);
return true;
}
/*
* Initialize the cpuset hierarchy in first directory of @gname and
* set cgroup.clone_children so that children inherit settings.
* Since the h->base_path is populated by init or ourselves, we know
* it is already initialized.
*/
static bool cgv1_handle_cpuset_hierarchy(struct cgv1_hierarchy *h,
const char *cgroup)
{
char *cgpath, *clonechildrenpath, v, *slash;
if (!string_in_list(h->controllers, "cpuset"))
return true;
if (*cgroup == '/')
cgroup++;
slash = strchr(cgroup, '/');
if (slash)
*slash = '\0';
cgpath = must_make_path(h->mountpoint, h->base_cgroup, cgroup, NULL);
if (slash)
*slash = '/';
if (do_mkdir(cgpath, 0755) < 0 && errno != EEXIST) {
pam_cgfs_debug("Failed to create '%s'", cgpath);
free(cgpath);
return false;
}
clonechildrenpath = must_make_path(cgpath, "cgroup.clone_children", NULL);
if (!file_exists(clonechildrenpath)) { /* unified hierarchy doesn't have clone_children */
free(clonechildrenpath);
free(cgpath);
return true;
}
if (lxc_read_from_file(clonechildrenpath, &v, 1) < 0) {
pam_cgfs_debug("Failed to read %s: %s", clonechildrenpath, strerror(errno));
free(clonechildrenpath);
free(cgpath);
return false;
}
/* Make sure any isolated cpus are removed from cpuset.cpus. */
if (!cg_filter_and_set_cpus(cgpath, v == '1')) {
pam_cgfs_debug("%s", "Failed to remove isolated cpus\n");
free(clonechildrenpath);
free(cgpath);
return false;
}
if (v == '1') { /* already set for us by someone else */
pam_cgfs_debug("%s", "\"cgroup.clone_children\" was already set to \"1\"\n");
free(clonechildrenpath);
free(cgpath);
return true;
}
/* copy parent's settings */
if (!cg_copy_parent_file(cgpath, "cpuset.mems")) {
pam_cgfs_debug("%s", "Failed to copy \"cpuset.mems\" settings\n");
free(cgpath);
free(clonechildrenpath);
return false;
}
free(cgpath);
if (lxc_write_to_file(clonechildrenpath, "1", 1, false, 0660) < 0) {
/* Set clone_children so children inherit our settings */
pam_cgfs_debug("Failed to write 1 to %s", clonechildrenpath);
free(clonechildrenpath);
return false;
}
free(clonechildrenpath);
return true;
}
/* Create and chown @cgroup for all given controllers in a cgroupfs v1 hierarchy
* (For example, create @cgroup for the cpu and cpuacct controller mounted into
* /sys/fs/cgroup/cpu,cpuacct). Check if the path already exists and report back
* to the caller in @existed.
*/
#define __PAM_CGFS_USER "/user/"
#define __PAM_CGFS_USER_LEN 6
static bool cgv1_create_one(struct cgv1_hierarchy *h, const char *cgroup, uid_t uid, gid_t gid, bool *existed)
{
char *clean_base_cgroup, *path;
char **controller;
struct cgv1_hierarchy *it;
bool created = false;
*existed = false;
it = h;
for (controller = it->controllers; controller && *controller;
controller++) {
if (!cgv1_handle_cpuset_hierarchy(it, cgroup))
return false;
/* If systemd has already created a cgroup for us, keep using
* it.
*/
if (cg_systemd_chown_existing_cgroup(it->mountpoint,
it->base_cgroup, uid, gid,
it->systemd_user_slice))
return true;
/* We need to make sure that we do not create an endless chain
* of sub-cgroups. So we check if we have already logged in
* somehow (sudo -i, su, etc.) and have created a
* /user/PAM_user/idx cgroup. If so, we skip that part. For most
* cgroups this is unnecessary since we use the init_cgroup
* anyway, but for controllers which have an existing systemd
* cgroup that does not match the current uid, this is pretty
* useful.
*/
if (strncmp(it->base_cgroup, __PAM_CGFS_USER, __PAM_CGFS_USER_LEN) == 0) {
free(it->base_cgroup);
it->base_cgroup = must_copy_string("/");
} else {
clean_base_cgroup =
strstr(it->base_cgroup, __PAM_CGFS_USER);
if (clean_base_cgroup)
*clean_base_cgroup = '\0';
}
path = must_make_path(it->mountpoint, it->init_cgroup, cgroup, NULL);
pam_cgfs_debug("Constructing path: %s\n", path);
if (file_exists(path)) {
bool our_cg = cg_belongs_to_uid_gid(path, uid, gid);
if (our_cg)
*existed = false;
else
*existed = true;
pam_cgfs_debug("%s existed and does %shave our uid: %d and gid: %d\n",
path, our_cg ? "" : "not ", uid, gid);
free(path);
return our_cg;
}
created = mkdir_parent(it->mountpoint, path);
if (!created) {
free(path);
continue;
}
if (chown(path, uid, gid) < 0)
mysyslog(LOG_WARNING,
"Failed to chown %s to %d:%d: %s\n", path,
(int)uid, (int)gid, strerror(errno), NULL);
pam_cgfs_debug("Chowned %s to %d:%d\n", path, (int)uid, (int)gid);
free(path);
break;
}
return created;
}
/* Try to remove @cgroup for all given controllers in a cgroupfs v1 hierarchy
* (For example, try to remove @cgroup for the cpu and cpuacct controller
* mounted into /sys/fs/cgroup/cpu,cpuacct). Ignores failures.
*/
static bool cgv1_remove_one(struct cgv1_hierarchy *h, const char *cgroup)
{
char *path;
/* Better safe than sorry. */
if (!h->controllers)
return true;
/* Cgroups created by systemd for us which we re-use won't be removed
* here, since we're using init_cgroup + cgroup as path instead of
* base_cgroup + cgroup.
*/
path = must_make_path(h->mountpoint, h->init_cgroup, cgroup, NULL);
(void)recursive_rmdir(path);
free(path);
return true;
}
/* Try to remove @cgroup the cgroupfs v2 hierarchy. */
static bool cgv2_remove(const char *cgroup)
{
struct cgv2_hierarchy *v2;
char *path;
if (!cgv2_hierarchies)
return true;
v2 = *cgv2_hierarchies;
/* If we reused an already existing cgroup, don't bother trying to
* remove (a potentially wrong)/the path.
* Cgroups created by systemd for us which we re-use would be removed
* here, since we're using base_cgroup + cgroup as path.
*/
if (v2->systemd_user_slice)
return true;
path = must_make_path(v2->mountpoint, v2->base_cgroup, cgroup, NULL);
(void)recursive_rmdir(path);
free(path);
return true;
}
/* Create @cgroup in all detected cgroupfs v1 hierarchy. If the creation fails
* for any cgroupfs v1 hierarchy, remove all we have created so far. Report
* back, to the caller if the creation failed due to @cgroup already existing
* via @existed.
*/
static bool cgv1_create(const char *cgroup, uid_t uid, gid_t gid, bool *existed)
{
struct cgv1_hierarchy **it, **rev_it;
bool all_created = true;
for (it = cgv1_hierarchies; it && *it; it++) {
if (!(*it)->controllers || !(*it)->mountpoint ||
!(*it)->init_cgroup || !(*it)->create_rw_cgroup)
continue;
if (!cgv1_create_one(*it, cgroup, uid, gid, existed)) {
all_created = false;
break;
}
}
if (all_created)
return true;
for (rev_it = cgv1_hierarchies; rev_it && *rev_it && (*rev_it != *it);
rev_it++)
cgv1_remove_one(*rev_it, cgroup);
return false;
}
/* Create @cgroup in the cgroupfs v2 hierarchy. Report back, to the caller if
* the creation failed due to @cgroup already existing via @existed.
*/
static bool cgv2_create(const char *cgroup, uid_t uid, gid_t gid, bool *existed)
{
int ret;
char *clean_base_cgroup;
char *path;
struct cgv2_hierarchy *v2;
bool our_cg = false, created = false;
*existed = false;
if (!cgv2_hierarchies || !(*cgv2_hierarchies)->create_rw_cgroup)
return true;
v2 = *cgv2_hierarchies;
/* We can't be placed under init's cgroup for the v2 hierarchy. We need
* to be placed under our current cgroup.
*/
if (cg_systemd_chown_existing_cgroup(v2->mountpoint, v2->base_cgroup,
uid, gid, v2->systemd_user_slice))
goto delegate_files;
/* We need to make sure that we do not create an endless chain of
* sub-cgroups. So we check if we have already logged in somehow (sudo
* -i, su, etc.) and have created a /user/PAM_user/idx cgroup. If so, we
* skip that part.
*/
if (strncmp(v2->base_cgroup, __PAM_CGFS_USER, __PAM_CGFS_USER_LEN) == 0) {
free(v2->base_cgroup);
v2->base_cgroup = must_copy_string("/");
} else {
clean_base_cgroup = strstr(v2->base_cgroup, __PAM_CGFS_USER);
if (clean_base_cgroup)
*clean_base_cgroup = '\0';
}
path = must_make_path(v2->mountpoint, v2->base_cgroup, cgroup, NULL);
pam_cgfs_debug("Constructing path \"%s\"\n", path);
if (file_exists(path)) {
our_cg = cg_belongs_to_uid_gid(path, uid, gid);
pam_cgfs_debug("%s existed and does %shave our uid: %d and gid: %d\n",
path, our_cg ? "" : "not ", uid, gid);
free(path);
if (our_cg) {
*existed = false;
goto delegate_files;
} else {
*existed = true;
return false;
}
}
created = mkdir_parent(v2->mountpoint, path);
if (!created) {
free(path);
return false;
}
/* chown cgroup to user */
if (chown(path, uid, gid) < 0)
mysyslog(LOG_WARNING, "Failed to chown %s to %d:%d: %s\n",
path, (int)uid, (int)gid, strerror(errno), NULL);
else
pam_cgfs_debug("Chowned %s to %d:%d\n", path, (int)uid, (int)gid);
free(path);
delegate_files:
/* chown cgroup.procs to user */
if (v2->systemd_user_slice)
path = must_make_path(v2->mountpoint, v2->base_cgroup,
"/cgroup.procs", NULL);
else
path = must_make_path(v2->mountpoint, v2->base_cgroup, cgroup,
"/cgroup.procs", NULL);
ret = chown(path, uid, gid);
if (ret < 0)
mysyslog(LOG_WARNING, "Failed to chown %s to %d:%d: %s\n",
path, (int)uid, (int)gid, strerror(errno), NULL);
else
pam_cgfs_debug("Chowned %s to %d:%d\n", path, (int)uid, (int)gid);
free(path);
/* chown cgroup.subtree_control to user */
if (v2->systemd_user_slice)
path = must_make_path(v2->mountpoint, v2->base_cgroup,
"/cgroup.subtree_control", NULL);
else
path = must_make_path(v2->mountpoint, v2->base_cgroup, cgroup,
"/cgroup.subtree_control", NULL);
ret = chown(path, uid, gid);
if (ret < 0)
mysyslog(LOG_WARNING, "Failed to chown %s to %d:%d: %s\n",
path, (int)uid, (int)gid, strerror(errno), NULL);
free(path);
/* chown cgroup.threads to user */
if (v2->systemd_user_slice)
path = must_make_path(v2->mountpoint, v2->base_cgroup,
"/cgroup.threads", NULL);
else
path = must_make_path(v2->mountpoint, v2->base_cgroup, cgroup,
"/cgroup.threads", NULL);
ret = chown(path, uid, gid);
if (ret < 0 && errno != ENOENT)
mysyslog(LOG_WARNING, "Failed to chown %s to %d:%d: %s\n",
path, (int)uid, (int)gid, strerror(errno), NULL);
free(path);
return true;
}
/* Create writeable cgroups for @user at login. Details can be found in the
* preamble/license at the top of this file.
*/
static int handle_login(const char *user, uid_t uid, gid_t gid)
{
int idx = 0, ret;
bool existed;
char cg[PATH_MAX];
cg_escape();
while (idx >= 0) {
ret = snprintf(cg, PATH_MAX, "/user/%s/%d", user, idx);
if (ret < 0 || ret >= PATH_MAX) {
mysyslog(LOG_ERR, "Username too long\n", NULL);
return PAM_SESSION_ERR;
}
existed = false;
if (!cgv2_create(cg, uid, gid, &existed)) {
if (existed) {
cgv2_remove(cg);
idx++;
continue;
}
mysyslog(LOG_ERR, "Failed to create a cgroup for user %s\n", user, NULL);
return PAM_SESSION_ERR;
}
existed = false;
if (!cgv1_create(cg, uid, gid, &existed)) {
if (existed) {
cgv2_remove(cg);
idx++;
continue;
}
mysyslog(LOG_ERR, "Failed to create a cgroup for user %s\n", user, NULL);
return PAM_SESSION_ERR;
}
if (!cg_enter(cg)) {
mysyslog( LOG_ERR, "Failed to enter user cgroup %s for user %s\n", cg, user, NULL);
return PAM_SESSION_ERR;
}
break;
}
return PAM_SUCCESS;
}
/* Try to prune cgroups we created and that now are empty from all cgroupfs v1
* hierarchies.
*/
static bool cgv1_prune_empty_cgroups(const char *user)
{
bool controller_removed = true;
bool all_removed = true;
struct cgv1_hierarchy **it;
for (it = cgv1_hierarchies; it && *it; it++) {
int ret;
char *path_base, *path_init;
char **controller;
if (!(*it)->controllers || !(*it)->mountpoint ||
!(*it)->init_cgroup || !(*it)->create_rw_cgroup)
continue;
for (controller = (*it)->controllers; controller && *controller;
controller++) {
bool path_base_rm, path_init_rm;
path_base = must_make_path((*it)->mountpoint, (*it)->base_cgroup, "/user", user, NULL);
pam_cgfs_debug("cgroupfs v1: Trying to prune \"%s\"\n", path_base);
ret = recursive_rmdir(path_base);
if (ret == -ENOENT || ret >= 0)
path_base_rm = true;
else
path_base_rm = false;
free(path_base);
path_init = must_make_path((*it)->mountpoint, (*it)->init_cgroup, "/user", user, NULL);
pam_cgfs_debug("cgroupfs v1: Trying to prune \"%s\"\n", path_init);
ret = recursive_rmdir(path_init);
if (ret == -ENOENT || ret >= 0)
path_init_rm = true;
else
path_init_rm = false;
free(path_init);
if (!path_base_rm && !path_init_rm) {
controller_removed = false;
continue;
}
controller_removed = true;
break;
}
if (!controller_removed)
all_removed = false;
}
return all_removed;
}
/* Try to prune cgroup we created and that now is empty from the cgroupfs v2
* hierarchy.
*/
static bool cgv2_prune_empty_cgroups(const char *user)
{
int ret;
struct cgv2_hierarchy *v2;
char *path_base, *path_init;
bool path_base_rm, path_init_rm;
if (!cgv2_hierarchies)
return true;
v2 = *cgv2_hierarchies;
path_base = must_make_path(v2->mountpoint, v2->base_cgroup, "/user", user, NULL);
pam_cgfs_debug("cgroupfs v2: Trying to prune \"%s\"\n", path_base);
ret = recursive_rmdir(path_base);
if (ret == -ENOENT || ret >= 0)
path_base_rm = true;
else
path_base_rm = false;
free(path_base);
path_init = must_make_path(v2->mountpoint, v2->init_cgroup, "/user", user, NULL);
pam_cgfs_debug("cgroupfs v2: Trying to prune \"%s\"\n", path_init);
ret = recursive_rmdir(path_init);
if (ret == -ENOENT || ret >= 0)
path_init_rm = true;
else
path_init_rm = false;
free(path_init);
if (!path_base_rm && !path_init_rm)
return false;
return true;
}
/* Wrapper around cgv{1,2}_prune_empty_cgroups(). */
static void cg_prune_empty_cgroups(const char *user)
{
(void)cgv1_prune_empty_cgroups(user);
(void)cgv2_prune_empty_cgroups(user);
}
/* Free allocated information for detected cgroupfs v1 hierarchies. */
static void cgv1_free_hierarchies(void)
{
struct cgv1_hierarchy **it;
if (!cgv1_hierarchies)
return;
for (it = cgv1_hierarchies; it && *it; it++) {
if ((*it)->controllers) {
char **tmp;
for (tmp = (*it)->controllers; tmp && *tmp; tmp++)
free(*tmp);
free((*it)->controllers);
}
free((*it)->mountpoint);
free((*it)->base_cgroup);
free((*it)->fullcgpath);
free((*it)->init_cgroup);
}
free(cgv1_hierarchies);
}
/* Free allocated information for the detected cgroupfs v2 hierarchy. */
static void cgv2_free_hierarchies(void)
{
struct cgv2_hierarchy **it;
if (!cgv2_hierarchies)
return;
for (it = cgv2_hierarchies; it && *it; it++) {
if ((*it)->controllers) {
char **tmp;
for (tmp = (*it)->controllers; tmp && *tmp; tmp++)
free(*tmp);
free((*it)->controllers);
}
free((*it)->mountpoint);
free((*it)->base_cgroup);
free((*it)->fullcgpath);
free((*it)->init_cgroup);
}
free(cgv2_hierarchies);
}
/* Wrapper around cgv{1,2}_free_hierarchies(). */
static void cg_exit(void)
{
cgv1_free_hierarchies();
cgv2_free_hierarchies();
}
int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc,
const char **argv)
{
int ret;
uid_t uid = 0;
gid_t gid = 0;
const char *PAM_user = NULL;
ret = pam_get_user(pamh, &PAM_user, NULL);
if (ret != PAM_SUCCESS) {
mysyslog(LOG_ERR, "PAM-CGFS: couldn't get user\n", NULL);
return PAM_SESSION_ERR;
}
if (!get_uid_gid(PAM_user, &uid, &gid)) {
mysyslog(LOG_ERR, "Failed to get uid and gid for %s\n", PAM_user, NULL);
return PAM_SESSION_ERR;
}
if (!cg_init(uid, gid)) {
mysyslog(LOG_ERR, "Failed to get list of controllers\n", NULL);
return PAM_SESSION_ERR;
}
/* Try to prune cgroups, that are actually empty but were still marked
* as busy by the kernel so we couldn't remove them on session close.
*/
cg_prune_empty_cgroups(PAM_user);
if (cg_mount_mode == CGROUP_UNKNOWN)
return PAM_SESSION_ERR;
if (argc > 1 && !strcmp(argv[0], "-c")) {
char **clist = make_string_list(argv[1], ",");
/*
* We don't allow using "all" and other controllers explicitly because
* that simply doesn't make any sense.
*/
if (string_list_length(clist) > 1 && string_in_list(clist, "all")) {
mysyslog(LOG_ERR, "Invalid -c option, cannot specify individual controllers alongside 'all'\n", NULL);
free_string_list(clist);
return PAM_SESSION_ERR;
}
cg_mark_to_make_rw(clist);
free_string_list(clist);
}
return handle_login(PAM_user, uid, gid);
}
int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc,
const char **argv)
{
int ret;
uid_t uid = 0;
gid_t gid = 0;
const char *PAM_user = NULL;
ret = pam_get_user(pamh, &PAM_user, NULL);
if (ret != PAM_SUCCESS) {
mysyslog(LOG_ERR, "PAM-CGFS: couldn't get user\n", NULL);
return PAM_SESSION_ERR;
}
if (!get_uid_gid(PAM_user, &uid, &gid)) {
mysyslog(LOG_ERR, "Failed to get uid and gid for %s\n", PAM_user, NULL);
return PAM_SESSION_ERR;
}
if (cg_mount_mode == CGROUP_UNINITIALIZED) {
if (!cg_init(uid, gid))
mysyslog(LOG_ERR, "Failed to get list of controllers\n", NULL);
if (argc > 1 && !strcmp(argv[0], "-c")) {
char **clist = make_string_list(argv[1], ",");
/*
* We don't allow using "all" and other controllers explicitly because
* that simply doesn't make any sense.
*/
if (string_list_length(clist) > 1 && string_in_list(clist, "all")) {
mysyslog(LOG_ERR, "Invalid -c option, cannot specify individual controllers alongside 'all'\n", NULL);
free_string_list(clist);
return PAM_SESSION_ERR;
}
cg_mark_to_make_rw(clist);
free_string_list(clist);
}
}
cg_prune_empty_cgroups(PAM_user);
cg_exit();
return PAM_SUCCESS;
}
You can’t perform that action at this time.