Package: libcgroup3
Version: 3.1.0-2+b2
Severity: wishlist

Please add minimum and maximum UID selectors to cgrules.conf,
so could use lines like

  >=1000 cpu student/%u

to match UIDs 1000 and above, and

  <=999  *  *  ignore

to match UIDs up to 999.

The patch below seems to do exactly this.
This patch also includes my fix for #1113911 .

Sometime soon I will generate a patch for upstream V3.2 and report there.

Cheers, Paul

Paul Szabo       [email protected]       www.maths.usyd.edu.au/u/psz
School of Mathematics and Statistics   University of Sydney    Australia


-- System Information:
Debian Release: 13.0
  APT prefers stable-security
  APT policy: (500, 'stable-security'), (500, 'stable')
Architecture: i386 (x86_64)

Kernel: Linux 6.12+pk13.01 (SMP w/4 CPU threads; PREEMPT)
Locale: LANG=C.UTF-8, LC_CTYPE=C.UTF-8 (charmap=UTF-8), LANGUAGE not set
Shell: /bin/sh linked to /usr/bin/dash
Init: systemd (via /run/systemd/system)

Versions of packages libcgroup3 depends on:
ii  libc6        2.41-12
ii  libsystemd0  257.7-1

libcgroup3 recommends no packages.

libcgroup3 suggests no packages.

-- no debconf information
diff -r -u -p a-3.1.0-2/doc/man/cgrules.conf.5 b-3.1.0-2/doc/man/cgrules.conf.5
--- a-3.1.0-2/doc/man/cgrules.conf.5    2025-09-05 16:11:37.300790881 +1000
+++ b-3.1.0-2/doc/man/cgrules.conf.5    2025-09-06 13:30:35.077854928 +1000
@@ -18,10 +18,10 @@ Rules have two formats:
 
 .in +4n
 .nf
-<user>                 <controllers>           <destination>
+<user>                  <controllers>   <destination>   <options>
 .fi
 .nf
-<user>:<process name>  <controllers>           <destination>
+<user>:<process name>   <controllers>   <destination>   <options>
 .fi
 .in
 
@@ -32,10 +32,13 @@ can be:
 .nf
     - a user name
     - a group name with @group syntax
+    - a minimum or maximum UID with >=number or <=number syntax
     - the wildcard '*', for any user or group
     - '%', which is equivalent to "ditto" (useful for
       multi-line rules where different cgroups need to be
       specified for various hierarchies for a single user)
+      (Multi-line is probably useless for CGROUPv2, and is
+      probably mis-handled by libcgroup code: do not use.)
 .fi
 
 .I process name
@@ -43,6 +46,7 @@ is optional and it can be:
 .nf
     - a process name
     - a full command path of a process
+    - partial process or command path name with part* syntax
 .fi
 
 .I controllers
@@ -67,6 +71,16 @@ can be:
 
           '\\' can be used to escape '%'
 .fi
+
+.I options
+is optional and it can be:
+.nf
+    - "ignore" - if a process matches a rule with the "ignore" option,
+       then it will be ignored by cgrulesengd. cgrulesengd processing
+       will stop for this process, and all rules below this rule will
+       not affect it.
+.fi
+
 First rule which matches the criteria will be executed.
 
 Any text starting with '#' is considered as a start of comment line and is
@@ -123,13 +137,37 @@ at the end of the list. It will put a ta
 previous rules to default/ control group.
 
 .nf
-@students      cpu,cpuacct     students/%u
+@students       cpu,cpuacct     students/%u
 .fi
 Processes in cpu and cpuacct subsystems started by anybody from students group
-belong to group students/name. Where "name" is user name of owner of the
-process.
+belong to control group students/name, where "name" is user name of owner of
+the process.
+
+.nf
+root            *               *           ignore
+daemon          *               *           ignore
+www-data        *               *           ignore
+sshd            *               *           ignore
+   ...
+@faculty        cpu             faculty/%u
+*               cpu             student/%u
+.fi
+ "System" processes are un-touched, those started by faculty members
+belong to faculty/name, those of others e.g. students to student/name.
+Need to enumerate all system users.
 
