From: Bruno Haible Date: Sun, 1 Oct 2023 18:18:12 +0000 (+0200) Subject: Avoid crash by stack overflow during plural expression evaluation. X-Git-Tag: v0.23~324 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ef37a15408b285976978fa0d1e105ef97e23fa59;p=thirdparty%2Fgettext.git Avoid crash by stack overflow during plural expression evaluation. * gettext-runtime/intl/plural-exp.h (enum eval_status, struct eval_result): New types. (plural_eval): Change return type to 'struct eval_result'. * gettext-runtime/intl/eval-plural.h (EVAL_MAXDEPTH, OK): New macros. (plural_eval_recurse): New function, extracted from plural_eval. (plural_eval): Change return type to 'struct eval_result'. Invoke plural_eval_recurse. * gettext-runtime/intl/dcigettext.c (plural_lookup): Update. * gettext-tools/src/plural-eval.h: Update comment. * gettext-tools/src/msgl-check.c (plural_expression_histogram): Update. (check_plural_eval): Update. Fail with an error message if the plural expression evaluation produced a stack overflow. * gettext-tools/tests/plural-3: Update. --- diff --git a/gettext-runtime/intl/dcigettext.c b/gettext-runtime/intl/dcigettext.c index f1cb79db7..7bdc7a760 100644 --- a/gettext-runtime/intl/dcigettext.c +++ b/gettext-runtime/intl/dcigettext.c @@ -1468,14 +1468,19 @@ plural_lookup (struct loaded_l10nfile *domain, unsigned long int n, const char *translation, size_t translation_len) { struct loaded_domain *domaindata = (struct loaded_domain *) domain->data; + struct eval_result result; unsigned long int index; const char *p; - index = plural_eval (domaindata->plural, n); - if (index >= domaindata->nplurals) - /* This should never happen. It means the plural expression and the - given maximum value do not match. */ + result = plural_eval (domaindata->plural, n); + if (result.status != PE_OK) + /* The plural expression evaluation failed. */ index = 0; + else if (result.value >= domaindata->nplurals) + /* The plural expression and the given maximum value do not match. */ + index = 0; + else + index = result.value; /* Skip INDEX strings at TRANSLATION. */ p = translation; diff --git a/gettext-runtime/intl/eval-plural.h b/gettext-runtime/intl/eval-plural.h index 38413c4d4..90eb34050 100644 --- a/gettext-runtime/intl/eval-plural.h +++ b/gettext-runtime/intl/eval-plural.h @@ -18,20 +18,53 @@ #define STATIC static #endif -/* Evaluate the plural expression and return an index value. */ -STATIC -unsigned long int -plural_eval (const struct expression *pexp, unsigned long int n) +/* While the bison parser is able to support expressions of a maximum depth + of YYMAXDEPTH = 10000, the runtime evaluation of a parsed plural expression + has a smaller maximum recursion depth. + If we did not limit the recursion depth, a program that just invokes + ngettext() on a thread other than the main thread could get a crash by + stack overflow in the following circumstances: + - On systems with glibc, after the stack size has been reduced, + e.g. on x86_64 systems after "ulimit -s 260". + This stack size is only sufficient for ca. 3919 recursions. + Cf. + - On systems with musl libc, because there the thread stack size (for a + thread other than the main thread) by default is only 128 KB, see + . + - On AIX 7 systems, because there the thread stack size (for a thread + other than the main thread) by default is only 96 KB, see + . + This stack size is only sufficient for between 887 and 1363 recursions, + depending on the compiler and compiler optimization options. + A maximum depth of 100 is a large enough for all practical needs + and also small enough to avoid stack overflow even with small thread stack + sizes. */ +#ifndef EVAL_MAXDEPTH +# define EVAL_MAXDEPTH 100 +#endif + +/* A shorthand that denotes a successful evaluation result with a value V. */ +#define OK(v) (struct eval_result) { .status = PE_OK, .value = (v) } + +/* Evaluates a plural expression PEXP for n=N, up to ALLOWED_DEPTH. */ +static struct eval_result +plural_eval_recurse (const struct expression *pexp, unsigned long int n, + unsigned int allowed_depth) { + if (allowed_depth == 0) + /* The allowed recursion depth is exhausted. */ + return (struct eval_result) { .status = PE_STACKOVF }; + allowed_depth--; + switch (pexp->nargs) { case 0: switch (pexp->operation) { case var: - return n; + return OK (n); case num: - return pexp->val.num; + return OK (pexp->val.num); default: break; } @@ -40,52 +73,77 @@ plural_eval (const struct expression *pexp, unsigned long int n) case 1: { /* pexp->operation must be lnot. */ - unsigned long int arg = plural_eval (pexp->val.args[0], n); - return ! arg; + struct eval_result arg = + plural_eval_recurse (pexp->val.args[0], n, allowed_depth); + if (arg.status != PE_OK) + return arg; + return OK (! arg.value); } case 2: { - unsigned long int leftarg = plural_eval (pexp->val.args[0], n); + struct eval_result leftarg = + plural_eval_recurse (pexp->val.args[0], n, allowed_depth); + if (leftarg.status != PE_OK) + return leftarg; if (pexp->operation == lor) - return leftarg || plural_eval (pexp->val.args[1], n); + { + if (leftarg.value) + return OK (1); + struct eval_result rightarg = + plural_eval_recurse (pexp->val.args[1], n, allowed_depth); + if (rightarg.status != PE_OK) + return rightarg; + return OK (rightarg.value ? 1 : 0); + } else if (pexp->operation == land) - return leftarg && plural_eval (pexp->val.args[1], n); + { + if (!leftarg.value) + return OK (0); + struct eval_result rightarg = + plural_eval_recurse (pexp->val.args[1], n, allowed_depth); + if (rightarg.status != PE_OK) + return rightarg; + return OK (rightarg.value ? 1 : 0); + } else { - unsigned long int rightarg = plural_eval (pexp->val.args[1], n); + struct eval_result rightarg = + plural_eval_recurse (pexp->val.args[1], n, allowed_depth); + if (rightarg.status != PE_OK) + return rightarg; switch (pexp->operation) { case mult: - return leftarg * rightarg; + return OK (leftarg.value * rightarg.value); case divide: #if !INTDIV0_RAISES_SIGFPE - if (rightarg == 0) + if (rightarg.value == 0) raise (SIGFPE); #endif - return leftarg / rightarg; + return OK (leftarg.value / rightarg.value); case module: #if !INTDIV0_RAISES_SIGFPE - if (rightarg == 0) + if (rightarg.value == 0) raise (SIGFPE); #endif - return leftarg % rightarg; + return OK (leftarg.value % rightarg.value); case plus: - return leftarg + rightarg; + return OK (leftarg.value + rightarg.value); case minus: - return leftarg - rightarg; + return OK (leftarg.value - rightarg.value); case less_than: - return leftarg < rightarg; + return OK (leftarg.value < rightarg.value); case greater_than: - return leftarg > rightarg; + return OK (leftarg.value > rightarg.value); case less_or_equal: - return leftarg <= rightarg; + return OK (leftarg.value <= rightarg.value); case greater_or_equal: - return leftarg >= rightarg; + return OK (leftarg.value >= rightarg.value); case equal: - return leftarg == rightarg; + return OK (leftarg.value == rightarg.value); case not_equal: - return leftarg != rightarg; + return OK (leftarg.value != rightarg.value); default: break; } @@ -96,10 +154,24 @@ plural_eval (const struct expression *pexp, unsigned long int n) case 3: { /* pexp->operation must be qmop. */ - unsigned long int boolarg = plural_eval (pexp->val.args[0], n); - return plural_eval (pexp->val.args[boolarg ? 1 : 2], n); + struct eval_result boolarg = + plural_eval_recurse (pexp->val.args[0], n, allowed_depth); + if (boolarg.status != PE_OK) + return boolarg; + return plural_eval_recurse (pexp->val.args[boolarg.value ? 1 : 2], n, + allowed_depth); } } /* NOTREACHED */ - return 0; + return (struct eval_result) { .status = PE_ASSERT }; +} + +/* Evaluates a plural expression PEXP for n=N. */ +STATIC +struct eval_result +plural_eval (const struct expression *pexp, unsigned long int n) +{ + return plural_eval_recurse (pexp, n, EVAL_MAXDEPTH); } + +#undef OK diff --git a/gettext-runtime/intl/plural-exp.h b/gettext-runtime/intl/plural-exp.h index abfb4c331..7eb9aed8b 100644 --- a/gettext-runtime/intl/plural-exp.h +++ b/gettext-runtime/intl/plural-exp.h @@ -27,6 +27,8 @@ extern "C" { #endif +/* Parsing a plural expression. */ + enum expression_operator { /* Without arguments: */ @@ -109,9 +111,25 @@ extern void EXTRACT_PLURAL_EXPRESSION (const char *nullentry, unsigned long int *npluralsp) attribute_hidden; + +/* Evaluating a parsed plural expression. */ + +enum eval_status +{ + PE_OK, /* Evaluation succeeded, produced a value */ + PE_STACKOVF, /* Stack overflow */ + PE_ASSERT /* Assertion failure */ +}; + +struct eval_result +{ + enum eval_status status; + unsigned long int value; /* Only relevant for status == PE_OK */ +}; + #if !defined (_LIBC) && !defined (IN_LIBINTL) && !defined (IN_LIBGLOCALE) -extern unsigned long int plural_eval (const struct expression *pexp, - unsigned long int n); +extern struct eval_result plural_eval (const struct expression *pexp, + unsigned long int n); #endif diff --git a/gettext-tools/src/msgl-check.c b/gettext-tools/src/msgl-check.c index 32c21adf5..0d4d98fdc 100644 --- a/gettext-tools/src/msgl-check.c +++ b/gettext-tools/src/msgl-check.c @@ -1,5 +1,5 @@ /* Checking of messages in PO files. - Copyright (C) 1995-1998, 2000-2008, 2010-2016, 2019 Free Software Foundation, Inc. + Copyright (C) 1995-2023 Free Software Foundation, Inc. Written by Ulrich Drepper , April 1995. This program is free software: you can redistribute it and/or modify @@ -75,9 +75,9 @@ plural_expression_histogram (const struct plural_distribution *self, count = 0; for (n = min; n <= max; n++) { - unsigned long val = plural_eval (expr, n); + struct eval_result res = plural_eval (expr, n); - if (val == j) + if (res.status == PE_OK && res.value == j) count++; } @@ -123,7 +123,24 @@ check_plural_eval (const struct expression *plural_expr, for (n = 0; n <= 1000; n++) { - unsigned long val = plural_eval (plural_expr, n); + struct eval_result res = plural_eval (plural_expr, n); + if (res.status != PE_OK) + { + /* End of protection against arithmetic exceptions. */ + uninstall_sigfpe_handler (); + + if (res.status == PE_STACKOVF) + po_xerror (PO_SEVERITY_ERROR, header, NULL, 0, 0, false, + _("plural expression can produce stack overflow")); + else + /* Other res.status values should not occur. */ + abort (); + + free (array); + return 1; + } + + unsigned long val = res.value; if ((long) val < 0) { diff --git a/gettext-tools/src/plural-eval.h b/gettext-tools/src/plural-eval.h index 6968596af..d2b3ce983 100644 --- a/gettext-tools/src/plural-eval.h +++ b/gettext-tools/src/plural-eval.h @@ -1,5 +1,5 @@ /* Expression evaluation for plural form selection. - Copyright (C) 2005-2006, 2019 Free Software Foundation, Inc. + Copyright (C) 2005-2023 Free Software Foundation, Inc. Written by Bruno Haible , 2005. This program is free software: you can redistribute it and/or modify @@ -19,7 +19,7 @@ #define _PLURAL_EVAL_H -/* Definition of 'struct expression', and +/* Definition of 'struct expression' and 'struct eval_result', and declaration of extract_plural_expression() and plural_eval(). */ #include "plural-exp.h" diff --git a/gettext-tools/tests/plural-3 b/gettext-tools/tests/plural-3 index 4a4fd3fe6..0943f438e 100644 --- a/gettext-tools/tests/plural-3 +++ b/gettext-tools/tests/plural-3 @@ -4,10 +4,9 @@ # Test that ngettext() does not crash when the plural form rule leads to # a stack overflow. -# In fact, the stack overflow is caught by the bison parser (plural.c, -# macro YYMAXDEPTH = 10000). Since the same bison parser is used by -# glibc, libintl, and 'msgfmt -c', we can observe the behaviour by using -# the msgfmt option '-c'. +# In fact, a stack overflow at parsing time is caught by the bison parser +# (plural.c, macro YYMAXDEPTH = 10000), and a stack overflow at evaluation +# time is caught as well (eval-plural.h, macro EVAL_MAXDEPTH = 100). test -d plural-3-dir || mkdir plural-3-dir test -d plural-3-dir/ll || mkdir plural-3-dir/ll @@ -36,17 +35,17 @@ msgstr[1] "y" EOF : ${MSGFMT=msgfmt} -${MSGFMT} -c -o plural-3-dir/ll/LC_MESSAGES/plural.mo plural-3-ll.po || Exit 1 +${MSGFMT} -o plural-3-dir/ll/LC_MESSAGES/plural.mo plural-3-ll.po || Exit 1 LANGUAGE= TEXTDOMAIN=plural TEXTDOMAINDIR=plural-3-dir \ $NGETTEXT --env LC_ALL=ll X Y 42 > dataout test $? = 0 || Exit 1 -test y = "`cat dataout`" || Exit 1 +test x = "`cat dataout`" || Exit 1 LANGUAGE= TEXTDOMAIN=plural TEXTDOMAINDIR=plural-3-dir \ $NGETTEXT --env LC_ALL=ll --thread X Y 42 > thread-dataout test $? = 0 || Exit 1 -test y = "`cat thread-dataout`" || Exit 1 +test x = "`cat thread-dataout`" || Exit 1 # This one is large enough that YYMAXDEPTH is exceeded, i.e. the parser fails.