]> git.ipfire.org Git - thirdparty/util-linux.git/commitdiff
libsmartcols: reject pathological regex patterns in filter
authorKarel Zak <kzak@redhat.com>
Tue, 23 Jun 2026 14:21:39 +0000 (16:21 +0200)
committerKarel Zak <kzak@redhat.com>
Wed, 24 Jun 2026 09:20:15 +0000 (11:20 +0200)
Consecutive ERE quantifiers (e.g. a++, a**) and nested group
repetitions (e.g. (a+)+) cause glibc regcomp() to allocate gigabytes
for the NFA, triggering OOM on small inputs.

Reject such patterns in filter_compile_param() before they reach
regcomp(), and limit regex pattern size to SCOLS_FILTER_MAX_REGSZ (256
bytes). Document all filter expression limits in scols-filter(5).

Addresses: https://github.com/util-linux/util-linux/pull/4436
Signed-off-by: Karel Zak <kzak@redhat.com>
libsmartcols/scols-filter.5.adoc
libsmartcols/src/filter-param.c
libsmartcols/src/fuzz.c
libsmartcols/src/smartcolsP.h

index 79921a307a4a849455c0f0c29d46949e91925fba..5822ac00a54384059bbae2f7f3001818e8ea3931 100644 (file)
@@ -111,6 +111,14 @@ long double. The `integer` may be followed by the multiplicative suffixes KiB,
 GiB, TiB, PiB, EiB, ZiB, and YiB (the "iB" is optional, e.g., "K" has the same
 meaning as "KiB").
 
+The filter expression string is limited to 1024 bytes. The number of nodes
+(parameters and operators) in a single expression is limited to 256.
+
+Regular expression patterns used with the `=~` and `!~` operators are limited to
+256 bytes. Patterns with consecutive quantifiers (e.g., `a++`, `a**`) or nested
+group repetitions (e.g., `(a+)+`) are rejected because they can cause excessive
+memory consumption in the POSIX regex compiler.
+
 == AUTHORS
 
 mailto:kzak@redhat.com[Karel Zak]
index b5fbc7ca78eeb7a6e2aca5d7000dc2fd19a9ab26..b7bff69687db0ffd26099ab45aeaf3700bcfdc5d 100644 (file)
@@ -131,6 +131,32 @@ struct filter_node *filter_new_param(
        return (struct filter_node *) n;
 }
 
+/* Consecutive ERE quantifiers (a++, a**) and nested group repetitions
+ * ((a+)+, (a*)*) cause glibc regcomp() to allocate gigabytes for the NFA. */
+static int is_unsafe_regex(const char *pattern)
+{
+       size_t i, len = strlen(pattern);
+
+       for (i = 0; i + 1 < len; i++) {
+               if ((pattern[i] == '+' || pattern[i] == '*')
+                   && (pattern[i + 1] == '+' || pattern[i + 1] == '*'))
+                       return 1;
+
+               if (pattern[i] == ')'
+                   && (pattern[i + 1] == '+' || pattern[i + 1] == '*')) {
+                       int j;
+
+                       for (j = (int) i - 1; j >= 0; j--) {
+                               if (pattern[j] == '(')
+                                       break;
+                               if (pattern[j] == '+' || pattern[j] == '*')
+                                       return 1;
+                       }
+               }
+       }
+       return 0;
+}
+
 int filter_compile_param(struct libscols_filter *fltr, struct filter_param *n)
 {
        int rc;
@@ -141,6 +167,10 @@ int filter_compile_param(struct libscols_filter *fltr, struct filter_param *n)
                return 0;
        if (n->type != SCOLS_DATA_STRING || !n->val.str)
                return -EINVAL;
+       if (strlen(n->val.str) > SCOLS_FILTER_MAX_REGSZ)
+               return -EINVAL;
+       if (is_unsafe_regex(n->val.str))
+               return -EINVAL;
 
        n->re = calloc(1, sizeof(regex_t));
        if (!n->re)
index e06724df6c6229fa071afe1c97021e916ab54760..0ecfa2efc5bc52523bc1b81adb06b282b8c91539 100644 (file)
@@ -8,65 +8,60 @@
 
 static void process_string(const char *str)
 {
-    /* Parse the input as a libsmartcols filter expression. This drives the
-       flex scanner, the bison grammar, parameter/holder construction and the
-       regcomp() done for the =~ operator. */
-    struct libscols_filter *fltr = scols_new_filter(str);
+       struct libscols_filter *fltr = scols_new_filter(str);
 
-    scols_unref_filter(fltr);
+       scols_unref_filter(fltr);
 }
 
 int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
 {
-    char *str;
+       char *str;
 
-    /* Larger than the library's 1024-byte limit to exercise the
-     * rejection path, small enough to avoid OOM in sanitizer builds */
-    if (size > 4096)
-        return 0;
+       if (size > 4096)
+               return 0;
 
-    str = malloc(size + 1);
-    if (!str)
-        return 0;
+       str = malloc(size + 1);
+       if (!str)
+               return 0;
 
-    memcpy(str, data, size);
-    str[size] = '\0';
+       memcpy(str, data, size);
+       str[size] = '\0';
 
-    process_string(str);
+       process_string(str);
 
-    free(str);
-    return 0;
+       free(str);
+       return 0;
 }
 
 #ifndef FUZZ_TARGET
 int main(int argc, char **argv)
 {
-    for (int i = 1; i < argc; i++) {
-        FILE *f = fopen(argv[i], "r");
-        char *buf;
-        long len;
-        size_t n;
+       for (int i = 1; i < argc; i++) {
+               FILE *f = fopen(argv[i], "r");
+               char *buf;
+               long len;
+               size_t n;
 
-        if (!f)
-            continue;
-        fseek(f, 0, SEEK_END);
-        len = ftell(f);
-        fseek(f, 0, SEEK_SET);
-        if (len < 0) {
-            fclose(f);
-            continue;
-        }
-        buf = malloc(len + 1);
-        if (!buf) {
-            fclose(f);
-            continue;
-        }
-        n = fread(buf, 1, len, f);
-        fclose(f);
-        buf[n] = '\0';
-        process_string(buf);
-        free(buf);
-    }
-    return 0;
+               if (!f)
+                       continue;
+               fseek(f, 0, SEEK_END);
+               len = ftell(f);
+               fseek(f, 0, SEEK_SET);
+               if (len < 0) {
+                       fclose(f);
+                       continue;
+               }
+               buf = malloc(len + 1);
+               if (!buf) {
+                       fclose(f);
+                       continue;
+               }
+               n = fread(buf, 1, len, f);
+               fclose(f);
+               buf[n] = '\0';
+               process_string(buf);
+               free(buf);
+       }
+       return 0;
 }
 #endif
index fe59877954fb09271813f95e4577eceff1bddab7..f7504830d6aca32387dd46a1ff80459ece35abec 100644 (file)
@@ -566,6 +566,7 @@ struct libscols_filter {
 
 #define SCOLS_FILTER_MAX_EXPRSZ        1024
 #define SCOLS_FILTER_MAX_NODES 256
+#define SCOLS_FILTER_MAX_REGSZ 256
 
 struct filter_node *__filter_new_node(enum filter_ntype type, size_t sz);
 void filter_ref_node(struct filter_node *n);