]> git.ipfire.org Git - thirdparty/libcgroup.git/commitdiff
Merge the cgruleseng daemon from Steve Olivieri
authorBalbir Singh <balbir@linux.vnet.ibm.com>
Fri, 26 Sep 2008 11:56:34 +0000 (11:56 +0000)
committerBalbir Singh <balbir@linux.vnet.ibm.com>
Fri, 26 Sep 2008 11:56:34 +0000 (11:56 +0000)
Signed-off-by: Steve Olivieri <sjo@redhat.com>
Signed-off-by: Balbir Singh <balbir@linux.vnet.ibm.com>
git-svn-id: https://libcg.svn.sourceforge.net/svnroot/libcg/trunk@190 4f4bb910-9a46-0410-90c8-c897d4f1cd53

12 files changed:
Makefile
Makefile.in
README_daemon [new file with mode: 0644]
api.c
cgrulesengd.c [new file with mode: 0644]
cgrulesengd.h [new file with mode: 0644]
libcgroup-internal.h
libcgroup.h
samples/cgred.conf [new file with mode: 0644]
scripts/init.d/cgred [new file with mode: 0644]
tests/Makefile
tests/setuid.c [new file with mode: 0644]

index 2819e8be55dd75b190e2153c1fd3c2646b4710a9..380dde752cbe089fa9d1476dccca11c81bdb1a0a 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -21,7 +21,7 @@ bindir=${exec_prefix}/bin
 sbindir=${exec_prefix}/sbin
 libdir=${exec_prefix}/lib
 includedir=${prefix}/include
-prefix=/usr/local
+prefix=/usr
 exec_prefix=${prefix}
 INSTALL=install
 INSTALL_DATA=install -m 644
@@ -29,18 +29,22 @@ PACKAGE_VERSION=0.31
 CFLAGS=-g -O2 $(INC) -DPACKAGE_VERSION=$(PACKAGE_VERSION)
 VERSION=1
 
-all: libcgroup.so cgconfigparser cgexec cgclassify
+all: libcgroup.so cgconfigparser cgexec cgclassify cgrulesengd
 
 cgconfigparser: libcgroup.so config.c y.tab.c lex.yy.c libcgroup.h file-ops.c
        $(CC) $(CFLAGS) -o $@ y.tab.c lex.yy.c config.c file-ops.c \
-       $(LDFLAGS) $(LIBS)
+               $(LDFLAGS) $(LIBS)
 
 cgexec: libcgroup.so cgexec.c libcgroup.h
        $(CC) $(CFLAGS) -Wall -o $@ cgexec.c $(LDFLAGS) $(LIBS)
 
-cgclassify: cgclassify.c
+cgclassify: libcgroup.so cgclassify.c
        $(CC) $(CFLAGS) -Wall -o $@ cgclassify.c $(LDFLAGS) $(LIBS)
 
+cgrulesengd: libcgroup.so libcgroup.h cgrulesengd.c cgrulesengd.h
+       $(CC) -std=gnu99 $(DEBUG) $(CFLAGS) -Wall -o $@ cgrulesengd.c \
+               $(LDFLAGS) $(LIBS)
+
 y.tab.c: parse.y lex.yy.c
        $(YACC) -v -d parse.y
 
@@ -49,7 +53,7 @@ lex.yy.c: lex.l
 
 libcgroup.so: api.c libcgroup.h wrapper.c
        $(CC) $(CFLAGS) -shared -fPIC -Wl,--soname,$@.$(VERSION) -o $@ api.c \
-       wrapper.c
+               wrapper.c
        ln -sf $@ $@.$(VERSION)
 
 test:
@@ -59,14 +63,15 @@ pam_cgroup.so: pam_cgroup.c
        $(CC) $(CFLAGS) -shared -fPIC -Wall -o $@ pam_cgroup.c $(LDFLAGS) \
        $(LIBS) -lpam
 
-install: libcgroup.so cgexec cgclassify
+install: libcgroup.so cgexec cgclassify cgconfigparser
        $(INSTALL_DATA) -D libcgroup.h $(DESTDIR)$(includedir)/libcgroup.h
        $(INSTALL) -D libcgroup.so $(DESTDIR)$(libdir)/libcgroup-$(PACKAGE_VERSION).so
        ln -sf libcgroup-$(PACKAGE_VERSION).so $(DESTDIR)$(libdir)/libcgroup.so.$(VERSION)
        ln -sf libcgroup.so.$(VERSION) $(DESTDIR)$(libdir)/libcgroup.so
        $(INSTALL) -D cgconfigparser $(DESTDIR)$(sbindir)
-       $(INSTALL) cgexec $(DESTDIR)$(bindir)/cgexec
-       $(INSTALL) cgclassify $(DESTDIR)$(bindir)/cgclassify
+       $(INSTALL) -D cgexec $(DESTDIR)$(bindir)/cgexec
+       $(INSTALL) -D cgclassify $(DESTDIR)$(bindir)/cgclassify
+       $(INSTALL) -D cgrulesengd $(DESTDIR)$(bindir)/cgrulesengd
 
 uninstall: libcgroup.so
        rm -f $(DESTDIR)$(includedir)/libcgroup.h
@@ -76,8 +81,9 @@ uninstall: libcgroup.so
        rm -f $(DESTDIR)$(sbindir)/cgconfigparser
        rm -f $(DESTDIR)$(bindir)/cgexec
        rm -f $(DESTDIR)$(bindir)/cgclassify
+       rm -f $(DESTDIR)$(bindir)/cgrulesengd
 
 clean:
-       \rm -f y.tab.c y.tab.h lex.yy.c y.output libcgroup.so cgclassify\
+       \rm -f y.tab.c y.tab.h lex.yy.c y.output libcgroup.so cgclassify \
        libcgroup.so.$(VERSION) cgconfigparser config.log config.status cgexec \
-       pam_cgroup.so
+       pam_cgroup.so cgrulesengd
index f8755786ab8f4f537a1fff0d3692b081bd6d407c..aa68f4eae93b8e9f90bec26c98a022f62f64cc49 100644 (file)
@@ -29,11 +29,11 @@ PACKAGE_VERSION=@PACKAGE_VERSION@
 CFLAGS=@CFLAGS@ $(INC) -DPACKAGE_VERSION=$(PACKAGE_VERSION)
 VERSION=1
 
-all: libcgroup.so cgconfigparser cgexec cgclassify
+all: libcgroup.so cgconfigparser cgexec cgclassify cgrulesengd
 
 cgconfigparser: libcgroup.so config.c y.tab.c lex.yy.c libcgroup.h file-ops.c
        $(CC) $(CFLAGS) -o $@ y.tab.c lex.yy.c config.c file-ops.c \
-       $(LDFLAGS) $(LIBS)
+               $(LDFLAGS) $(LIBS)
 
 cgexec: libcgroup.so cgexec.c libcgroup.h
        $(CC) $(CFLAGS) -Wall -o $@ cgexec.c $(LDFLAGS) $(LIBS)
@@ -41,6 +41,10 @@ cgexec: libcgroup.so cgexec.c libcgroup.h
 cgclassify: libcgroup.so cgclassify.c
        $(CC) $(CFLAGS) -Wall -o $@ cgclassify.c $(LDFLAGS) $(LIBS)
 
+cgrulesengd: libcgroup.so libcgroup.h cgrulesengd.c cgrulesengd.h
+       $(CC) -std=gnu99 $(DEBUG) $(CFLAGS) -Wall -o $@ cgrulesengd.c \
+               $(LDFLAGS) $(LIBS)
+
 y.tab.c: parse.y lex.yy.c
        $(YACC) -v -d parse.y
 
@@ -49,7 +53,7 @@ lex.yy.c: lex.l
 
 libcgroup.so: api.c libcgroup.h wrapper.c
        $(CC) $(CFLAGS) -shared -fPIC -Wl,--soname,$@.$(VERSION) -o $@ api.c \
-       wrapper.c
+               wrapper.c
        ln -sf $@ $@.$(VERSION)
 
 test:
@@ -67,6 +71,7 @@ install: libcgroup.so cgexec cgclassify cgconfigparser
        $(INSTALL) -D cgconfigparser $(DESTDIR)$(sbindir)
        $(INSTALL) -D cgexec $(DESTDIR)$(bindir)/cgexec
        $(INSTALL) -D cgclassify $(DESTDIR)$(bindir)/cgclassify
+       $(INSTALL) -D cgrulesengd $(DESTDIR)$(bindir)/cgrulesengd
 
 uninstall: libcgroup.so
        rm -f $(DESTDIR)$(includedir)/libcgroup.h
@@ -76,8 +81,9 @@ uninstall: libcgroup.so
        rm -f $(DESTDIR)$(sbindir)/cgconfigparser
        rm -f $(DESTDIR)$(bindir)/cgexec
        rm -f $(DESTDIR)$(bindir)/cgclassify
+       rm -f $(DESTDIR)$(bindir)/cgrulesengd
 
 clean:
        \rm -f y.tab.c y.tab.h lex.yy.c y.output libcgroup.so cgclassify \
        libcgroup.so.$(VERSION) cgconfigparser config.log config.status cgexec \