+.nf
+<=999           *               *           ignore
+@faculty        cpu             faculty/%u
+*               cpu             student/%u
+.fi
+Probably same effect as previous example, but more succint.
 
+.nf
+@faculty        cpu             faculty/%u
+>=1000          cpu             student/%u
+.fi
+Same again.
 
 .SH FILES
 .LP
@@ -151,15 +189,4 @@ default libcgroup configuration files di
 cgconfig.conf (5), cgclassify (1), cgred.conf (5), cgrules.d (5)
 
 .SH BUGS
-
-
-
-
-
-
-
-
-
-
-
-
+Hopefully none... but look in code for comments to the contrary!
diff -r -u -p a-3.1.0-2/src/api.c b-3.1.0-2/src/api.c
--- a-3.1.0-2/src/api.c 2023-07-29 06:05:21.000000000 +1000
+++ b-3.1.0-2/src/api.c 2025-09-06 09:23:28.443710391 +1000
@@ -609,6 +609,8 @@ static int cgroup_parse_rules_file(char
        char options[CG_OPTIONS_MAX] = { '\0' };
        uid_t uid = CGRULE_INVALID;
        gid_t gid = CGRULE_INVALID;
+       uid_t minuid = CGRULE_INVALID;
+       uid_t maxuid = CGRULE_INVALID;
        bool has_options = false;
        size_t len_username;
        int len_procname;
@@ -705,6 +707,14 @@ static int cgroup_parse_rules_file(char
                strncpy(user, key, len_username);
                user[sizeof(user) - 1] = '\0';
 
+
+               /* 
+                * PSz BUG ALERT:
+                * The values we use for CGRULE_INVALID and CGRULE_WILD 
((uid_t) -1 and -2)
+                * are legit UID values, and may clash: a problem if there is a 
process
+                * with UID -1, or if we tried to set a rule for UID -2.
+                * On small systems where uid_t is 16 bits, the usual 
nobody=65534 is -2.
+                */
                /*
                 * Next, check the user/group.  If it's a % sign, then we are
                 * continuing another rule and UID/GID should not be reset.
@@ -727,8 +737,10 @@ static int cgroup_parse_rules_file(char
                        if (grp) {
                                uid = CGRULE_INVALID;
                                gid = grp->gr_gid;
+                               minuid = CGRULE_INVALID;
+                               maxuid = CGRULE_INVALID;
                        } else {
-                               cgroup_warn("Entry for %s not found. Skipping 
rule on line %d.\n",
+                               cgroup_warn("Entry for group %s not found. 
Skipping rule on line %d.\n",
                                            itr, linenum);
                                skipped = true;
                                continue;
@@ -737,12 +749,46 @@ static int cgroup_parse_rules_file(char
                        /* Special wildcard rule. */
                        uid = CGRULE_WILD;
                        gid = CGRULE_WILD;
+                       minuid = CGRULE_INVALID;
+                       maxuid = CGRULE_INVALID;
+               } else if (strncmp(user, ">=", 2) == 0) {
+                       /* New minUID rule. */
+                       itr = &(user[2]);
+                       i = atoi(itr);
+                       if (i > 0 && i < CGRULE_WILD) {
+                               uid = CGRULE_INVALID;
+                               gid = CGRULE_INVALID;
+                               minuid = (uid_t)i;
+                               maxuid = CGRULE_INVALID;
+                       } else {
+                               cgroup_warn("Invalid minUID in %s. Skipping 
rule on line %d.\n",
+                                           user, linenum);
+                               skipped = true;
+                               continue;
+                       }
+               } else if (strncmp(user, "<=", 2) == 0) {
+                       /* New maxUID rule. */
+                       itr = &(user[2]);
+                       i = atoi(itr);
+                       if (i > 0 && i < CGRULE_WILD) {
+                               uid = CGRULE_INVALID;
+                               gid = CGRULE_INVALID;
+                               minuid = CGRULE_INVALID;
+                               maxuid = (uid_t)i;
+                       } else {
+                               cgroup_warn("Invalid maxUID in %s. Skipping 
rule on line %d.\n",
+                                           user, linenum);
+                               skipped = true;
+                               continue;
+                       }
                } else if (*itr != '%') {
                        /* New UID rule. */
                        pwd = getpwnam(user);
                        if (pwd) {
                                uid = pwd->pw_uid;
                                gid = CGRULE_INVALID;
+                               minuid = CGRULE_INVALID;
+                               maxuid = CGRULE_INVALID;
                        } else {
                                cgroup_warn("Entry for %s not found. Skipping 
rule on line %d.\n",
                                            user, linenum);
@@ -772,7 +818,9 @@ static int cgroup_parse_rules_file(char
                        }
                }
 
-               if (uid == muid || gid == mgid || uid == CGRULE_WILD)
+               if (uid == muid || gid == mgid || uid == CGRULE_WILD ||
+                   (minuid != CGRULE_INVALID && muid >= minuid) ||
+                   (maxuid != CGRULE_INVALID && muid <= maxuid) )
                        matched = true;
 
                if (!cache) {
@@ -784,9 +832,18 @@ static int cgroup_parse_rules_file(char
                                 * If there is a rule based on process name,
                                 * it should be matched with mprocname.
                                 */
+                               /* 
+                                * PSz BUG ALERT:
+                                * procname match different from cgrulesengd:
+                                *  - does not accept missing (NULL) mprocname 
as match
+                                *  - does not handle wildcard in procname
+                                * (and wonder about RT status for ignore_rt in 
V3.2).
+                                */
                                if (!mprocname) {
                                        uid = CGRULE_INVALID;
                                        gid = CGRULE_INVALID;
+                                       minuid = CGRULE_INVALID;
+                                       maxuid = CGRULE_INVALID;
                                        matched = false;
                                        continue;
                                }
@@ -795,6 +852,8 @@ static int cgroup_parse_rules_file(char
                                if (strcmp(mprocname, procname) && 
strcmp(mproc_base, procname)) {
                                        uid = CGRULE_INVALID;
                                        gid = CGRULE_INVALID;
+                                       minuid = CGRULE_INVALID;
+                                       maxuid = CGRULE_INVALID;
                                        matched = false;
                                        free(mproc_base);
                                        continue;
@@ -818,6 +877,8 @@ static int cgroup_parse_rules_file(char
 
                newrule->uid = uid;
                newrule->gid = gid;
+               newrule->minuid = minuid;
+               newrule->maxuid = maxuid;
                newrule->is_ignore = false;
 
                len_username = min(len_username, sizeof(newrule->username) - 1);
@@ -3982,148 +4043,6 @@ static int cgroup_find_matching_controll
 }
 
 /**
- * Evaluates if rule is an ignore rule and the pid/procname match this rule.
- * If rule is an ignore rule and the pid/procname match this rule, then this
- * function returns true.  Otherwise it returns false.
- *
- *     @param rule The rule being evaluated
- *     @param pid PID of the process being compared
- *     @param procname Process name of the process being compared
- *     @return True if the rule is an ignore rule and this pid/procname
- *             match the rule.  False otherwise
- */
-STATIC bool cgroup_compare_ignore_rule(const struct cgroup_rule * const rule, 
pid_t pid,
-                                      const char * const procname)
-{
-       char *controller_list[MAX_MNT_ELEMENTS] = { '\0' };
-       char *cgroup_list[MAX_MNT_ELEMENTS] = { '\0' };
-       int rule_matching_controller_idx;
-       int cgroup_list_matching_idx;
-       bool found_match = false;
-       char *token, *saveptr;
-       int ret, i;
-
-       if (!rule->is_ignore)
-               /* Immediately return if the 'ignore' option is not set */
-               return false;
-
-       ret = cg_get_cgroups_from_proc_cgroups(pid, cgroup_list, 
controller_list,
-                                              MAX_MNT_ELEMENTS);
-       if (ret < 0)
-               goto out;
-
-       ret = cgroup_find_matching_destination(cgroup_list, rule->destination,
-                                              &cgroup_list_matching_idx);
-       if (ret < 0)
-               /* No cgroups matched */
-               goto out;
-
-       token = strtok_r(controller_list[cgroup_list_matching_idx], ",", 
&saveptr);
-       while (token != NULL) {
-
-               ret = cgroup_find_matching_controller(rule->controllers, token,
-                                                     
&rule_matching_controller_idx);
-               if (ret == 0)
-                       /* We found a matching controller */
-                       break;
-
-               token = strtok_r(NULL, ",", &saveptr);
-       }
-
-       if (!rule->procname) {
-               /*
-                * The rule procname is empty, thus it's a wildcard and
-                * all processes match.
-                */
-               found_match = true;
-               goto out;
-       }
-
-       if (!strcmp(rule->procname, procname)) {
-               found_match = true;
-               goto out;
-       }
-
-       if (cgroup_compare_wildcard_procname(rule->procname, procname))
-               found_match = true;
-
-out:
-       for (i = 0; i < MAX_MNT_ELEMENTS; i++) {
-               if (controller_list[i])
-                       free(controller_list[i]);
-               if (cgroup_list[i])
-                       free(cgroup_list[i]);
-       }
-
-       return found_match;
-}
-
-static struct cgroup_rule *cgroup_find_matching_rule_uid_gid(uid_t uid, gid_t 
gid,
-                                                            struct cgroup_rule 
*rule)
-{
-       /* Temporary user data */
-       struct passwd *usr = NULL;
-
-       /* Temporary group data */
-       struct group *grp = NULL;
-
-       /* Temporary string pointer */
-       char *sp = NULL;
-
-       /* Loop variable */
-       int i = 0;
-
-       while (rule) {
-               /* Skip "%" which indicates continuation of previous rule. */
-               if (rule->username[0] == '%') {
-                       rule = rule->next;
-                       continue;
-               }
-               /* The wildcard rule always matches. */
-               if ((rule->uid == CGRULE_WILD) && (rule->gid == CGRULE_WILD))
-                       return rule;
-
-               /* This is the simple case of the UID matching. */
-               if (rule->uid == uid)
-                       return rule;
-
-               /* This is the simple case of the GID matching. */
-               if (rule->gid == gid)
-                       return rule;
-
-               /* If this is a group rule, the UID might be a member. */
-               if (rule->username[0] == '@') {
-                       /* Get the group data. */
-                       sp = &(rule->username[1]);
-                       grp = getgrnam(sp);
-                       if (!grp) {
-                               rule = rule->next;
-                               continue;
-                       }
-
-                       /* Get the data for UID. */
-                       usr = getpwuid(uid);
-                       if (!usr) {
-                               rule = rule->next;
-                               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])))
-                                       return rule;
-                       }
-               }
-
-               /* If we haven't matched, try the next rule. */
-               rule = rule->next;
-       }
-
-       /* If we get here, no rules matched. */
-       return NULL;
-}
-
-/**
  * Finds the first rule in the cached list that matches the given UID, GID
  * or PROCESS NAME, and returns a pointer to that rule.
  * This function uses rl_lock.
@@ -4138,53 +4057,137 @@ static struct cgroup_rule *cgroup_find_m
 static struct cgroup_rule *cgroup_find_matching_rule(uid_t uid, gid_t gid, 
pid_t pid,
                                                     const char *procname)
 {
-       /* Return value */
+       /* Return value (rule) */
        struct cgroup_rule *ret = rl.head;
+
+       /* Temporary data */
+       struct passwd *usr = NULL;
+       struct group *grp = NULL;
+       char *sp = NULL;
+       int i = 0;
        char *base = NULL;
 
        pthread_rwlock_wrlock(&rl_lock);
        while (ret) {
-               ret = cgroup_find_matching_rule_uid_gid(uid, gid, ret);
-               if (!ret)
-                       break;
-               if (cgroup_compare_ignore_rule(ret, pid, procname))
+               /* PSz Do UID/GID matching inline, no need for separate routine 
*/
+
+               /* Skip "%" which indicates continuation of previous rule. */
+               /*
+                * PSz BUG ALERT: simply skip % lines: not evaluate and
+                * "add" to previous, not handle as multi-line rule.
+                * Was like that, is like that also in upstream V3.2.
+                * I guess that the idea of % with separate controllers
+                * or destinations is invalid with CGROUPv2, anyway.
+                */
+               if (ret->username[0] == '%') {
+                       goto trynext;
+               }
+
+               /* UID/GID matching - in a bogus while loop so can break */
+               while(1) {
+                       /* The wildcard rule always matches. */
+                       if ((ret->uid == CGRULE_WILD) && (ret->gid == 
CGRULE_WILD))
+                               break;
+       
+                       /* This is the simple case of the UID matching. */
+                       if (ret->uid == uid)
+                               break;
+       
+                       /* This is the simple case of the GID matching. */
+                       if (ret->gid == gid)
+                               break;
+       
+                       /* Matching minUID or maxUID */
+                       if (ret->minuid != CGRULE_INVALID && uid >= ret->minuid)
+                               break;
+                       if (ret->maxuid != CGRULE_INVALID && uid <= ret->maxuid)
+                               break;
+
+                       /* If this is a group rule, the UID might be a member. 
*/
                        /*
-                        * This pid matched a rule that instructs the
-                        * cgrules daemon to ignore this process.
+                        * PSz DOCUMENTATION BUG ALERT - Should document that 
"default"
+                        * membership is checked, not the result of any 
setgroups()
                         */
+                       if (ret->username[0] == '@') {
+                               /* Get the group data. */
+                               sp = &(ret->username[1]);
+                               grp = getgrnam(sp);
+                               if (!grp)
+                                       goto trynext;
+       
+                               /* Get the data for UID. */
+                               usr = getpwuid(uid);
+                               if (!usr)
+                                       goto trynext;
+       
+                               /* 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 grpmatched;
+                               }
+                       }
+                       /* Did not match */
+                       goto trynext;
+       grpmatched:
                        break;
-               if (ret->is_ignore) {
-                       /*
-                        * The rule currently being examined is an ignore
-                        * rule, but it didn't match this pid. Move on to
-                        * the next rule
-                        */
-                       ret = ret->next;
-                       continue;
                }
-               if (!procname)
-                       /* If procname is NULL, return a rule matching UID or 
GID. */
-                       break;
-               if (!ret->procname)
-                       /* If no process name in a rule, that means wildcard */
-                       break;
-               if (!strcmp(ret->procname, procname))
-                       break;
 
-               base = cgroup_basename(procname);
-               if (!strcmp(ret->procname, base))
-                       /* Check a rule of basename. */
-                       break;
-               if (cgroup_compare_wildcard_procname(ret->procname, procname))
-                       break;
+               /* procname matching - in a bogus while loop so can break */
+               while(1) {
+                       /* If procname is NULL, we take it as matched */
+                       if (!procname)
+                               break;
+                       /* If no process name in a rule then any will match */
+                       if (!ret->procname)
+                               break;
+                       /* Simple match */
+                       if (!strcmp(ret->procname, procname))
+                               break;
+                       /* Wildcard match */
+                       if (cgroup_compare_wildcard_procname(ret->procname, 
procname))
+                               break;
+       
+                       /* Check basename (as rules are most commonly written) 
*/
+                       /* Try both "simple" and wildcard matches */
+                       base = cgroup_basename(procname);
+                       if (!strcmp(ret->procname, base))
+                               break;
+                       if (cgroup_compare_wildcard_procname(ret->procname, 
base))
+                               break;
+
+                       /* Did not match */
+                       free(base);
+                       base = NULL;
+                       goto trynext;
+               }
+               if (base) {
+                       free(base);
+                       base = NULL;
+               }
+
+               /*
+                * PSz Upstream V3.2 knows about ignore and ignore_rt options,
+                * and should check whether the process matches. Documentation
+                * is unclear about what to do if both ignore and ignore_rt are
+                * specified, or if the RT status of process does not match.
+                * What upstream V3.2 should do:
+                * if ((rule->is_ignore & CGRULE_OPT_IGNORE))
+                *      break;
+                * if ((rule->is_ignore & CGRULE_OPT_IGNORE_RT) && 
cgroup_is_rt_task(pid) == true)
+                *      break;
+                *
+                * V3.1 knows about a single ignore option, to match all 
processes:
+                * nothing to check here.
+                * Done all checks - we matched, found the right rule.
+                */
+               break;
+
+
+trynext:
                ret = ret->next;
-               free(base);
-               base = NULL;
        }
        pthread_rwlock_unlock(&rl_lock);
 
-       if (base)
-               free(base);
 
        return ret;
 }
@@ -4682,6 +4685,8 @@ finished:
  * This function may be called after creating new control groups to move
  * running PIDs into the newly created control groups.
  *     @return 0 on success, < 0 on error
+ * PSz: Do not use if caller would not have CGFLAG_USECACHE.
+ * PSz: Used from cgrulesengd.c only, so it is OK to use CGFLAG_USECACHE below.
  */
 int cgroup_change_all_cgroups(void)
 {
@@ -4765,6 +4770,16 @@ void cgroup_print_rules_config(FILE *fp)
                else
                        fprintf(fp, "  GID: %d\n", itr->gid);
 
+               if (itr->minuid == CGRULE_INVALID)
+                       fprintf(fp, "  minUID: N/A\n");
+               else
+                       fprintf(fp, "  minUID: %d\n", itr->minuid);
+
+               if (itr->maxuid == CGRULE_INVALID)
+                       fprintf(fp, "  maxUID: N/A\n");
+               else
+                       fprintf(fp, "  maxUID: %d\n", itr->maxuid);
+
                fprintf(fp, "  DEST: %s\n", itr->destination);
 
                fprintf(fp, "  CONTROLLERS:\n");
diff -r -u -p a-3.1.0-2/src/libcgroup-internal.h 
b-3.1.0-2/src/libcgroup-internal.h
--- a-3.1.0-2/src/libcgroup-internal.h  2023-07-29 06:05:21.000000000 +1000
+++ b-3.1.0-2/src/libcgroup-internal.h  2025-09-06 07:32:27.101136272 +1000
@@ -155,6 +155,8 @@ struct cgroup_rules_data {
 struct cgroup_rule {
        uid_t uid;
        gid_t gid;
+       uid_t minuid;
+       uid_t maxuid;
        bool is_ignore;
        char *procname;
        char username[LOGIN_NAME_MAX];
@@ -404,8 +406,6 @@ void cgroup_free_controller(struct cgrou
 int cgroup_parse_rules_options(char *options, struct cgroup_rule * const rule);
 int cg_get_cgroups_from_proc_cgroups(pid_t pid, char *cgroup_list[], char 
*controller_list[],
                                     int list_len);
-bool cgroup_compare_ignore_rule(const struct cgroup_rule * const rule, pid_t 
pid,
-                               const char * const procname);
 bool cgroup_compare_wildcard_procname(const char * const rule_procname,
                                      const char * const procname);
 int cgroup_process_v1_mnt(char *controllers[], struct mntent *ent, int 
*mnt_tbl_idx);

Reply via email to