[PATCH] intl: Import plural expression hardening from GNU gettext
Avinal Kumar
avinal.xlvii@gmail.com
Wed Apr 29 17:04:28 GMT 2026
The plural expression evaluator plural_eval() in eval-plural.h uses
unbounded recursion, which can cause a stack overflow crash with
deeply nested expressions in malicious .mo files. This is
particularly dangerous on threads with small stacks (musl libc
default: 128 KB, AIX 7 default: 96 KB, glibc after ulimit -s 260:
~3919 recursions max).
Additionally, division by zero in plural expressions triggers
raise(SIGFPE), which is not multithread-safe, catching SIGFPE
requires per-process signal handlers that race with other threads.
Fix both by importing the hardening from GNU gettext:
- Replace unbounded plural_eval() with depth-limited
plural_eval_recurse() (EVAL_MAXDEPTH=100), returning a
struct eval_result with status instead of a bare unsigned long.
- Return PE_INTDIV status on division by zero instead of raising
SIGFPE. Remove the architecture-specific INTDIV0_RAISES_SIGFPE
macro and the conditional #include <signal.h>.
- Update plural_lookup() in dcigettext.c to handle the new return
type, falling back to index 0 on any evaluation failure.
Based on GNU gettext commits ef37a1540 and 726bfb1d1.
Original author: Bruno Haible <bruno@clisp.org>
Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
---
This is the first in a series of patches to sync glibc's intl/ with
upstream GNU gettext. The last sync was about 1.5 years ago.
Since then, gettext has accumulated security fixes, bug fixes, and
dead code cleanup that glibc lacks.
After excluding Windows-specific and build system changes (which
account for ~80% of the raw diff), approximately 540 lines of
meaningful change need to be imported across ~10 files.
This particular change was already discussed and approved on
https://sourceware.org/pipermail/libc-alpha/2023-October/152010.html
but wasn't followed by any patches.
intl/dcigettext.c | 27 +++------
intl/eval-plural.h | 139 ++++++++++++++++++++++++++++++++++-----------
intl/plural-exp.h | 21 ++++++-
3 files changed, 134 insertions(+), 53 deletions(-)
diff --git a/intl/dcigettext.c b/intl/dcigettext.c
index 9ebc54eeec..fb948c1092 100644
--- a/intl/dcigettext.c
+++ b/intl/dcigettext.c
@@ -68,20 +68,6 @@ extern int errno;
#include <locale.h>
-#ifdef _LIBC
- /* Guess whether integer division by zero raises signal SIGFPE.
- Set to 1 only if you know for sure. In case of doubt, set to 0. */
-# if defined __alpha__ || defined __arm__ || defined __i386__ \
- || defined __m68k__ || defined __s390x__
-# define INTDIV0_RAISES_SIGFPE 1
-# else
-# define INTDIV0_RAISES_SIGFPE 0
-# endif
-#endif
-#if !INTDIV0_RAISES_SIGFPE
-# include <signal.h>
-#endif
-
#if defined HAVE_SYS_PARAM_H || defined _LIBC
# include <sys/param.h>
#endif
@@ -1381,14 +1367,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/intl/eval-plural.h b/intl/eval-plural.h
index 243f6027a1..e231e9bfd3 100644
--- a/intl/eval-plural.h
+++ b/intl/eval-plural.h
@@ -14,24 +14,59 @@
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
+/* Written by Ulrich Drepper and Bruno Haible. */
+
#ifndef STATIC
#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 +75,73 @@ 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)
- raise (SIGFPE);
-#endif
- return leftarg / rightarg;
+ if (rightarg.value == 0)
+ return (struct eval_result) { .status = PE_INTDIV };
+ return OK (leftarg.value / rightarg.value);
case module:
-#if !INTDIV0_RAISES_SIGFPE
- if (rightarg == 0)
- raise (SIGFPE);
-#endif
- return leftarg % rightarg;
+ if (rightarg.value == 0)
+ return (struct eval_result) { .status = PE_INTDIV };
+ 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 +152,25 @@ 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/intl/plural-exp.h b/intl/plural-exp.h
index f0d3a611e1..e2e0c30563 100644
--- a/intl/plural-exp.h
+++ b/intl/plural-exp.h
@@ -26,6 +26,7 @@
extern "C" {
#endif
+/* Parsing a plural expression. */
enum expression_operator
{
@@ -109,8 +110,26 @@ 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_INTDIV, /* Integer division by zero */
+ PE_INTOVF, /* Integer overflow */
+ 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,
+extern struct eval_result plural_eval (const struct expression *pexp,
unsigned long int n);
#endif
--
2.54.0
More information about the Libc-alpha
mailing list