-       pam_cgroup.so
+       pam_cgroup.so cgrulesengd
diff --git a/README_daemon b/README_daemon
new file mode 100644 (file)
index 0000000..1ff7a33
--- /dev/null
@@ -0,0 +1,178 @@
+DESCRIPTION
+===========
+The CGroup Rules Engine Daemon is a tool that will automatically place tasks
+into the correct cgroup based on UID/GID events from the kernel.  It will not
+automatically classify tasks that are already running, but it will classify
+any new tasks, and any tasks which change their UID/GID.  Note that we use
+the euid and egid, not the ruid and rgid.
+
+Unlike other tools, cgrulesengd caches the rules configuration in a data
+structure (it's actually just a FIFO linked list) so that it doesn't need to
+parse the configuration file more than once.  This should be much faster than
+parsing the rules for each UID/GID event.  Eventually, this caching logic
+should be part of libcgroup, so that any other program can take advantage of it
+(and so that all programs are using the same table).  The configuration can
+be reloaded without stopping the daemon (more information below).
+
+WHY A DAEMON?
+=============
+A daemon is easy to use, and allows an administrator to ensure that all tasks
+are classified into the correct cgroups without constantly monitoring the
+system.  The daemon is transparent to the users, and does not require any
+modifications to existing userspace programs.  Finally, the daemon can be
+started and stopped at any time, including at boot time with other services.
+Thus, sytem administrators can decide not to use the daemon if they choose.
+
+Most importantly, some programs create new users and/or  run scripts,
+threads, etc. as those users using suexec().  This call does not go through
+PAM, so these scripts would continue running in the same cgroup as the parent
+program.  This behavior is likely not ideal, and the daemon would solve this
+problem.
+
+Apache does this.  Apache creates a user called 'apache' and uses setuid() to
+launch tasks as that user.  This does not go through PAM, so without a daemon,
+these tasks would continue to run in the 'root' cgroup rather than in the
+'apache' or 'webserver' cgroup.  The daemon fixes this problem by catching the
+setuid() call and moving the tasks into the correct cgroup.
+
+We would ask Apache to modify their software to interface with libcgroup, but
+this solution is less than optimal because a lot of userspace software would
+have to be changed, and some authors might intentionally not interact with
+libcgroup, which could create an exploit.  The daemon is a simple, transparent
+solution.
+
+USING THE DAEMON
+================
+The daemon can be used as a service with the cgred script, which is shipped
+as scripts/init.d/cgred.  This script should be installed as /etc/init.d/cgred
+and used like any other service.  To start the daemon,
+       /etc/init.d/cgred start
+To stop it,
+       /etc/init.d/cgred stop
+The restart (stop,start), condrestart (same as restart, but only if the daemon
+was already started), and status (print whether the daemon is started or
+stopped) commands are also supported.  An additional command, "flash", allows
+you to reload the configuration file without stopping the daemon.
+       /etc/init.d/cgred flash
+The cgred script automatically loads configuration from /etc/cgred.d/cgred.conf,
+which is shipped as samples/cgred.conf.  See that file for more information.
+
+If you choose not to run the daemon as a service, the following options are
+currently supported:
+       --nodaemon      Do not run as a daemon
+       --nolog         Write log output to stdout instead of a log file
+       --config [FILE] Read rules configuration from FILE instead of
+                       /etc/cgrules.conf
+
+You can ask the daemon to reload the configuration by sending it SIGUSR2.  The
+easiest way to do this is with the 'kill' command:
+       kill -s SIGUSR2 [PID]
+
+TESTING
+=======
+The program setuid (found in tests/setuid.c) can help you test the daemon.  By
+default, this program attempts to change its UID to root and then idles until
+you kill it.  You can change the default behavior to use a different UID, or
+you can uncomment the second block of code to instead attempt to change the
+GID.
+
+In order to make sure that everything works, I used the following rules:
+       sjo             cpu             default
+       cgtest          cpu             cgtest
+       %               memory          default
+       @cgroup         cpu,memory      cgtest
+       peter           cpu             test1
+       %               memory          test2
+       @root           *               default
+       *               *               default
+
+The users 'sjo' and 'cgtest' were normal users.  'peter' is not a user on the
+system.  The group 'cgroup' is a group containing sjo,root,cgtest as members,
+and the group 'root' contains only root.  The cgroups 'default' and 'cgtest'
+exist, while 'test1' and 'test2' do not.  Currently, the daemon does not check
+for the existance of 'test1', though this would be easier to do once the
+parsing and caching logic is moved into libcgroup.
+
+I ran the following tests, all of which were successful:
+       - set UID to sjo (should move cpu controller into default)
+       - set UID to root (should move cpu,memory controllers into cgtest)
+       - set UID to cgtest (should move cpu controller into cgtest, memory
+               controller into default)
+       - set GID to root (should move all controllers into default)
+       - set GID to cgroup (should move cpu, memory into cgtest)
+       - set GID to users (should move all controllers into default)
+
+The parsing logic will skip the 'peter' rule as well as its multi-line
+components (in this case "% memory test2"), because the user does not exist.
+This should work for group rules, too.  Attempting to setuid() or setgid() to a
+user/group that doesn't exist will just return an error and not generate a
+kernel event of the PROC_EVENT_UID or PROC_EVENT_GID type, so the daemon won't
+do anything for it.
+
+CONCERNS/ISSUES
+===============
+ - Netlink can be unreliable, and the daemon might miss an event if the buffer
+   is full.  One possible solution is to have one or two files that the kernel
+   can queue UID/GID changes in, and have the daemon read those files whenever
+   they are updated.  From testing, this does not actually appear to be a real
+   problem, but it could become one with faster machines.
+ - The daemon does not care for namespaces at all, which can cause conflicts
+   with containers.  If a user places his tasks into exec-based cgroups (such
+   as 'network' and 'development'), the daemon will not realize this and will
+   simply place them into the user's cgroup (so, sjo/ instead of sjo/network/).
+
+CHANGELOG
+=========
+V9:
+ - Updated documentation, because it was very old and incorrect.
+ - Reverted the changes to cgexec and cgclassify.
+ - New API function: cgroup_change_cgroup_uid_gid_flags().
+ - Deprecated cgroup_change_cgroup_uid_gid().
+ - Rewrote some of the rule matching and execution logic in api.c to be
+   faster, and easier to read.
+ - Changes all negative return values to positive values.  As a side effect,
+   cgroup_parse_rules() now returns -1 when we get a match and we are using
+   non-cached rules.
+ - Changes CGROUP_FUSECACHE to CGFLAG_USECACHE.
+ - Flags are now enumerated (cgflags), instead of #defines.
+
+V8:
+ - Moved the event-handling logic back into the daemon, where it should be.
+ - Changed cgroup_parse_rules() to work with cached rules or non-cached rules.
+   The other parsing function is no longer needed, and should be deprecated.
+ - Non-cached rules now work with the same structs as cached rules.
+ - Modified cgroup_change_cgroup_uid_gid() with a new 'flags' parameter.
+   Currently, the only flag is "CGROUP_FUSECACHE" to use the cached rules logic
+   (or not).
+ - Added cgroup_rules_loaded() boolean, to check whether the cached rules have
+   been loaded yet, and cgroup_init_rules_cache() to load them.
+ - Modified cgexec and cgclassify to work with the new
+   cgroup_change_cgroup_uid_gid().
+
+V7:
+ - Moved parsing and caching logic into libcgroup.
+ - Added locking mechanisms around the list of rules.
+ - Cleaned up #includes in cgrulesegnd.[h,c].
+ - Added notification if netlink receive queue overflows.
+ - Added logic to catch SIGINT in addition to SIGTERM.
+ - New API functions:
+       - cgroup_free_rule(struct cgroup_rule*)
+       - cgroup_free_rule_list(struct cgroup_rule_list*)
+       - cgroup_parse_rules(void)
+       - cgroup_print_rules_config(FILE*)
+       - cgroup_reload_cached_rules(void)
+       - cgroup_change_cgroup_event(struct proc_event*, int, FILE*)
+
+V6:
+ - Wrote new parsing logic, which is cleaner and simpler.
+ - Added cgred script to enable using the daemon as a service.
+ - Wrote caching logic to cache rules table.
+ - Added the ability to force a reload of the rules table with SIGUSR2 signal.
+ - Added two structures to libcgroup:  cgre_rule and cgre_rules_list
+ - New API function:  cgroup_reload_cached_rules, which reloads the rules table.
+ - Added logging capabilities (default log is /root/cgrulesengd.conf)
+
+TODO
+====
+ - Find a way to replace Netlink, or at least clean up that code.
+ - Find a solution to the namespace problem.
diff --git a/api.c b/api.c
index fc7e18ab5c481b3eabf078b76cc88f3b02a2e871..60a458cf90e9f2022cbc7c1e6bce4bcf4359907b 100644 (file)
--- a/api.c
+++ b/api.c
@@ -58,6 +58,18 @@ static pthread_rwlock_t cg_mount_table_lock = PTHREAD_RWLOCK_INITIALIZER;
 /* Check if cgroup_init has been called or not. */
 static int cgroup_initialized;
 
+/* Check if the rules cache has been loaded or not. */
+static bool cgroup_rules_loaded;
+
+/* List of configuration rules */
+static struct cgroup_rule_list rl;
+
+/* Temporary list of configuration rules (for non-cache apps) */
+static struct cgroup_rule_list trl;
+
+/* Lock for the list of rules (rl) */
+static pthread_rwlock_t rl_lock = PTHREAD_RWLOCK_INITIALIZER;
+
 static int cg_chown_file(FTS *fts, FTSENT *ent, uid_t owner, gid_t group)
 {
        int ret = 0;
@@ -120,6 +132,359 @@ static int cgroup_test_subsys_mounted(const char *name)
        return 0;
 }
 
+/**
+ * Free a single cgroup_rule struct.
+ *     @param r The rule to free from memory
+ */
+static void cgroup_free_rule(struct cgroup_rule *r)
+{
+       /* Loop variable */
+       int i = 0;
+
+       /* Make sure our rule is not NULL, first. */
+       if (!r) {
+               dbg("Warning: Attempted to free NULL rule.\n");
+               return;
+       }
+
+       /* We must free any used controller strings, too. */
+       for(i = 0; i < MAX_MNT_ELEMENTS; i++) {
+               if (r->controllers[i])
+                       free(r->controllers[i]);
+       }
+
+       free(r);
+}
+
+/**
+ * Free a list of cgroup_rule structs.  If rl is the main list of rules,
+ * the lock must be taken for writing before calling this function!
+ *     @param rl Pointer to the list of rules to free from memory
+ */
+static void cgroup_free_rule_list(struct cgroup_rule_list *rl)
+{
+       /* Temporary pointer */
+       struct cgroup_rule *tmp = NULL;
+
+       /* Make sure we're not freeing NULL memory! */
+       if (!(rl->head)) {
+               dbg("Warning: Attempted to free NULL list.\n");
+               return;
+       }
+
+       while (rl->head) {
+               tmp = rl->head;
+               rl->head = tmp->next;
+               cgroup_free_rule(tmp);
+       }
+
+       /* Don't leave wild pointers around! */
+       rl->head = NULL;
+       rl->tail = NULL;
+}
+
+/**
+ * Parse the configuration file that maps UID/GIDs to cgroups.  If ever the
+ * configuration file is modified, applications should call this function to
+ * load the new configuration rules.  The function caller is responsible for
+ * calling free() on each rule in the list.
+ *
+ * The cache parameter alters the behavior of this function.  If true, this
+ * function will read the entire configuration file and store the results in
+ * rl (global rules list).  If false, this function will only parse until it
+ * finds a rule matching the given UID or GID.  It will store this rule in rl,
+ * as well as any children rules (rules that begin with a %) that it has.
+ *
+ * This function is NOT thread safe!
+ *     @param cache True to cache rules, else false
+ *     @param muid If cache is false, the UID to match against
+ *     @param mgid If cache is false, the GID to match against
+ *     @return 0 on success, -1 if no cache and match found, > 0 on error.
+ * TODO: Make this function thread safe!
+ */
+static int cgroup_parse_rules(bool cache, uid_t muid, gid_t mgid)
+{
+       /* File descriptor for the configuration file */
+       FILE *fp = NULL;
+
+       /* Buffer to store the line we're working on */
+       char *buff = NULL;
+
+       /* Iterator for the line we're working on */
+       char *itr = NULL;
+
+       /* Pointer to the list that we're using */
+       struct cgroup_rule_list *lst = NULL;
+
+       /* Rule to add to the list */
+       struct cgroup_rule *newrule = NULL;
+
+       /* Structure to get GID from group name */
+       struct group *grp = NULL;
+
+       /* Structure to get UID from user name */
+       struct passwd *pwd = NULL;
+
+       /* Temporary storage for a configuration rule */
+       char user[LOGIN_NAME_MAX] = { '\0' };
+       char controllers[CG_CONTROLLER_MAX] = { '\0' };
+       char destination[FILENAME_MAX] = { '\0' };
+       uid_t uid = CGRULE_INVALID;
+       gid_t gid = CGRULE_INVALID;
+
+       /* The current line number */
+       unsigned int linenum = 0;
+
+       /* Did we skip the previous line? */
+       bool skipped = false;
+
+       /* Have we found a matching rule (non-cache mode)? */
+       bool matched = false;
+
+       /* Return codes */
+       int ret = 0;
+
+       /* Temporary buffer for strtok() */
+       char *stok_buff = NULL;
+
+       /* Loop variable. */
+       int i = 0;
+
+       /* Open the configuration file. */
+       pthread_rwlock_wrlock(&rl_lock);
+       fp = fopen(CGRULES_CONF_FILE, "r");
+       if (!fp) {
+               dbg("Failed to open configuration file %s with"
+                               " error: %s\n", CGRULES_CONF_FILE,
+                               strerror(errno));
+               ret = errno;
+               goto finish;
+       }
+
+       buff = calloc(CGROUP_RULE_MAXLINE, sizeof(char));
+       if (!buff) {
+               dbg("Out of memory?  Error: %s\n", strerror(errno));
+               ret = errno;
+               goto close_unlock;
+       }
+
+       /* Determine which list we're using. */
+       if (cache)
+               lst = &rl;
+       else
+               lst = &trl;
+
+       /* If our list already exists, clean it. */
+       if (lst->head)
+               cgroup_free_rule_list(lst);
+
+       /* Now, parse the configuration file one line at a time. */
+       dbg("Parsing configuration file.\n");
+       while ((itr = fgets(buff, CGROUP_RULE_MAXLINE, fp)) != NULL) {
+               linenum++;
+
+               /* We ignore anything after a # sign as comments. */
+               if ((itr = strchr(buff, '#')))
+                       *itr = '\0';
+
+               /* We also need to remove the newline character. */
+               if ((itr = strchr(buff, '\n')))
+                       *itr = '\0';
+
+               /* Now, skip any leading tabs and spaces. */
+               itr = buff;
+               while (itr && isblank(*itr))
+                       itr++;
+
+               /* If there's nothing left, we can ignore this line. */
+               if (!strlen(itr))
+                       continue;
+
+               /*
+                * If we skipped the last rule and this rule is a continuation
+                * of it (begins with %), then we should skip this rule too.
+                */
+               if (skipped && *itr == '%') {
+                       dbg("Warning: Skipped child of invalid rule,"
+                                       " line %d.\n", linenum);
+                       memset(buff, '\0', CGROUP_RULE_MAXLINE);
+                       continue;
+               }
+
+               /*
+                * If there is something left, it should be a rule.  Otherwise,
+                * there's an error in the configuration file.
+                */
+               skipped = false;
+               memset(user, '\0', LOGIN_NAME_MAX);
+               memset(controllers, '\0', CG_CONTROLLER_MAX);
+               memset(destination, '\0', FILENAME_MAX);
+               i = sscanf(itr, "%s%s%s", user, controllers, destination);
+               if (i != 3) {
+                       dbg("Failed to parse configuration file on"
+                                       " line %d.\n", linenum);
+                       goto parsefail;
+               }
+
+               /*
+                * Next, check the user/group.  If it's a % sign, then we
+                * are continuing another rule and UID/GID should not be
+                * reset.  If it's a @, we're dealing with a GID rule.  If
+                * it's a *, then we do not need to do a lookup because the
+                * rule always applies (it's a wildcard).  If we're using
+                * non-cache mode and we've found a matching rule, we only
+                * continue to parse if we're looking at a child rule.
+                */
+               if ((!cache) && matched && (strncmp(user, "%", 1) != 0)) {
+                       /* If we make it here, we finished (non-cache). */
+                       dbg("Parsing of configuration file complete.\n\n");
+                       ret = -1;
+                       goto cleanup;
+               }
+               if (strncmp(user, "@", 1) == 0) {
+                       /* New GID rule. */
+                       itr = &(user[1]);
+                       if ((grp = getgrnam(itr))) {
+                               uid = CGRULE_INVALID;
+                               gid = grp->gr_gid;
+                       } else {
+                               dbg("Warning: Entry for %s not"
+                                               "found.  Skipping rule on line"
+                                               " %d.\n", itr, linenum);
+                               memset(buff, '\0', CGROUP_RULE_MAXLINE);
+                               skipped = true;
+                               continue;
+                       }
+               } else if (strncmp(user, "*", 1) == 0) {
+                       /* Special wildcard rule. */
+                       uid = CGRULE_WILD;
+                       gid = CGRULE_WILD;
+               } else if (*itr != '%') {
+                       /* New UID rule. */
+                       if ((pwd = getpwnam(user))) {
+                               uid = pwd->pw_uid;
+                               gid = CGRULE_INVALID;
+                       } else {
+                               dbg("Warning: Entry for %s not"
+                                               "found.  Skipping rule on line"
+                                               " %d.\n", user, linenum);
+                               memset(buff, '\0', CGROUP_RULE_MAXLINE);
+                               skipped = true;
+                               continue;
+                       }
+               } /* Else, we're continuing another rule (UID/GID are okay). */
+
+               /*
+                * If we are not caching rules, then we need to check for a
+                * match before doing anything else.  We consider four cases:
+                * The UID matches, the GID matches, the UID is a member of the
+                * GID, or we're looking at the wildcard rule, which always
+                * matches.  If none of these are true, we simply continue to
+                * the next line in the file.
+                */
+               if (grp && muid != CGRULE_INVALID) {
+                       pwd = getpwuid(muid);
+                       for (i = 0; grp->gr_mem[i]; i++) {
+                               if (!(strcmp(pwd->pw_name, grp->gr_mem[i])))
+                                       matched = true;
+                       }
+               }
+
+               if (uid == muid || gid == mgid || uid == CGRULE_WILD) {
+                       matched = true;
+               }
+
+               if (!cache && !matched)
+                       continue;
+
+               /*
+                * Now, we're either caching rules or we found a match.  Either
+                * way, copy everything into a new rule and push it into the
+                * list.
+                */
+               newrule = calloc(1, sizeof(struct cgroup_rule));
+               if (!newrule) {
+                       dbg("Out of memory?  Error: %s\n", strerror(errno));
+                       ret = errno;
+                       goto cleanup;
+               }
+
+               newrule->uid = uid;
+               newrule->gid = gid;
+               strncpy(newrule->name, user, strlen(user));
+               strncpy(newrule->destination, destination, strlen(destination));
+               newrule->next = NULL;
+
+               /* Parse the controller list, and add that to newrule too. */
+               stok_buff = strtok(controllers, ",");
+               if (!stok_buff) {
+                       dbg("Failed to parse controllers on line"
+                                       " %d\n", linenum);
+                       goto destroyrule;
+               }
+
+               i = 0;
+               do {
+                       if (i >= MAX_MNT_ELEMENTS) {
+                               dbg("Too many controllers listed"
+                                       " on line %d\n", linenum);
+                               goto destroyrule;
+                       }
+
+                       newrule->controllers[i] = strndup(stok_buff,
+                                                       strlen(stok_buff) + 1);
+                       if (!(newrule->controllers[i])) {
+                               dbg("Out of memory?  Error was: %s\n",
+                                       strerror(errno));
+                               goto destroyrule;
+                       }
+                       i++;
+               } while ((stok_buff = strtok(NULL, ",")));
+
+               /* Now, push the rule. */
+               if (lst->head == NULL) {
+                       lst->head = newrule;
+                       lst->tail = newrule;
+               } else {
+                       lst->tail->next = newrule;
+                       lst->tail = newrule;
+               }
+
+                dbg("Added rule %s (UID: %d, GID: %d) -> %s for controllers:",
+                        lst->tail->name, lst->tail->uid, lst->tail->gid,
+                       lst->tail->destination);
+                for (i = 0; lst->tail->controllers[i]; i++) {
+                       dbg(" %s", lst->tail->controllers[i]);
+               }
+               dbg("\n");
+
+               /* Finally, clear the buffer. */
+               memset(buff, '\0', CGROUP_RULE_MAXLINE);
+               grp = NULL;
+               pwd = NULL;
+       }
+
+       /* If we make it here, there were no errors. */
+       dbg("Parsing of configuration file complete.\n\n");
+       ret = (matched && !cache) ? -1 : 0;
+       goto cleanup;
+
+destroyrule:
+       cgroup_free_rule(newrule);
+
+parsefail:
+       ret = ECGROUPPARSEFAIL;
+
+cleanup:
+       free(buff);
+
+close_unlock:
+       fclose(fp);
+       pthread_rwlock_unlock(&rl_lock);
+finish:
+       return ret;
+}
+
 /**
  * cgroup_init(), initializes the MOUNT_POINT.
  *
@@ -293,7 +658,7 @@ static char *cg_build_path_locked(char *name, char *path, char *type)
        return NULL;
 }
 
-char *cg_build_path(char *name, char *path, char *type)
+static char *cg_build_path(char *name, char *path, char *type)
 {
        pthread_rwlock_rdlock(&cg_mount_table_lock);
        path = cg_build_path_locked(name, path, type);
@@ -301,6 +666,7 @@ char *cg_build_path(char *name, char *path, char *type)
 
        return path;
 }
+
 /** cgroup_attach_task_pid is used to assign tasks to a cgroup.
  *  struct cgroup *cgroup: The cgroup to assign the thread to.
  *  pid_t tid: The thread to be assigned to the cgroup.
@@ -771,7 +1137,7 @@ free_parent:
 
 /**
  * @cgroup: cgroup data structure to be filled with parent values and then
- *          passed down for creation
+ *       passed down for creation
  * @ignore_ownership: Ignore doing a chown on the newly created cgroup
  */
 int cgroup_create_cgroup_from_parent(struct cgroup *cgroup,
@@ -1225,216 +1591,197 @@ static void cg_free_controller_array(char *controllers[])
        }
 }
 
