]> git.ipfire.org Git - thirdparty/util-linux.git/commitdiff
libsmartcols: harden filter parser against fuzzer issues
authorKarel Zak <kzak@redhat.com>
Tue, 23 Jun 2026 09:52:30 +0000 (11:52 +0200)
committerKarel Zak <kzak@redhat.com>
Wed, 24 Jun 2026 09:20:15 +0000 (11:20 +0200)
Reject non-param nodes in filter_compile_param() to prevent
type confusion when the parser passes an expression node as
a regex operand (heap-use-after-free / heap-buffer-overflow).

Limit filter expression size to SCOLS_FILTER_MAX_EXPRSZ to prevent
pathological inputs from causing unbounded memory growth in the parser
(OOM).

Addresses: https://github.com/util-linux/util-linux/pull/4430
Signed-off-by: Karel Zak <kzak@redhat.com>
libsmartcols/src/filter-expr.c
libsmartcols/src/filter-param.c
libsmartcols/src/filter-parser.y
libsmartcols/src/filter.c
libsmartcols/src/fuzz.c
libsmartcols/src/smartcolsP.h

index c6c8146165b1dd99899abbf97c2e4f1c6be0dc4c..233f661e232927511ce4d746898473edeed9da61 100644 (file)
@@ -14,16 +14,24 @@ struct filter_expr {
 };
 
 struct filter_node *filter_new_expr(
-                       struct libscols_filter *fltr __attribute__((__unused__)),
+                       struct libscols_filter *fltr,
                        enum filter_etype type,
                        struct filter_node *left,
                        struct filter_node *right)
 {
-       struct filter_expr *n = (struct filter_expr *) __filter_new_node(
-                                       F_NODE_EXPR, sizeof(struct filter_expr));
+       struct filter_expr *n;
 
+       if (fltr && fltr->parsing && fltr->parse_nodes >= SCOLS_FILTER_MAX_NODES) {
+               errno = ERANGE;
+               return NULL;
+       }
+
+       n = (struct filter_expr *) __filter_new_node(
+                                       F_NODE_EXPR, sizeof(struct filter_expr));
        if (!n)
                return NULL;
+       if (fltr && fltr->parsing)
+               fltr->parse_nodes++;
 
        n->type = type;
 
index 9cec0d39dafa44426b02dbbae05a6b2236771b27..b5fbc7ca78eeb7a6e2aca5d7000dc2fd19a9ab26 100644 (file)
@@ -96,11 +96,20 @@ struct filter_node *filter_new_param(
                enum filter_holder holder,
                void *data)
 {
-       struct filter_param *n = (struct filter_param *) __filter_new_node(
+       struct filter_param *n;
+
+       if (fltr && fltr->parsing && fltr->parse_nodes >= SCOLS_FILTER_MAX_NODES) {
+               errno = ERANGE;
+               return NULL;
+       }
+
+       n = (struct filter_param *) __filter_new_node(
                                        F_NODE_PARAM,
                                        sizeof(struct filter_param));
        if (!n)
                return NULL;
+       if (fltr && fltr->parsing)
+               fltr->parse_nodes++;
 
        n->type = type;
        n->holder = holder;
index 23b05df33a997101af4ef9406d8b06b6b02c59ab..e02de2ea35aa1966258c3e998acfa361d4377f72 100644 (file)
@@ -17,6 +17,8 @@
 
 void yyerror(yyscan_t *locp, struct libscols_filter *fltr, char const *fmt, ...);
 
+#define YYMAXDEPTH 256
+
 %}
 
 %define api.pure full
@@ -79,41 +81,133 @@ filter:
 expr:
        param                   { $$ = $1; }
        | '(' expr ')'          { $$ = $2; }
-       | expr T_AND expr       { $$ = filter_new_expr(fltr, F_EXPR_AND, $1, $3); }
-       | expr T_OR expr        { $$ = filter_new_expr(fltr, F_EXPR_OR, $1, $3); }
-       | T_NEG expr            { $$ = filter_new_expr(fltr, F_EXPR_NEG, NULL, $2); }
-       | expr T_EQ expr        { $$ = filter_new_expr(fltr, F_EXPR_EQ, $1, $3); }
-       | expr T_NE expr        { $$ = filter_new_expr(fltr, F_EXPR_NE, $1, $3); }
-       | expr T_LE expr        { $$ = filter_new_expr(fltr, F_EXPR_LE, $1, $3); }
-       | expr T_LT expr        { $$ = filter_new_expr(fltr, F_EXPR_LT, $1, $3); }
-       | expr T_GE expr        { $$ = filter_new_expr(fltr, F_EXPR_GE, $1, $3); }
-       | expr T_GT expr        { $$ = filter_new_expr(fltr, F_EXPR_GT, $1, $3); }
+       | expr T_AND expr       {
+               $$ = filter_new_expr(fltr, F_EXPR_AND, $1, $3);
+               if (!$$) {
+                       filter_unref_node($1);
+                       filter_unref_node($3);
+                       YYERROR;
+               }
+       }
+       | expr T_OR expr        {
+               $$ = filter_new_expr(fltr, F_EXPR_OR, $1, $3);
+               if (!$$) {
+                       filter_unref_node($1);
+                       filter_unref_node($3);
+                       YYERROR;
+               }
+       }
+       | T_NEG expr            {
+               $$ = filter_new_expr(fltr, F_EXPR_NEG, NULL, $2);
+               if (!$$) {
+                       filter_unref_node($2);
+                       YYERROR;
+               }
+       }
+       | expr T_EQ expr        {
+               $$ = filter_new_expr(fltr, F_EXPR_EQ, $1, $3);
+               if (!$$) {
+                       filter_unref_node($1);
+                       filter_unref_node($3);
+                       YYERROR;
+               }
+       }
+       | expr T_NE expr        {
+               $$ = filter_new_expr(fltr, F_EXPR_NE, $1, $3);
+               if (!$$) {
+                       filter_unref_node($1);
+                       filter_unref_node($3);
+                       YYERROR;
+               }
+       }
+       | expr T_LE expr        {
+               $$ = filter_new_expr(fltr, F_EXPR_LE, $1, $3);
+               if (!$$) {
+                       filter_unref_node($1);
+                       filter_unref_node($3);
+                       YYERROR;
+               }
+       }
+       | expr T_LT expr        {
+               $$ = filter_new_expr(fltr, F_EXPR_LT, $1, $3);
+               if (!$$) {
+                       filter_unref_node($1);
+                       filter_unref_node($3);
+                       YYERROR;
+               }
+       }
+       | expr T_GE expr        {
+               $$ = filter_new_expr(fltr, F_EXPR_GE, $1, $3);
+               if (!$$) {
+                       filter_unref_node($1);
+                       filter_unref_node($3);
+                       YYERROR;
+               }
+       }
+       | expr T_GT expr        {
+               $$ = filter_new_expr(fltr, F_EXPR_GT, $1, $3);
+               if (!$$) {
+                       filter_unref_node($1);
+                       filter_unref_node($3);
+                       YYERROR;
+               }
+       }
 
        | expr T_REG expr       {
-               if (filter_compile_param(fltr, (struct filter_param *) $3) != 0)
+               $$ = NULL;
+               if (filter_compile_param(fltr, (struct filter_param *) $3) == 0)
+                       $$ = filter_new_expr(fltr, F_EXPR_REG, $1, $3);
+               if (!$$) {
+                       filter_unref_node($1);
+                       filter_unref_node($3);
                        YYERROR;
-               $$ = filter_new_expr(fltr, F_EXPR_REG, $1, $3);
+               }
        }
 
        | expr T_NREG expr      {
-               if (filter_compile_param(fltr, (struct filter_param *) $3) != 0)
+               $$ = NULL;
+               if (filter_compile_param(fltr, (struct filter_param *) $3) == 0)
+                       $$ = filter_new_expr(fltr, F_EXPR_NREG, $1, $3);
+               if (!$$) {
+                       filter_unref_node($1);
+                       filter_unref_node($3);
                        YYERROR;
-               $$ = filter_new_expr(fltr, F_EXPR_NREG, $1, $3);
+               }
        }
 ;
 
 param:
-       T_NUMBER        { $$ = filter_new_param(fltr, SCOLS_DATA_U64, 0, (void *) (&$1)); }
-       | T_FLOAT       { $$ = filter_new_param(fltr, SCOLS_DATA_FLOAT, 0, (void *) (&$1)); }
-       | T_HOLDER      { $$ = filter_new_param(fltr, SCOLS_DATA_NONE, F_HOLDER_COLUMN, (void *) $1); }
-       | T_STRING      { $$ = filter_new_param(fltr, SCOLS_DATA_STRING, 0, (void *) $1); }
+       T_NUMBER        {
+               $$ = filter_new_param(fltr, SCOLS_DATA_U64, 0, (void *) (&$1));
+               if (!$$)
+                       YYERROR;
+       }
+       | T_FLOAT       {
+               $$ = filter_new_param(fltr, SCOLS_DATA_FLOAT, 0, (void *) (&$1));
+               if (!$$)
+                       YYERROR;
+       }
+       | T_HOLDER      {
+               $$ = filter_new_param(fltr, SCOLS_DATA_NONE, F_HOLDER_COLUMN, (void *) $1);
+               if (!$$)
+                       YYERROR;
+       }
+       | T_STRING      {
+               $$ = filter_new_param(fltr, SCOLS_DATA_STRING, 0, (void *) $1);
+               if (!$$)
+                       YYERROR;
+       }
        | T_TRUE        {
                bool x = true;
                $$ = filter_new_param(fltr, SCOLS_DATA_BOOLEAN, 0, (void *) &x);
+               if (!$$)
+                       YYERROR;
        }
        | T_FALSE       {
                bool x = false;
                $$ = filter_new_param(fltr, SCOLS_DATA_BOOLEAN, 0, (void *) &x);
+               if (!$$)
+                       YYERROR;
        }
        | T_INVALID_NUMBER {            /* YYerror token is unsupported in old Bisons */
                ignore_result( $$ );    /* suppress "unset value" warning */
index 125e0246f5b3c70b20b8ab5b305731ba80e7dd26..669824b97fa152b1a770ebb7afbdcdd3600374af 100644 (file)
@@ -193,6 +193,9 @@ int scols_filter_parse_string(struct libscols_filter *fltr, const char *str)
        if (!str || !*str)
                return 0;       /* empty filter is not error */
 
+       if (strlen(str) > SCOLS_FILTER_MAX_EXPRSZ)
+               return -ERANGE;
+
        fltr->src = fmemopen((void *) str, strlen(str), "r");
        if (!fltr->src)
                return -errno;
@@ -200,7 +203,11 @@ int scols_filter_parse_string(struct libscols_filter *fltr, const char *str)
        yylex_init_extra(fltr, &sc);
        yyset_in(fltr->src, sc);
 
+       fltr->parsing = 1;
        rc = yyparse(sc, fltr);
+       fltr->parsing = 0;
+       fltr->parse_nodes = 0;
+
        yylex_destroy(sc);
 
        fclose(fltr->src);
index cb50385e4ee570621d3e1a64ab339d1527cf5a4a..e06724df6c6229fa071afe1c97021e916ab54760 100644 (file)
@@ -18,7 +18,14 @@ static void process_string(const char *str)
 
 int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
 {
-    char *str = malloc(size + 1);
+    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;
+
+    str = malloc(size + 1);
     if (!str)
         return 0;
 
index ac6923693094580f7fcd5eff6d788dc89c5c8444..fe59877954fb09271813f95e4577eceff1bddab7 100644 (file)
@@ -559,8 +559,14 @@ struct libscols_filter {
 
        struct list_head params;
        struct list_head counters;
+
+       size_t parse_nodes;
+       unsigned int parsing : 1;
 };
 
+#define SCOLS_FILTER_MAX_EXPRSZ        1024
+#define SCOLS_FILTER_MAX_NODES 256
+
 struct filter_node *__filter_new_node(enum filter_ntype type, size_t sz);
 void filter_ref_node(struct filter_node *n);
 void filter_unref_node(struct filter_node *n);