[v2,1/2] intl: Import plural expression hardening from GNU gettext
Checks
| Context |
Check |
Description |
| redhat-pt-bot/TryBot-apply_patch |
success
|
Patch applied to master at the time it was sent
|
Commit Message
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>
Discussed on: https://sourceware.org/pipermail/libc-alpha/2023-October/152010.html
Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
---
intl/dcigettext.c | 27 +++------
intl/eval-plural.h | 139 ++++++++++++++++++++++++++++++++++-----------
intl/plural-exp.h | 21 ++++++-
3 files changed, 134 insertions(+), 53 deletions(-)
Comments
On 01/05/26 08:24, Avinal Kumar wrote:
> 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>
> Discussed on: https://sourceware.org/pipermail/libc-alpha/2023-October/152010.html
>
> Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
LGTM, thanks.
Reviwed-by: Adhemerval Zanella <adhemerval.zanella@linaro.org>
> ---
> 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
>
@@ -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;
@@ -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
+
@@ -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