-/** cg_parse_rules_config_file
- * parses the config file and determines the rule application based on
- * uid and gid.
+/**
+ * Finds the first rule in the cached list that matches the given UID or GID,
+ * and returns a pointer to that rule.  This function uses rl_lock.
  *
- *  returns 0 on success.
+ * This function may NOT be thread safe.
+ *     @param uid The UID to match
+ *     @param gid The GID to match
+ *     @return Pointer to the first matching rule, or NULL if no match
+ * TODO: Determine thread-safeness and fix if not safe.
  */
-static int cg_parse_rules_config_file(struct cgroup_rules_data *cgrldp,
-                                               struct cgroup *cgroups[])
+static struct cgroup_rule *cgroup_find_matching_rule_uid_gid(const uid_t uid,
+                               const gid_t gid)
 {
-       FILE *fp;
-       char buf[FILENAME_MAX];
-       char user[FILENAME_MAX];
-       char dest[PATH_MAX];
-       char buf_ctrl[FILENAME_MAX];
-       char *controllers[CG_CONTROLLER_MAX];
-       struct cgroup *cgroup;
-       int cgindex = 0, match_uid = 0, match_gid = 0, i, ret = 0;
-
-       memset(controllers, 0, CG_CONTROLLER_MAX);
-       memset(buf_ctrl, 0, FILENAME_MAX);
+       /* Return value */
+       struct cgroup_rule *ret = rl.head;
 
-       fp = fopen(CGRULES_CONF_FILE, "r");
-       if (fp == NULL) {
-               dbg("Open of file %s failed: %s", CGRULES_CONF_FILE,
-                       strerror(errno));
-               return ECGOTHER;
-       }
+       /* Temporary user data */
+       struct passwd *usr = NULL;
 
-       /* In case of multi line rule, we need to prepare multiple
-        * cgroups structure. That's why caller has passed an array
-        * of cgroup pointers. Keep a index of current empty cgroup
-        * structure which can be passed to cg_prepare_cgroup.
-        */
-       cgindex = 0;
-
-       /* Parse file */
-       while (fgets(buf, FILENAME_MAX, fp) != NULL) {
-               char *tptr, *line;
-               struct group *group;
-
-               line = buf;
-               /* skip the leading white space */
-               while (*line && isspace(*line))
-                       line++;
-
-               /* Rip off the comments */
-               tptr = strchr(line, '#');
-               if (tptr)
-                       *tptr = '\0';
-
-               /* Rip off the newline char */
-               tptr = strchr(line, '\n');
-               if (tptr)
-                       *tptr = '\0';
-
-               /* Anything left ? */
-               if (!strlen(line))
-                       continue;
+       /* Temporary group data */
+       struct group *grp = NULL;
 
-               user[0] = dest[0] = buf_ctrl[0] = '\0';
+       /* Temporary string pointer */
+       char *sp = NULL;
 
-               i = sscanf(line, "%s%s%s", user, buf_ctrl, dest);
-               dbg("scanned line[%d]: user[%s], controllers[%s],"
-                       " dest[%s]\n", i, user, buf_ctrl, dest);
+       /* Loop variable */
+       int i = 0;
 
-               /* If we encounter a rule which does not begin with %,
-                * and either match_uid or match_gid is set, that means
-                * we have processed one rule and if that rule was muti
-                * line then it has ended. Return back. Remember, we execute
-                * only first matching rule (either single line or multiline)
-                */
-               if ((match_uid || match_gid) && strcmp(user, "%")) {
-                       match_uid = 0;
-                       match_gid = 0;
-                       fclose(fp);
-                       return 0;
+       pthread_rwlock_wrlock(&rl_lock);
+       while (ret) {
+               /* The wildcard rule always matches. */
+               if ((ret->uid == CGRULE_WILD) && (ret->gid == CGRULE_WILD)) {
+                       goto finished;
                }
 
-               if (i == CGRULES_MAX_FIELDS_PER_LINE) {
-                       /* a complete line */
-                       if (((strcmp(cgrldp->pw->pw_name, user) == 0) ||
-                               (strcmp(user, "*") == 0)) ||
-                               (match_uid && !strcmp(user, "%"))) {
-                               match_uid = 1;
-
-                               cgroup = calloc(1, sizeof(struct cgroup));
-                               if (!cgroup) {
-                                       ret = ECGOTHER;
-                                       goto out;
-                               }
-                               cgroups[cgindex] = cgroup;
-                               cgindex++;
-
-                               ret = cg_prepare_controller_array(buf_ctrl,
-                                               controllers);
-                               if (ret)
-                                       goto out;
-                               ret = cg_prepare_cgroup(cgroup, cgrldp->pid,
-                                                       dest, controllers);
-                               if (ret)
-                                       goto out;
-
-                               cg_free_controller_array(controllers);
-                       } else if (user[0] == '@' ||
-                                       (match_gid && !strcmp(user, "%"))) {
-                               errno = 0;
-                               group = getgrgid(cgrldp->gid);
-                               if (!group) {
-                                       dbg("getgrgid() failed for gid %d\n",
-                                               cgrldp->pw->pw_gid);
-                                       ret = ECGOTHER;
-                                       goto out;
-                               }
-                               if ((strcmp(group->gr_name, user+1) == 0)
-                                       || (strcmp(user, "*") == 0) ||
-                                       (match_gid && !strcmp(user, "%"))) {
-                                       match_gid = 1;
-
-                                       cgroup = (struct cgroup *)
-                                               calloc(1, sizeof(struct cgroup));
-                                       if (!cgroup) {
-                                               ret = ECGOTHER;
-                                               goto out;
-                                       }
-                                       cgroups[cgindex] = cgroup;
-                                       cgindex++;
-                                       ret = cg_prepare_controller_array(buf_ctrl, controllers);
-                                       if (ret)
-                                               goto out;
-                                       ret = cg_prepare_cgroup(cgroup,
-                                                       cgrldp->pid,
-                                                       dest, controllers);
-                                       if (ret)
-                                               goto out;
-                                       cg_free_controller_array(controllers);
-                               }
+               /* This is the simple case of the UID matching. */
+               if (ret->uid == uid) {
+                       goto finished;
+               }
+
+               /* This is the simple case of the GID matching. */
+               if (ret->gid == gid) {
+                       goto finished;
+               }
+
+               /* If this is a group rule, the UID might be a member. */
+               if (ret->name[0] == '@') {
+                       /* Get the group data. */
+                       sp = &(ret->name[1]);
+                       grp = getgrnam(sp);
+                       if (!grp) {
+                               continue;
+                       }
+
+                       /* Get the data for UID. */
+                       usr = getpwuid(uid);
+                       if (!usr) {
+                               continue;
+                       }
+
+                       /* If UID is a member of group, we matched. */
+                       for (i = 0; grp->gr_mem[i]; i++) {
+                               if (!(strcmp(usr->pw_name, grp->gr_mem[i])))
+                                       goto finished;
                        }
-               } else {
-                       dbg("invalid line '%s' - skipped", line);
                }
+
+               /* If we haven't matched, try the next rule. */
+               ret = ret->next;
        }
-       /* If we are here, then none of the rule matched for the task */
-       dbg("No rules matched for task with pid %d\n", cgrldp->pid);
-       fclose(fp);
-       return 0;
-out:
-       fclose(fp);
-       cg_free_controller_array(controllers);
-       /* Free the cgroups allocated so far */
-       for (i = 0; (i < CG_CONTROLLER_MAX) && cgroups[i]; i++)
-               cgroup_free(&cgroups[i]);
+
+       /* If we get here, no rules matched. */
+       ret = NULL;
+
+finished:
+       pthread_rwlock_unlock(&rl_lock);
        return ret;
 }
 
-/** cgroup_change_cgroup_uid_gid changes the cgroup of a program based on
- * rules in the config file. Rules are search based on uid and gid
- * and the pid is placed into destination group (if permissions are
- * there).
+/**
+ * Changes the cgroup of a program based on the rules in the config file.  If a
+ * rule exists for the given UID or GID, then the given PID is placed into the
+ * correct group.  By default, this function parses the configuration file each
+ * time it is called.
+ * 
+ * The flags can alter the behavior of this function:
+ *     CGFLAG_USECACHE: Use cached rules instead of parsing the config file
  *
- *  returns 0 on success.
+ * This function may NOT be thread safe. 
+ *     @param uid The UID to match
+ *     @param gid The GID to match
+ *     @param pid The PID of the process to move
+ *     @param flags Bit flags to change the behavior, as defined above
+ *     @return 0 on success, > 0 on error
+ * TODO: Determine thread-safeness and fix of not safe.
  */
-int cgroup_change_cgroup_uid_gid(uid_t uid, gid_t gid, pid_t pid)
+int cgroup_change_cgroup_uid_gid_flags(const uid_t uid, const gid_t gid,
+                               const pid_t pid, const int flags)
 {
-       int ret = 0, i;
-       struct passwd *pw;
-       struct cgroup_rules_data cgrld, *cgrldp = &cgrld;
-       struct cgroup *cgroups[CG_HIER_MAX];
+       /* Temporary pointer to a rule */
+       struct cgroup_rule *tmp = NULL;
+
+       /* Return codes */
+       int ret = 0;
 
+       /* We need to check this before doing anything else! */
        if (!cgroup_initialized) {
                dbg("libcgroup is not initialized\n");
-               return ECGROUPNOTINITIALIZED;
+               ret = ECGROUPNOTINITIALIZED;
+               goto finished;
        }
-       memset(cgrldp, 0, sizeof(struct cgroup_rules_data));
-       memset(cgroups, 0, CG_HIER_MAX);
 
-       pw = getpwuid(uid);
-       if (!pw) {
-               dbg("Could not retrieve the credentials of user"
-                       "with uid %d\n", uid);
-               return ECGOTHER;
-       }
-       cgrldp->pw = pw;
-       cgrldp->pid = pid;
-       cgrldp->gid = gid;
+       /* 
+        * If the user did not ask for cached rules, we must parse the
+        * configuration to find a matching rule (if one exists).  Else, we'll
+        * find the first match in the cached list (rl).
+        */
+       if (!(flags & CGFLAG_USECACHE)) {
+               dbg("Not using cached rules for PID %d.\n", pid);
+               ret = cgroup_parse_rules(false, uid, gid);
+
+               /* The configuration file has an error!  We must exit now. */
+               if (ret != -1 && ret != 0) {
+                       dbg("Failed to parse the configuration rules.\n");
+                       goto finished;
+               }
 
-       /* Parse config file */
-       ret = cg_parse_rules_config_file(cgrldp, cgroups);
-       if (ret) {
-               dbg("Parsing of %s failed\n", CGRULES_CONF_FILE);
-               return ret;
+               /* We did not find a matching rule, so we're done. */
+               if (ret == 0) {
+                       dbg("No rule found to match PID: %d, UID: %d, "
+                               "GID: %d\n", pid, uid, gid);
+                       goto finished;
+               }
+
+               /* Otherwise, we did match a rule and it's in trl. */
+               tmp = trl.head;
+       } else {
+               /* Find the first matching rule in the cached list. */
+               tmp = cgroup_find_matching_rule_uid_gid(uid, gid);
+               if (!tmp) {
+                       dbg("No rule found to match PID: %d, UID: %d, "
+                               "GID: %d\n", pid, uid, gid);
+                       ret = 0;
+                       goto finished;
+               }
        }
+       dbg("Found matching rule %s for PID: %d, UID: %d, GID: %d\n",
+                       tmp->name, pid, uid, gid);
 
-       /* Add task to cgroups */
-       for (i = 0; (i < CG_HIER_MAX) && cgroups[i]; i++) {
-               ret = cgroup_attach_task_pid(cgroups[i], cgrldp->pid);
+       /* If we are here, then we found a matching rule, so execute it. */
+       do {
+               dbg("Executing rule %s for PID %d... ", tmp->name, pid);
+               ret = cgroup_change_cgroup_path(tmp->destination,
+                               pid, tmp->controllers);
                if (ret) {
-                       dbg("cgroup_attach_task_pid failed:%d\n", ret);
-                       goto out;
+                       dbg("FAILED! (Error Code: %d)\n", ret);
+                       goto finished;
                }
-       }
-out:
-       /* Free the cgroups */
-       for (i = 0; (i < CG_HIER_MAX) && cgroups[i]; i++)
-               cgroup_free(&cgroups[i]);
+               dbg("OK!\n");
+
+               /* Now, check for multi-line rules.  As long as the "next"
+                * rule starts with '%', it's actually part of the rule that
+                * we just executed.
+                */
+               tmp = tmp->next;
+       } while (tmp && (tmp->name[0] == '%'));
+
+finished:
        return ret;
 }
 
-/** cgroup_change_cgroup_path changes the cgroup of a program based on
- * the path provided by user. In this case user already knows in which
- * cgroup the task should go and no rules file have to be parsed.
+/**
+ * Provides backwards-compatibility with older versions of the API.  This
+ * function is deprecated, and cgroup_change_cgroup_uid_gid_flags() should be
+ * used instead.  In fact, this function simply calls the newer one with flags
+ * set to 0 (none).
+ *     @param uid The UID to match
+ *     @param gid The GID to match
+ *     @param pid The PID of the process to move
+ *     @return 0 on success, > 0 on error
+ * 
+ */
+int cgroup_change_cgroup_uid_gid(uid_t uid, gid_t gid, pid_t pid)
+{
+       return cgroup_change_cgroup_uid_gid_flags(uid, gid, pid, 0);
+}
+
+/**
+ * Changes the cgroup of a program based on the path provided.  In this case,
+ * the user must already know into which cgroup the task should be placed and
+ * no rules will be parsed.
  *
  *  returns 0 on success.
  */
@@ -1460,3 +1807,104 @@ int cgroup_change_cgroup_path(char *dest, pid_t pid, char *controllers[])
        }
        return 0;
 }
+
+/**
+ * Print the cached rules table.  This function should be called only after
+ * first calling cgroup_parse_config(), but it will work with an empty rule
+ * list.
+ *     @param fp The file stream to print to
+ */
+void cgroup_print_rules_config(FILE *fp)
+{
+       /* Iterator */
+       struct cgroup_rule *itr;
+
+       /* Loop variable */
+       int i = 0;
+
+       pthread_rwlock_rdlock(&rl_lock);
+
+       if (!(rl.head)) {
+               fprintf(fp, "The rules table is empty.\n\n");
+               pthread_rwlock_unlock(&rl_lock);
+               return;
+       }
+
+       itr = rl.head;
+       while (itr) {
+               fprintf(fp, "Rule: %s\n", itr->name);
+
+               if (itr->uid == CGRULE_WILD)
+                       fprintf(fp, "  UID: any\n");
+               else if (itr->uid == CGRULE_INVALID)
+                       fprintf(fp, "  UID: N/A\n");
+               else
+                       fprintf(fp, "  UID: %d\n", itr->uid);
+
+               if (itr->gid == CGRULE_WILD)
+                       fprintf(fp, "  GID: any\n");
+               else if (itr->gid == CGRULE_INVALID)
+                       fprintf(fp, "  GID: N/A\n");
+               else
+                       fprintf(fp, "  GID: %d\n", itr->gid);
+
+               fprintf(fp, "  DEST: %s\n", itr->destination);
+
+               fprintf(fp, "  CONTROLLERS:\n");
+               for (i = 0; i < MAX_MNT_ELEMENTS; i++) {
+                       if (itr->controllers[i]) {
+                               fprintf(fp, "    %s\n", itr->controllers[i]);
+                       }
+               }
+               fprintf(fp, "\n");
+               itr = itr->next;
+       }
+       pthread_rwlock_unlock(&rl_lock);
+}
+
+/**
+ * Reloads the rules list, using the given configuration file.  This function
+ * is probably NOT thread safe (calls cgroup_parse_rules()).
+ *     @return 0 on success, > 0 on failure
+ */
+int cgroup_reload_cached_rules()
+{
+       /* Return codes */
+       int ret = 0;
+
+       dbg("Reloading cached rules from %s.\n", CGRULES_CONF_FILE);
+       if ((ret = cgroup_parse_rules(true, CGRULE_INVALID, CGRULE_INVALID))) {
+               dbg("Error parsing configuration file \"%s\": %d.\n",
+                       CGRULES_CONF_FILE, ret);
+               ret = ECGROUPPARSEFAIL;
+               goto finished;
+       }
+               
+       #ifdef DEBUG
+               cgroup_print_rules_config(stdout);
+       #endif
+
+finished:
+       return ret;
+}
+
+/**
+ * Initializes the rules cache.
+ *     @return 0 on success, > 0 on error
+ */
+int cgroup_init_rules_cache()
+{
+       /* Return codes */
+       int ret = 0;
+
+       /* Attempt to read the configuration file and cache the rules. */
+       ret = cgroup_parse_rules(true, CGRULE_INVALID, CGRULE_INVALID);
+       if (ret) {
+               dbg("Could not initialize rule cache, error was: %d\n", ret);
+               cgroup_rules_loaded = false;
+       } else {
+               cgroup_rules_loaded = true;
+       }
+
+       return ret;
+}
diff --git a/cgrulesengd.c b/cgrulesengd.c
new file mode 100644 (file)
index 0000000..02d0cd5
--- /dev/null
@@ -0,0 +1,625 @@
+/*
+ * Copyright Red Hat Inc. 2008
+ *
+ * Author: Steve Olivieri <sjo@redhat.com>
+ * Author: Vivek Goyal <vgoyal@redhat.com>
+ *
+ * Some part of the programs have been derived from Dhaval Giani's posting
+ * for daemon to place the task in right container. Original copyright notice
+ * follows.
+ *
+ * Copyright IBM Corporation, 2007
+ * Author: Dhaval Giani <dhaval <at> linux.vnet.ibm.com>
+ * Derived from test_cn_proc.c by Matt Helsley
+ * Original copyright notice follows
+ *
+ * Copyright (C) Matt Helsley, IBM Corp. 2005
+ * Derived from fcctl.c by Guillaume Thouvenin
+ * Original copyright notice follows:
+ *
+ * Copyright (C) 2005 BULL SA.
+ * Written by Guillaume Thouvenin <guillaume.thouvenin <at> bull.net>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2.1 of the GNU Lesser General Public License
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it would be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * TODO Stop using netlink for communication (or at least rewrite that part).
+ */
+
+#include "libcgroup.h"
+#include "cgrulesengd.h"
+
+#include <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/socket.h>
+#include <sys/syslog.h>
+#include <string.h>
+#include <linux/netlink.h>
+#include <signal.h>
+#include <time.h>
+
+#include <sys/stat.h>
+#include <unistd.h>
+#include <linux/connector.h>
+#include <linux/cn_proc.h>
+
+/* Log file */
+FILE* logfile;
+
+/**
+ * Prints the usage information for this program and, optionally, an error
+ * message.  This function uses vfprintf.
+ *     @param fd The file stream to print to
+ *     @param msg The error message to print (printf style)
+ *     @param ... Any args to msg (printf style)
+ */
+void usage(FILE* fd, const char* msg, ...)
+{
+       /* List of args to msg */
+       va_list ap;
+
+       /* Put all args after msg into the list. */
+       va_start(ap, msg);
+
+       if (msg)
+               vfprintf(fd, msg, ap);
+       fprintf(fd, "\n");
+       fprintf(fd, "cgrulesengd -- a daemon for the cgroups rules engine\n");
+       fprintf(fd, "  usage : cgrulesengd [--nodaemon] [--nolog] [--log FILE]"
+                       "\n");
+       va_end(ap);
+}
+
+/**
+ * Prints a formatted message (like printf()) to a file stream, and flushes
+ * the file stream's buffer so that the message is immediately readable.
+ *     @param fd The file stream to write to
+ *     @param format The format for the message (printf style)
+ *     @param ... Any args to format (printf style)
+ */
+void flog(FILE* fd, const char* format, ...)
+{
+       /* List of args to format */
+       va_list ap;
+
+       /* Print the message to the given stream. */
+       va_start(ap, format);
+       vfprintf(fd, format, ap);
+       va_end(ap);
+
+       /* Flush the stream's buffer, so the data is readable immediately. */
+       fflush(fd);
+}
+
+/**
+ * Process an event from the kernel, and determine the correct UID/GID/PID to
+ * pass to libcgroup.  Then, libcgroup will decide the cgroup to move the PID
+ * to, if any.
+ *     @param ev The event to process
+ *     @param type The type of event to process (part of ev)
+ *     @return 0 on success, > 0 on failure
+ */
+int cgre_process_event(const struct proc_event *ev, const int type)
+{
+       /* Handle for the /proc/PID/status file */
+       FILE *f;
+
+       /* Path for /proc/PID/status file */
+       char path[FILENAME_MAX];
+
+       /* Temporary buffer */
+       char *buf = NULL;
+
+       /* UID data */
+       uid_t ruid, euid, suid, fsuid;
+
+       /* GID data */
+       gid_t rgid, egid, sgid, fsgid;
+
+       /* Return codes */
+       int ret = 0;
+
+       /*
+        * First, we need to open the /proc/PID/status file so that we can
+        * get the effective UID and GID for the process that we're working
+        * on.  This process is probably not us, so we can't just call
+        * geteuid() or getegid().
+        */
+       sprintf(path, "/proc/%d/status", ev->event_data.id.process_pid);
+       f = fopen(path, "r");
+       if (!f) {
+               flog(logfile, "Failed to open %s", path);
+               goto finished;
+       }
+
+       /* Now, we need to find either the eUID or the eGID of the process. */
+       buf = calloc(4096, sizeof(char));
+       if (!buf) {
+               flog(logfile, "Failed to process event, out of"
+                               "memory?  Error: %s\n",
+                               strerror(errno));
+               ret = errno;
+               fclose(f);
+               goto finished;
+       }
+       switch (type) {
+       case PROC_EVENT_UID:
+               /* Have the eUID, need to find the eGID. */
+               while (fgets(buf, 4096, f)) {
+                       if (!strncmp(buf, "Gid:", 4)) {
+                               sscanf((buf + 5), "%d%d%d%d", &rgid, &egid,
+                                       &sgid, &fsgid);
+                               break;
+                       }
+                       memset(buf, '\0', 4096);
+               }
+               break;
+       case PROC_EVENT_GID:
+               /* Have the eGID, need to find the eUID. */
+               while (fgets(buf, 4096, f)) {
+                       if (!strncmp(buf, "Uid:", 4)) {
+                               sscanf((buf + 5), "%d%d%d%d", &ruid, &euid,
+                                       &suid, &fsuid);
+                               break;
+                       }
+                       memset(buf, '\0', 4096);
+               }
+               break;
+       default:
+               flog(logfile, "For some reason, we're processing a non-UID/GID"
+                               " event.  Something is wrong!\n");
+               break;
+       }
+       free(buf);
+       fclose(f);
+
+       /*
+        * Now that we have the UID, the GID, and the PID, we can make a call
+        * to libcgroup to change the cgroup for this PID.
+        */
+       switch (type) {
+       case PROC_EVENT_UID:
+               flog(logfile, "Attempting to change cgroup for PID: %d, "
+                               "UID: %d, GID: %d... ",
+                               ev->event_data.id.process_pid,
+                               ev->event_data.id.e.euid, egid);
+               ret = cgroup_change_cgroup_uid_gid_flags(
+                                       ev->event_data.id.e.euid,
+                                       egid, ev->event_data.id.process_pid,
+                                       CGFLAG_USECACHE);
+               break;
+       case PROC_EVENT_GID:
+               flog(logfile, "Attempting to change cgroup for PID: %d, "
+                               "UID: %d, GID: %d... ",
+                               ev->event_data.id.process_pid, euid,
+                               ev->event_data.id.e.egid);
+               ret = cgroup_change_cgroup_uid_gid_flags(euid,
+                                       ev->event_data.id.e.egid,
+                                       ev->event_data.id.process_pid,
+                                       CGFLAG_USECACHE);
+               break;
+       default:
+               break;
+       }
+
+       if (ret) {
+               flog(logfile, "FAILED!\n  (Error Code: %d)\n", ret);
+       } else {
+               flog(logfile, "OK!\n");
+       }
+
+finished:
+       return ret;
+}
+
+/**
+ * Handle a netlink message.  In the event of PROC_EVENT_UID or PROC_EVENT_GID,
+ * we pass the event along to cgre_process_event for further processing.  All
+ * other events are ignored.
+ *     @param cn_hdr The netlink message
+ *     @return 0 on success, > 0 on error
+ */
+int cgre_handle_msg(struct cn_msg *cn_hdr)
+{
+       /* The event to consider */
+       struct proc_event *ev;
+
+       /* Return codes */
+       int ret = 0;
+
+       /* Get the event data.  We only care about two event types. */
+       ev = (struct proc_event*)cn_hdr->data;
+       switch (ev->what) {
+       case PROC_EVENT_UID:
+               flog(logfile, "UID Event:\n");
+               flog(logfile, "  PID = %d, tGID = %d, rUID = %d, eUID = %d\n",
+                               ev->event_data.id.process_pid,
+                               ev->event_data.id.process_tgid,
+                               ev->event_data.id.r.ruid,
+                               ev->event_data.id.e.euid);
+               ret = cgre_process_event(ev, PROC_EVENT_UID);
+               break;
+       case PROC_EVENT_GID:
+               flog(logfile, "GID Event:\n");
+               flog(logfile, "  PID = %d, tGID = %d, rGID = %d, eGID = %d\n",
+                               ev->event_data.id.process_pid,
+                               ev->event_data.id.process_tgid,
+                               ev->event_data.id.r.rgid,
+                               ev->event_data.id.e.egid);
+               ret = cgre_process_event(ev, PROC_EVENT_GID);
+               break;
+       default:
+               break;
+       }
+
+       return ret;
+}
+
+int cgre_create_netlink_socket_process_msg()
+{
+       int sk_nl;
+       int err;
+       struct sockaddr_nl my_nla, kern_nla, from_nla;
+       socklen_t from_nla_len;
+       char buff[BUFF_SIZE];
+       int rc = -1;
+       struct nlmsghdr *nl_hdr;
+       struct cn_msg *cn_hdr;
+       enum proc_cn_mcast_op *mcop_msg;
+       size_t recv_len = 0;
+
+       /*
+        * Create an endpoint for communication. Use the kernel user
+        * interface device (PF_NETLINK) which is a datagram oriented
+        * service (SOCK_DGRAM). The protocol used is the connector
+        * protocol (NETLINK_CONNECTOR)
+        */
+       sk_nl = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_CONNECTOR);
+       if (sk_nl == -1) {
+               printf("socket sk_nl error");
+               return rc;
+       }
+
+       my_nla.nl_family = AF_NETLINK;
+       my_nla.nl_groups = CN_IDX_PROC;
+       my_nla.nl_pid = getpid();
+       my_nla.nl_pad = 0;
+
+       kern_nla.nl_family = AF_NETLINK;
+       kern_nla.nl_groups = CN_IDX_PROC;
+       kern_nla.nl_pid = 1;
+       kern_nla.nl_pad = 0;
+
+       err = bind(sk_nl, (struct sockaddr *)&my_nla, sizeof(my_nla));
+       if (err == -1) {
+               printf("binding sk_nl error");
+               goto close_and_exit;
+       }
+
+       nl_hdr = (struct nlmsghdr *)buff;
+       cn_hdr = (struct cn_msg *)NLMSG_DATA(nl_hdr);
+       mcop_msg = (enum proc_cn_mcast_op*)&cn_hdr->data[0];
+       printf("sending proc connector: PROC_CN_MCAST_LISTEN... ");
+       memset(buff, 0, sizeof(buff));
+       *mcop_msg = PROC_CN_MCAST_LISTEN;
+
+       /* fill the netlink header */
+       nl_hdr->nlmsg_len = SEND_MESSAGE_LEN;
+       nl_hdr->nlmsg_type = NLMSG_DONE;
+       nl_hdr->nlmsg_flags = 0;
+       nl_hdr->nlmsg_seq = 0;
+       nl_hdr->nlmsg_pid = getpid();
+
+       /* fill the connector header */
+       cn_hdr->id.idx = CN_IDX_PROC;
+       cn_hdr->id.val = CN_VAL_PROC;
+       cn_hdr->seq = 0;
+       cn_hdr->ack = 0;
+       cn_hdr->len = sizeof(enum proc_cn_mcast_op);
+       printf("sending netlink message len=%d, cn_msg len=%d\n",
+               nl_hdr->nlmsg_len, sizeof(struct cn_msg));
+       if (send(sk_nl, nl_hdr, nl_hdr->nlmsg_len, 0) != nl_hdr->nlmsg_len) {
+               printf("failed to send proc connector mcast ctl op!\n");
+               goto close_and_exit;
+       }
+       printf("sent\n");
+
+       for(memset(buff, 0, sizeof(buff)), from_nla_len = sizeof(from_nla);
+       ; memset(buff, 0, sizeof(buff)), from_nla_len = sizeof(from_nla)) {
+               struct nlmsghdr *nlh = (struct nlmsghdr*)buff;
+               memcpy(&from_nla, &kern_nla, sizeof(from_nla));
+               recv_len = recvfrom(sk_nl, buff, BUFF_SIZE, 0,
+               (struct sockaddr*)&from_nla, &from_nla_len);
+               if (recv_len == ENOBUFS) {
+                       flog(logfile, "************************************"
+                                       "***********\n"
+                                       "!***ERROR: NETLINK BUFFER FULL, MSG "
+                                       "DROPPED***!\n"
+                                       "************************************"
+                                       "***********\n");
+                       continue;
+               }
+               if (recv_len < 1)
+                       continue;
+               while (NLMSG_OK(nlh, recv_len)) {
+                       cn_hdr = NLMSG_DATA(nlh);
+                       if (nlh->nlmsg_type == NLMSG_NOOP)
+                               continue;
+                       if ((nlh->nlmsg_type == NLMSG_ERROR) ||
+                                       (nlh->nlmsg_type == NLMSG_OVERRUN))
+                               break;
+                       if(cgre_handle_msg(cn_hdr) < 0) {
+                               goto close_and_exit;
+                       }
+                       if (nlh->nlmsg_type == NLMSG_DONE)
+                               break;
+                       nlh = NLMSG_NEXT(nlh, recv_len);
+               }
+       }
+
+close_and_exit:
+       close(sk_nl);
+       return rc;
+}
+
+/**
+ * Turns this program into a daemon.  In doing so, we fork() and kill the
+ * parent process.  Note too that stdout, stdin, and stderr are closed in
+ * daemon mode, and a file descriptor for a log file is opened.
+ *     @param logp Path of the log file
+ *     @param daemon False to turn off daemon mode (no fork, leave FDs open)
+ *     @param logs False to disable logging (no log FD, leave stdout open)
+ *     @return 0 on success, > 0 on error
+ */
+int cgre_start_daemon(const char* logp, const unsigned char daemon,
+                       const unsigned char logs)
+{
+       /* PID returned from the fork() */
+       pid_t pid;
+
+       /* Current system time */
+       time_t tm;
+
+       /* Fork and die. */
+       if (daemon) {
+               pid = fork();
+               if (pid < 0) {
+                       openlog("CGRE", LOG_CONS, LOG_DAEMON|LOG_WARNING);
+                       syslog(LOG_DAEMON|LOG_WARNING, "Failed to fork,"
+                                       " error: %s", strerror(errno));
+                       closelog();
+                       flog(stderr, "Failed to fork(), %s\n", strerror(errno));
+                       return 1;
+               } else if (pid > 0) {
+                       flog(stdout, "Starting in daemon mode.\n");
+                       exit(EXIT_SUCCESS);
+               }
+
+               /* Change the file mode mask. */
+               umask(0);
+       } else {
+               dbg("Not using daemon mode.\n");
+               pid = getpid();
+       }
+
+       if (logs) {
+               logfile = fopen(logp, "a");
+               if (!logfile) {
+                       flog(stderr, "Failed to open log file %s, error: %s."
+                                       "  Continuing anyway.\n", logp,
+                                       strerror(errno));
+                       logfile = stdout;
+               } else {
+                       flog(logfile, "CGroup Rules Engine Daemon\n");
+                       tm = time(0);
+                       flog(logfile, "Current time: %s", ctime(&tm));
+                       flog(stdout, "Opened log file: %s\n", logp);
+               }
+       } else {
+               logfile = stdout;
+               flog(stdout, "Proceeding with stdout as log output.\n");
+       }
+
+       if (!daemon) {
+               /* We can skip the rest, since we're not becoming a daemon. */
+               flog(logfile, "Proceeding with PID %d\n\n", getpid());
+               if (logfile != stdout)
+                       flog(stdout, "Proceeding with PID %d\n", getpid());
+               return 0;
+       } else {
+               /* Get a new SID for the child. */
+               if (setsid() < 0) {
+                       flog(logfile, "Failed to get a new SID, error: %s\n",
+                                       strerror(errno));
+                       return 2;
+               }
+
+               /* Change to the root directory. */
+               if (chdir("/") < 0) {
+                       flog(logfile, "Failed to chdir to /, error: %s\n",
+                                       strerror(errno));
+                       return 3;
+               }
+
+               /* Close standard file descriptors. */
+               close(STDIN_FILENO);
+               if (logfile != stdout)
+                       close(STDOUT_FILENO);
+               close(STDERR_FILENO);
+       }
+
+       /* If we make it this far, we're a real daemon! Or we chose not to.  */
+       flog(logfile, "Proceeding with PID %d\n\n", getpid());
+       return 0;
+}
+
+/**
+ * Catch the SIGUSR2 signal and reload the rules configuration.  This function
+ * makes use of the logfile and flog() to print the new rules.
+ *     @param signum The signal that we caught (always SIGUSR2)
+ */
+void cgre_flash_rules(int signum)
+{
+       /* Current time */
+       time_t tm = time(0);
+
+       flog(logfile, "\nReloading rules configuration.\n");
+       flog(logfile, "Current time: %s\n", ctime(&tm));
+
+       /* Ask libcgroup to reload the rules table. */
+       cgroup_reload_cached_rules();
+
+       /* Print the results of the new table to our log file. */
+       cgroup_print_rules_config(logfile);
+       flog(logfile, "\n");
+}
+
+/**
+ * Catch the SIGTERM and SIGINT signals so that we can exit gracefully.  Before
+ * exiting, this function makes use of the logfile and flog().
+ *     @param signum The signal that we caught (SIGTERM, SIGINT)
+ */
+void cgre_catch_term(int signum)
+{
+       /* Current time */
+       time_t tm = time(0);
+
+       flog(logfile, "\nStopped CGroup Rules Engine Daemon at %s",
+                       ctime(&tm));
+       flog(logfile, "========================================");
+       flog(logfile, "========================================\n\n");
+
+       /* Close the log file, if we opened one. */
+       if (logfile && logfile != stdout)
+               fclose(logfile);
+
+       exit(EXIT_SUCCESS);
+}
+
+int main(int argc, char *argv[])
+{
+       /* Patch to the log file */
+       char logp[FILENAME_MAX];
+
+       /* For catching signals */
+       struct sigaction sa;
+
+       /* Should we daemonize? */
+       unsigned char daemon = 1;
+
+       /* Should we log? */
+       unsigned char logs = 1;
+
+       /* Return codes */
+       int ret = 0;
+
+       /* Loop variable */
+       int i = 0;
+
+       /* Make sure the user is root. */
+       if (getuid() != 0) {
+               fprintf(stderr, "Error: Only root can start/stop the control"
+                               " group rules engine daemon\n");
+               ret = 1;
+               goto finished;
+       }
+
+       /* Set the default log file. */
+       memset(logp, '\0', FILENAME_MAX);
+       strncpy(logp, "/root/cgrulesengd.log",
+                       strlen("/root/cgrulesengd.log"));
+       logfile = NULL;
+
+       /* Parse user args. */
+       for (i = 1; i < argc; i++) {
+               if (strncmp(argv[i], "--log", strlen("--log")) == 0) {
+                       i++;
+                       memset(logp, '\0', FILENAME_MAX);
+                       strncpy(logp, argv[i], strlen(argv[i]));
+                       continue;
+               }
+               if (strncmp(argv[i], "--nodaemon", strlen("--nodaemon")) == 0) {
+                       daemon = 0;
+                       continue;
+               }
+               if (strncmp(argv[i], "--nolog", strlen("--nolog")) == 0) {
+                       logs = 0;
+                       continue;
+               }
+
+               /* If we get here, the user specified an invalid arg. */
+               usage(stderr, "Invalid argument: %s", argv[i]);
+               ret = 2;
+               goto finished;
+       }
+
+       flog(stdout, "Log file is: %s\n", logp);
+
+       /* Initialize libcgroup. */
+       if ((ret = cgroup_init()) != 0) {
+               fprintf(stderr, "Error: libcgroup initialization failed, %d\n",
+                               ret);
+               goto finished;
+       }
+
+       /* Ask libcgroup to load the configuration rules. */
+       if ((ret = cgroup_init_rules_cache()) != 0) {
+               fprintf(stderr, "Error: libcgroup failed to initialize rules"
+                               "cache, %d\n", ret);
+               goto finished;
+       }
+
+       /* Now, start the daemon. */
+       if ((ret = cgre_start_daemon(logp, daemon, logs)) < 0) {
+               fprintf(stderr, "Error: Failed to launch the daemon, %d\n",
+                       ret);
+               goto finished;
+       }
+
+       /*
+        * Set up the signal handler to reload the cached rules upon reception
+        * of a SIGUSR2 signal.
+        */
+       sa.sa_handler = &cgre_flash_rules;
+       sa.sa_flags = 0;
+       sa.sa_restorer = NULL;
+       sigemptyset(&sa.sa_mask);
+       if ((ret = sigaction(SIGUSR2, &sa, NULL))) {
+               flog(logfile, "Failed to set up signal handler for SIGUSR2."
+                               " Error: %s\n", strerror(errno));
+               goto finished;
+       }
+
+       /*
+        * Set up the signal handler to catch SIGINT and SIGTERM so that we
+        * can exit gracefully.
+        */
+       sa.sa_handler = &cgre_catch_term;
+       ret = sigaction(SIGINT, &sa, NULL);
+       ret |= sigaction(SIGTERM, &sa, NULL);
+       if (ret) {
+               flog(logfile, "Failed to set up the signal handler.  Error:"
+                               " %s\n", strerror(errno));
+               goto finished;
+       }
+
+       /* Print the configuration to the log file, or stdout. */
+       cgroup_print_rules_config(logfile);
+       flog(logfile, "Started the CGroup Rules Engine Daemon.\n");
+
+       /* We loop endlesly in this function, unless we encounter an error. */
+       ret =  cgre_create_netlink_socket_process_msg();
+
+finished:
+       if (logfile && logfile != stdout)
+               fclose(logfile);
+
+       return ret;
+}
diff --git a/cgrulesengd.h b/cgrulesengd.h
new file mode 100644 (file)
index 0000000..bdffd31
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+ * Copyright Red Hat Inc. 2008
+ *
+ * Author:      Steve Olivieri <sjo@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2.1 of the GNU Lesser General Public License
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it would be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+#ifndef _CGRULESENGD_H
+#define _CGRULESENGD_H
+
+#include <features.h>
+
+__BEGIN_DECLS
+
+#include "libcgroup.h"
+#include <linux/connector.h>
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#ifndef __USE_GNU
+#define __USE_GNU
+#endif
+
+/* A simple macro for printing messages only when DEBUG is defined. */
+#ifdef DEBUG
+  #define fdbg(a, b...) fprintf(a, b)
+#else
+  #define fdbg(a, b...) do {} while(0)
+#endif /* DEBUG */
+
+/* The following ten macros are all for the Netlink code. */
+#define SEND_MESSAGE_LEN (NLMSG_LENGTH(sizeof(struct cn_msg) + \
+       sizeof(enum proc_cn_mcast_op)))
+#define RECV_MESSAGE_LEN (NLMSG_LENGTH(sizeof(struct cn_msg) + \
+       sizeof(struct proc_event)))
+
+#define SEND_MESSAGE_SIZE (NLMSG_SPACE(SEND_MESSAGE_LEN))
+#define RECV_MESSAGE_SIZE (NLMSG_SPACE(RECV_MESSAGE_LEN))
+
+#define max(x,y) ((y)<(x)?(x):(y))
+#define min(x,y) ((y)>(x)?(x):(y))
+
+#define BUFF_SIZE (max(max(SEND_MESSAGE_SIZE, RECV_MESSAGE_SIZE), 1024))
+#define MIN_RECV_SIZE (min(SEND_MESSAGE_SIZE, RECV_MESSAGE_SIZE))
+
+#define PROC_CN_MCAST_LISTEN (1)
+#define PROC_CN_MCAST_IGNORE (2)
+
+/**
+ * Prints the usage information for this program and, optionally, an error
+ * message.  This function uses vfprintf.
+ *     @param fd The file stream to print to
+ *     @param msg The error message to print (printf style)
+ *     @param ... Any args to msg (printf style)
+ */
+void cgre_usage(FILE *fd, const char *msg, ...);
+
+/**
+ * Prints a formatted message (like printf()) to a file stream, and flushes
+ * the file stream's buffer so that the message is immediately readable.
+ *     @param fd The file stream to write to
+ *     @param format The format for the message (printf style)
+ *     @param ... Any args to format (printf style)
+ */
+void flog(FILE* fd, const char* msg, ...);
+
+/**
+ * Process an event from the kernel, and determine the correct UID/GID/PID to
+ * pass to libcgroup.  Then, libcgroup will decide the cgroup to move the PID
+ * to, if any.
+ *     @param ev The event to process
+ *     @param type The type of event to process (part of ev)
+ *     @return 0 on success, > 0 on failure
+ */
+int cgre_process_event(const struct proc_event *ev, const int type);
+
+/**
+ * Handle a netlink message.  In the event of PROC_EVENT_UID or PROC_EVENT_GID,
+ * we pass the event along to cgre_process_event for further processing.  All
+ * other events are ignored.
+ *     @param cn_hdr The netlink message
+ *     @return 0 on success, > 0 on error
+ */
+int cgre_handle_message(struct cn_msg *cn_hdr);
+
+/**
+ * Turns this program into a daemon.  In doing so, we fork() and kill the
+ * parent process.  Note too that stdout, stdin, and stderr are closed in
+ * daemon mode, and a file descriptor for a log file is opened.
+ *     @param logp Path of the log file
+ *     @param daemon False to turn off daemon mode (no fork, leave FDs open)
+ *     @param logs False to disable logging (no log FD, leave stdout open)
+ *     @return 0 on success, > 0 on error
+ */
+int cgre_start_daemon(const char *logp, const unsigned char daemon,
+                       const unsigned char logs);
+
+/**
+ * Catch the SIGUSR2 signal and reload the rules configuration.  This function
+ * makes use of the logfile and flog() to print the new rules.
+ *     @param signum The signal that we caught (always SIGUSR2)
+ */
+void cgre_flash_rules(int signum);
+
+/**
+ * Catch the SIGTERM and SIGINT signal so that we can exit gracefully.  Before
+ * exiting, this function makes use of the logfile and flog().
+ *     @param signum The signal that we caught (SIGTERM, SIGINT)
+ */
+void cgre_catch_term(int signum);
+
+__END_DECLS
+
+#endif /* _CGRULESENGD_H */
+
index f422cc5a6e9ccf9fdb463debea9db5854115f35a..c2f2ce3160bd5b0bae1d262ef0b52947d96e1928 100644 (file)
@@ -60,6 +60,24 @@ struct cgroup_rules_data {
        gid_t   gid;
 };
 
