]> git.ipfire.org Git - thirdparty/gettext.git/commitdiff
Avoid crash by stack overflow during plural expression evaluation.
authorBruno Haible <bruno@clisp.org>
Sun, 1 Oct 2023 18:18:12 +0000 (20:18 +0200)
committerBruno Haible <bruno@clisp.org>
Fri, 6 Oct 2023 14:25:03 +0000 (16:25 +0200)
* 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.

gettext-runtime/intl/dcigettext.c
gettext-runtime/intl/eval-plural.h
gettext-runtime/intl/plural-exp.h
gettext-tools/src/msgl-check.c
gettext-tools/src/plural-eval.h
gettext-tools/tests/plural-3

index f1cb79db79ca3cff48a5e16e0f7fec9cc23e937d..7bdc7a76095ef7853c61557bb856ac6c69318063 100644 (file)
@@ -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;
index 38413c4d4327a7ba8c717d1188a6e5ae23266554..90eb34050a66c247862165521183f3baa6ee5efd 100644 (file)
 #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. <https://unix.stackexchange.com/questions/620720/>
+     - 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
+       <https://wiki.musl-libc.org/functional-differences-from-glibc.html#Thread-stack-size>.
+     - 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
+       <https://www.ibm.com/docs/en/aix/7.1?topic=programming-threads-library-options>.
+       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
index abfb4c3315c6e23e3c3e58a914be8c63804117de..7eb9aed8babd5c5e8af7c000c58dd95441150177 100644 (file)
@@ -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
 
 
index 32c21adf546cae496b0dcccf6cc235773b8a30e8..0d4d98fdcc2b859784a98abddef43e404c1142d6 100644 (file)
@@ -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 <drepper@gnu.ai.mit.edu>, 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)
             {
index 6968596afb7a029f1ada1912c1ee1eca04c11e3f..d2b3ce983170a9a7d98e7efb0156ba1714f5e6ae 100644 (file)
@@ -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 <bruno@clisp.org>, 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"
 
index 4a4fd3fe643380e50bd62066fbd665c7c46cb631..0943f438e2445d4e367a52e6e1c3ca6f527bf3bf 100644 (file)
@@ -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.