From: Karel Zak Date: Tue, 23 Jun 2026 09:52:30 +0000 (+0200) Subject: libsmartcols: harden filter parser against fuzzer issues X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d7ed845b966985c37c86e94df5cc6f3cfc98f9d2;p=thirdparty%2Futil-linux.git libsmartcols: harden filter parser against fuzzer issues 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 --- diff --git a/libsmartcols/src/filter-expr.c b/libsmartcols/src/filter-expr.c index c6c814616..233f661e2 100644 --- a/libsmartcols/src/filter-expr.c +++ b/libsmartcols/src/filter-expr.c @@ -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; diff --git a/libsmartcols/src/filter-param.c b/libsmartcols/src/filter-param.c index 9cec0d39d..b5fbc7ca7 100644 --- a/libsmartcols/src/filter-param.c +++ b/libsmartcols/src/filter-param.c @@ -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; diff --git a/libsmartcols/src/filter-parser.y b/libsmartcols/src/filter-parser.y index 23b05df33..e02de2ea3 100644 --- a/libsmartcols/src/filter-parser.y +++ b/libsmartcols/src/filter-parser.y @@ -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 */ diff --git a/libsmartcols/src/filter.c b/libsmartcols/src/filter.c index 125e0246f..669824b97 100644 --- a/libsmartcols/src/filter.c +++ b/libsmartcols/src/filter.c @@ -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); diff --git a/libsmartcols/src/fuzz.c b/libsmartcols/src/fuzz.c index cb50385e4..e06724df6 100644 --- a/libsmartcols/src/fuzz.c +++ b/libsmartcols/src/fuzz.c @@ -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; diff --git a/libsmartcols/src/smartcolsP.h b/libsmartcols/src/smartcolsP.h index ac6923693..fe5987795 100644 --- a/libsmartcols/src/smartcolsP.h +++ b/libsmartcols/src/smartcolsP.h @@ -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);