+/* A rule that maps UID/GID to a cgroup */
+struct cgroup_rule {
+       uid_t uid;
+       gid_t gid;
+       char name[LOGIN_NAME_MAX];
+       char destination[FILENAME_MAX];
+       char *controllers[MAX_MNT_ELEMENTS];
+       struct cgroup_rule *next;
+};
+
+/* Container for a list of rules */
+struct cgroup_rule_list {
+       struct cgroup_rule *head;
+       struct cgroup_rule *tail;
+       int len;
+};
+
+
 __END_DECLS
 
 #endif
index dc7e23f4647776c900414818e2360a9abb10d5fe..c9e9689c7eb51add2be345cf9c05f4a23a2ce6bc 100644 (file)
@@ -27,6 +27,8 @@ __BEGIN_DECLS
 #include <sys/stat.h>
 #include <sys/types.h>
 #include <unistd.h>
+#include <limits.h>
+#include <linux/cn_proc.h>
 
 #ifndef _GNU_SOURCE
 #define _GNU_SOURCE
@@ -94,6 +96,19 @@ struct list_of_names {
        struct list_of_names *next;
 };
 
+/* Maximum length of a line in the daemon config file */
+#define CGROUP_RULE_MAXLINE (FILENAME_MAX + LOGIN_NAME_MAX + \
+       CG_CONTROLLER_MAX + 3)
+
+/* Definitions for the uid and gid members of a cgroup_rules */
+#define CGRULE_INVALID (-1)
+#define CGRULE_WILD (-2)
+
+/* Flags for cgroup_change_cgroup_uid_gid() */
+enum cgflags {
+       CGFLAG_USECACHE = 0x01,
+};
+
 enum cg_msg_type {
        CG_MSG_LOAD_FILE,
        CG_MSG_UNLOAD_FILE,
@@ -124,6 +139,8 @@ enum cgroup_errors {
        ECGOTHER,
        ECGROUPNOTEQUAL,
        ECGCONTROLLERNOTEQUAL,
+       ECGROUPPARSEFAIL, /* Failed to parse rules configuration file. */
+       ECGROUPNORULES, /* Rules list does not exist. */
 };
 
 #define CG_MAX_MSG_SIZE                256
@@ -175,10 +192,70 @@ struct cgroup *cgroup_get_cgroup(struct cgroup *cgroup);
 int cgroup_create_cgroup_from_parent(struct cgroup *cgroup, int ignore_ownership);
 int cgroup_copy_cgroup(struct cgroup *dst, struct cgroup *src);
 
-/* Changes the cgroup of calling application based on rule file */
+/**
+ * Changes the cgroup of a program based on the rules in the config file.  If a
+ * rule exists for the given UID or GID, then the given PID is placed into the
+ * correct group.  By default, this function parses the configuration file each
+ * time it is called.
+ * 
+ * The flags can alter the behavior of this function:
+ *      CGFLAG_USECACHE: Use cached rules instead of parsing the config file
+ * 
+ * This function may NOT be thread safe.
+ *     @param uid The UID to match
+ *     @param gid The GID to match
+ *     @param pid The PID of the process to move
+ *     @param flags Bit flags to change the behavior, as defined above
+ *     @return 0 on success, > 0 on error
+ * TODO: Determine thread-safeness and fix if not safe.
+ */
+int cgroup_change_cgroup_uid_gid_flags(const uid_t uid, const gid_t gid,
+                               const pid_t pid, const int flags);
+
+/**
+ * Provides backwards-compatibility with older versions of the API.  This
+ * function is deprecated, and cgroup_change_cgroup_uid_gid_flags() should be
+ * used instead.  In fact, this function simply calls the newer one with flags
+ * set to 0 (none).
+ *     @param uid The UID to match
+ *     @param gid The GID to match
+ *     @param pid The PID of the process to move
+ *     @return 0 on success, > 0 on error
+ * 
+ */
 int cgroup_change_cgroup_uid_gid(uid_t uid, gid_t gid, pid_t pid);
+
+/**
+ * Changes the cgroup of a program based on the path provided.  In this case,
+ * the user must already know into which cgroup the task should be placed and
+ * no rules will be parsed.
+ *
+ *  returns 0 on success.
+ */
 int cgroup_change_cgroup_path(char *path, pid_t pid, char *controllers[]);
 
+/**
+ * Print the cached rules table.  This function should be called only after
+ * first calling cgroup_parse_config(), but it will work with an empty rule
+ * list.
+ *     @param fp The file stream to print to
+ */
+void cgroup_print_rules_config(FILE *fp);
+
+/**
+ * Reloads the rules list, using the given configuration file.  This function
+ * is probably NOT thread safe (calls cgroup_parse_rules_config()).
+ *     @return 0 on success, > 0 on failure
+ */
+int cgroup_reload_cached_rules(void);
+
+/**
+ * Initializes the rules cache.
+ *     @return 0 on success, > 0 on failure
+ */
+int cgroup_init_rules_cache(void);
+
+
 /* The wrappers for filling libcg structures */
 
 struct cgroup *cgroup_new_cgroup(const char *name);
diff --git a/samples/cgred.conf b/samples/cgred.conf
new file mode 100644 (file)
index 0000000..5363be5
--- /dev/null
@@ -0,0 +1,19 @@
+# /etc/cgred.d/cgred.conf - CGroup Rules Engine Daemon configuration file
+# 
+# The four options listed below (CONFIG_FILE, LOG_FILE, NODAEMON, NOLOG) are
+# the only valid ones.  Defining anything else in this file will cause the
+# CGroup Rules Engine program to fail.  So, don't do it.
+
+# The pathname to the configuration file for CGroup Rules Engine
+CONFIG_FILE="/etc/cgrules.conf"
+
+# The pathname to the log file for CGroup Rules Engine
+LOG_FILE="/root/cgrulesengd.log"
+
+# Uncomment the second line to run CGroup Rules Engine in non-daemon mode
+NODAEMON=""
+#NODAEMON="--nodaemon"
+
+# Uncomment the second line to disable logging for CGroup Rules Engine
+NOLOG=""
+#NOLOG="--nolog"
diff --git a/scripts/init.d/cgred b/scripts/init.d/cgred
new file mode 100644 (file)
index 0000000..b99e769
--- /dev/null
@@ -0,0 +1,129 @@
+#!/bin/bash
+#
+# Start/Stop the CGroups Rules Engine Daemon
+#
+# Copyright Red Hat Inc. 2008
+#
+# Authors:     Steve Olivieri <sjo@redhat.com>
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of version 2.1 of the GNU Lesser General Public License
+# as published by the Free Software Foundation.
+# 
+# This program is distributed in the hope that it would be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+#
+# cgred                GGroups Rules Engine Daemon
+# chkconfig:   2345 80 20
+# description: This is a daemon for automatically classifying processes \
+#              into cgroups based on UID/GID.
+#
+# processname: cgrulesengd
+# pidfile: /var/run/cgred.pid
+#
+### BEGIN INIT INFO
+# Provides:            cgrulesengd
+# Required-Start:      $local_fs $syslog $wlm
+# Required-Stop:       $local_fs $syslog
+# Should-Start:                
+# Should-Stop:         
+# Default-Start:       2 3 4 5
+# Default-Stop:                0 1 6
+# Short-Description:   start and stop the cgroups rules engine daemon
+# Description:         CGroup Rules Engine is a tool for automatically using \
+#                      cgroups to classify processes
+### END INIT INFO
+
+prefix=/usr
+exec_prefix=/usr
+bindir=/bin
+
+CGRED_BIN=${bindir}/cgrulesengd
+
+# Sanity checks
+[ -x $CGRED_BIN ] || exit 1
+[ -x /lib/libcgroup.so ] || exit 1
+
+# Source function library & LSB routines
+. /etc/rc.d/init.d/functions
+. /lib/lsb/init-functions
+
+# Read in configuration options.
+if [ -f "/etc/cgred.d/cgred.conf" ] ; then
+       . /etc/cgred.d/cgred.conf
+       OPTIONS="--config $CONFIG_FILE --log $LOG_FILE $NODAEMON $NOLOG"
+else
+       OPTIONS=""
+fi
+
+# For convenience
+processname=cgrulesengd
+servicename=cgred
+pidfile=/var/run/cgred.pid
+
+RETVAL=0
+
+start()
+{
+       echo $"Starting CGroup Rules Engine Daemon..."
+       if [ -f "/var/lock/subsys/$servicename" ] ; then
+               echo "$servicename is already running with PID `cat ${pidfile}`"
+               return 1
+       fi
+       daemon --check $servicename --pidfile $pidfile $processname $OPTIONS
+       RETVAL=$?
+       echo
+       [ $RETVAL -eq 0 ] && touch /var/lock/subsys/$servicename
+       echo "`pidof $processname`" > $pidfile
+}
+
+stop()
+{
+       echo -n $"Stopping CGroup Rules Engine Daemon..."
+       killproc -p $pidfile $processname -12
+       RETVAL=$?
+       echo
+       if [ $RETVAL -eq 0 ] ; then
+               rm -f /var/lock/subsys/$servicename
+               rm -f $pidfile
+       fi
+}
+
+# See how we are called
+case "$1" in
+       start)
+               start
+               ;;
+       stop)
+               stop
+               ;;
+       status)
+               status -p $pidfile $processname
+               RETVAL=$?
+               ;;
+       restart)
+               stop
+               start
+               ;;
+       condrestart)
+               if [ -f /var/lock/subsys/$servicename ] ; then
+                       stop
+                       start
+               fi
+               ;;
+       flash)
+               if [ -f /var/lock/subsys/$servicename ] ; then
+                       echo $"Reloading rules configuration..."
+                       kill -s 12 `cat ${pidfile}`
+                       RETVAL=$?
+                       echo
+               else
+                       echo $"$servicename is not running."
+               fi
+               ;;
+       *)
+               echo $"Usage: $0 {start|stop|status|restart|condrestart|flash}"
+               ;;
+esac
+
+exit $RETVAL
index e6680321d3845ecd63b44881b9a729703c649357..7a38b7a774906c9e7a0c5507e50e6aa8257292c4 100644 (file)
@@ -2,9 +2,11 @@ LDFLAGS = -L ..
 LIBS = -lcgroup -lpthread
 INC = -I ..
 CXXFLAGS = -g -O2 -Wall -DDEBUG $(INC)
+CFLAGS = -g -O2 -Wall -DDEBUG
 
 TARGET= libcgrouptest01 \
-       libcg_ba
+       libcg_ba \
+       setuid
 
 all:   $(TARGET)
 
@@ -14,5 +16,8 @@ libcgrouptest01: libcgrouptest01.c
 libcg_ba: libcg_ba.cpp
        $(CXX) $(CXXFLAGS) -o $@ $< $(LDFLAGS) $(LIBS)
 
+setuid: setuid.c
+       $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) $(LIBS)
+
 clean:
        \rm -f $(TARGET)
diff --git a/tests/setuid.c b/tests/setuid.c
new file mode 100644 (file)
index 0000000..9d77850
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * Copyright Red Hat Inc. 2008
+ *
+ * Author:      Steve Olivieri <sjo@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2.1 of the GNU Lesser General Public License
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it would be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <pwd.h>
+#include <grp.h>
+#include <errno.h>
+#include <string.h>
+
+/* 
+ * This is just a simple program for changing a UID or a GID.  Comment out
+ * whichever block you don't want to use.
+ */
+int main(int argc, char *argv[])
+{
+       /* User data */
+       struct passwd *pwd;
+
+       /* UID of user */
+       uid_t uid;
+
+       /* Group data */
+       struct group *grp;
+
+       /* GID of group */
+       gid_t gid;
+
+       /* Return codes */
+       int ret;
+
+       pwd = getpwnam(argv[1]);
+       uid = pwd->pw_uid;
+       fprintf(stdout, "Setting UID to %s (%d).\n", pwd->pw_name, uid);
+       if ((ret = setuid(uid))) {
+               fprintf(stderr, "Call to setuid() failed with error: %s\n",
+                               strerror(errno));
+               ret = -errno;
+               goto finished;
+       }
+
+//     while(1) {
+//             grp = getgrnam("root");
+//             gid = grp->gr_gid;
+//             fprintf(stdout, "Setting GID to %s (%d).\n",
+//                             grp->gr_name, gid);
+//             if ((ret = setgid(gid))) {
+//                     fprintf(stderr, "Call to setgid() failed with error:"
+//                                     " %s\n", strerror(errno));
+//                     ret = -errno;
+//                     goto finished;
+//             }
+//     }
+
+       while (1) {
+               usleep(3000000);
+       }
+
+finished:
+       return ret;
+}