]> git.ipfire.org Git - thirdparty/glibc.git/commitdiff
intl: Add tests for plural expression hardening
authorAvinal Kumar <avinal.xlvii@gmail.com>
Mon, 4 May 2026 17:22:06 +0000 (22:52 +0530)
committerAdhemerval Zanella <adhemerval.zanella@linaro.org>
Mon, 11 May 2026 18:49:41 +0000 (15:49 -0300)
The first test checks for stack overflow.  It uses a plural expression
nested 5000 levels deep using the !(1-(...)) pattern.  The parser
accepts it (below YYMAXDEPTH=10000), but evaluation exeeds
EVAL_MAXDEPTH=100 and falls back to index 0 instead of crashing with
SIGSEGV.

The second test checks for division by zero in plural expression.  The
expression (n!=1)+1/(n!=1729) triggers 1/0 for n=1729.  msgfmt only
validates 0<= n <= 1000, so the .mo file is accepted.  Evaluation
returns PE_INTDIV and falls back instead of raising SIGFPE.

Adaptations from gettext to glibc:

- gettext's plural-3 embeds the nested expresion as a literal string.
This test uses an AWK script (plural-depth.awk) to generate the same
expression.

- gettext uses LANGUAGE= (empty) with LC_ALL=ll and its own locale
setup.  glibc requires a real locale for setlocale() or else the "C"
locale override in dcigettext.c ignores LANGUAGE entirely.

The tests are derived from GNU gettext's plural-3 (commit 021348871a22)
and plural-4 (commit 429ba6c6b835), adapted to glibc's test framework.

Original author: Bruno Haible <bruno@clisp.org>

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
Reviewed-by: Adhemerval Zanella <adhemerval.zanella@linaro.org>
intl/Makefile
intl/plural-depth.awk [new file with mode: 0644]
intl/tst-plural-eval.c [new file with mode: 0644]
intl/tst-plural-eval.sh [new file with mode: 0644]

index 42875eb1a9cf8b7e8c6c5b92c95df3fde113d75a..a8b41a19931884589cfda7abcbdec5340b3327bc 100644 (file)
@@ -22,13 +22,21 @@ subdir = intl
 include ../Makeconfig
 
 headers = libintl.h
-routines = bindtextdom dcgettext dgettext gettext      \
+routines = bindtextdom dcgettext dgettext gettext \
           dcigettext dcngettext dngettext ngettext \
           finddomain loadmsgcat localealias textdomain
-aux =     l10nflist explodename plural plural-exp hash-string
+aux = l10nflist explodename plural plural-exp hash-string
 
 multithread-test-srcs := tst-gettext4 tst-gettext5 tst-gettext6
-test-srcs := tst-gettext tst-translit tst-gettext2 tst-codeset tst-gettext3
+test-srcs := \
+       tst-gettext \
+       tst-translit \
+       tst-gettext2 \
+       tst-codeset \
+       tst-gettext3 \
+       tst-plural-eval
+       # test-srcs
+
 ifeq ($(have-thread-library),yes)
 test-srcs += $(multithread-test-srcs)
 endif
@@ -53,9 +61,15 @@ $(objpfx)plural.o: $(objpfx)plural.c
 ifeq ($(run-built-tests),yes)
 ifeq (yes,$(build-shared))
 ifneq ($(strip $(MSGFMT)),:)
-tests-special += $(objpfx)tst-translit.out $(objpfx)tst-gettext.out \
-                $(objpfx)tst-gettext2.out $(objpfx)tst-codeset.out \
-                $(objpfx)tst-gettext3.out
+tests-special += \
+       $(objpfx)tst-translit.out \
+       $(objpfx)tst-gettext.out \
+       $(objpfx)tst-gettext2.out \
+       $(objpfx)tst-codeset.out \
+       $(objpfx)tst-gettext3.out \
+       $(objpfx)tst-plural-eval.out
+       # tests-special
+
 ifeq ($(have-thread-library),yes)
 tests-special += $(objpfx)tst-gettext4.out $(objpfx)tst-gettext5.out \
                 $(objpfx)tst-gettext6.out
@@ -103,6 +117,10 @@ $(objpfx)tst-gettext4.out: tst-gettext4.sh $(objpfx)tst-gettext4
 $(objpfx)tst-gettext6.out: tst-gettext6.sh $(objpfx)tst-gettext6
        $(SHELL) $< $(common-objpfx) '$(test-program-prefix)' $(common-objpfx)intl/; \
        $(evaluate-test)
+$(objpfx)tst-plural-eval.out: tst-plural-eval.sh $(objpfx)tst-plural-eval
+       $(SHELL) $< $(common-objpfx) '$(test-program-prefix)' \
+         $(common-objpfx)intl/; \
+       $(evaluate-test)
 
 $(objpfx)tst-codeset.out: $(codeset_mo)
 $(objpfx)tst-gettext3.out: $(codeset_mo)
@@ -140,6 +158,7 @@ CFLAGS-tst-gettext3.c += -DOBJPFX=\"$(objpfx)\"
 CFLAGS-tst-gettext4.c += -DOBJPFX=\"$(objpfx)\"
 CFLAGS-tst-gettext5.c += -DOBJPFX=\"$(objpfx)\"
 CFLAGS-tst-gettext6.c += -DOBJPFX=\"$(objpfx)\"
+CFLAGS-tst-plural-eval.c += -DOBJPFX=\"$(objpfx)\"
 
 ifeq ($(have-thread-library),yes)
 ifeq (yes,$(build-shared))
@@ -156,6 +175,7 @@ $(objpfx)tst-gettext3.out: $(objpfx)tst-gettext.out
 $(objpfx)tst-gettext4.out: $(objpfx)tst-gettext.out
 $(objpfx)tst-gettext5.out: $(objpfx)tst-gettext.out
 $(objpfx)tst-gettext6.out: $(objpfx)tst-gettext.out
+$(objpfx)tst-plural-eval.out: $(objpfx)tst-gettext.out
 
 CPPFLAGS += -D'LOCALEDIR="$(localedir)"' \
            -D'LOCALE_ALIAS_PATH="$(localedir)"' \
diff --git a/intl/plural-depth.awk b/intl/plural-depth.awk
new file mode 100644 (file)
index 0000000..7b200c0
--- /dev/null
@@ -0,0 +1,54 @@
+# plural-depth.awk - Generate .po file with deeply nested plural expression.
+# Copyright (C) 2026 Free Software Foundation, Inc.
+#
+# This file is part of the GNU C Library.
+#
+# The GNU C Library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# The GNU C Library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with the GNU C Library; if not, see
+# <https://www.gnu.org/licenses/>.
+
+# Generate a .po file whose Plural-Forms header contains a plural
+# expression nested DEPTH levels deep.  Each level wraps as !(1-(...)),
+# producing an expression that is accepted by the parser (YYMAXDEPTH=10000)
+# but exceeds EVAL_MAXDEPTH=100 at runtime.
+#
+# Usage: awk -v DEPTH=5000 -f plural-depth.awk > plural-depth.po
+
+BEGIN {
+    if (DEPTH == 0)
+       DEPTH = 5000
+
+    expr = ""
+    for (i = 0; i < DEPTH; i++)
+       expr = expr "!(1-"
+    expr = expr "(n!=1)"
+    for (i = 0; i < DEPTH; i++)
+       expr = expr ")"
+
+    print "msgid \"\""
+    print "msgstr \"\""
+    print "\"Project-Id-Version: test\\n\""
+    print "\"PO-Revision-Date: 2026-01-01 00:00+0000\\n\""
+    print "\"Last-Translator: \\n\""
+    print "\"Language-Team: \\n\""
+    print "\"Language: ll\\n\""
+    print "\"MIME-Version: 1.0\\n\""
+    print "\"Content-Type: text/plain; charset=ASCII\\n\""
+    print "\"Content-Transfer-Encoding: 8bit\\n\""
+    print "\"Plural-Forms: nplurals=2; plural=" expr ";\\n\""
+    print ""
+    print "msgid \"X\""
+    print "msgid_plural \"Y\""
+    print "msgstr[0] \"x\""
+    print "msgstr[1] \"y\""
+}
diff --git a/intl/tst-plural-eval.c b/intl/tst-plural-eval.c
new file mode 100644 (file)
index 0000000..8658598
--- /dev/null
@@ -0,0 +1,76 @@
+/* Test plural expression evaluation hardening.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <libintl.h>
+#include <locale.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <support/check.h>
+#include <support/support.h>
+
+static int
+do_test (void)
+{
+  unsetenv ("OUTPUT_CHARSET");
+
+  /* Use a real locale so that the active locale is not "C".  */
+  TEST_VERIFY (setenv ("LC_ALL", "de_DE.UTF-8", 1) == 0);
+  xsetlocale (LC_ALL, "");
+  /* Set a dummy langauge to override lookup.  */
+  TEST_VERIFY (setenv ("LANGUAGE", "ll", 1) == 0);
+
+  /* Test 1: Stack overflow in plural evaluation.
+     The .mo file has a plural expression with 5000 levels of nesting
+     like !(1-(!(1-(...(n!=1)...)))).  Before the fix, plural_eval()
+     used unbounded recursion and would crash with SIGSEGV on threads
+     with small stacks.  After the fix, EVAL_MAXDEPTH=100 causes
+     plural_eval_recurse() to return PE_STACKOVF, and plural_lookup()
+     falls back to index 0 (the singular form).  */
+
+  TEST_VERIFY (bindtextdomain ("plural-depth", OBJPFX "domaindir") != NULL);
+  TEST_VERIFY (textdomain ("plural-depth") != NULL);
+
+  /* ngettext must not crash.  The return value depends on whether
+     the depth limit is hit (falls back to index 0) or the expression
+     evaluates successfully.  Either result is acceptable.  */
+
+  const char *tr = ngettext ("X", "Y", 42);
+  TEST_VERIFY (tr != NULL);
+  TEST_VERIFY (strcmp (tr, "x") == 0 || strcmp (tr, "y") == 0);
+
+  /* Test 2: Division by zero in plural evaluation.
+     The .mo file has plural expression (n!=1)+1/(n!=1729).
+     For n=1729, (n!=1729) is 0, so 1/0 triggers division by zero.
+     Before the fix, this raised SIGFPE.  After the fix,
+     plural_eval_recurse() returns PE_INTDIV, and plural_lookup()
+     falls back to index 0.  */
+
+  TEST_VERIFY (bindtextdomain ("plural-divzero", OBJPFX "domaindir") != NULL);
+  TEST_VERIFY (textdomain ("plural-divzero") != NULL);
+
+  /* ngettext with n=1729 must not crash with SIGFPE.  */
+  tr = ngettext ("X", "Y", 1729);
+  TEST_VERIFY (tr != NULL);
+  TEST_VERIFY (strcmp (tr, "x") == 0 || strcmp (tr, "y") == 0
+              || strcmp (tr, "z") == 0);
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/intl/tst-plural-eval.sh b/intl/tst-plural-eval.sh
new file mode 100644 (file)
index 0000000..f020fa9
--- /dev/null
@@ -0,0 +1,67 @@
+#!/bin/sh
+# Test plural expression evaluation hardening.
+# Copyright (C) 2026 Free Software Foundation, Inc.
+# This file is part of the GNU C Library.
+
+# The GNU C Library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+
+# The GNU C Library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+
+# You should have received a copy of the GNU Lesser General Public
+# License along with the GNU C Library; if not, see
+# <https://www.gnu.org/licenses/>.
+
+set -e
+
+common_objpfx=$1
+test_program_prefix=$2
+objpfx=$3
+
+# Create domain directories.
+mkdir -p ${objpfx}domaindir/ll/LC_MESSAGES
+
+# Test 1: Deeply nested plural expression (stack overflow test).
+# This expression has 5000 levels of nesting, well above EVAL_MAXDEPTH=100
+# but below YYMAXDEPTH=10000 so the parser accepts it.
+LC_ALL=C awk -v DEPTH=5000 -f plural-depth.awk > ${objpfx}plural-depth.po
+
+msgfmt -o ${objpfx}domaindir/ll/LC_MESSAGES/plural-depth.mo \
+       ${objpfx}plural-depth.po || exit 1
+
+# Test 2: Division by zero in plural expression (SIGFPE test).
+# The expression 1/(n!=1729) triggers division by zero for n=1729.
+# msgfmt -c only checks 0 <= n <= 1000, so this passes validation.
+cat > ${objpfx}plural-divzero.po <<EOF
+msgid ""
+msgstr ""
+"Project-Id-Version: test\n"
+"PO-Revision-Date: 2026-01-01 00:00+0000\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"Language: ll\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=ASCII\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n!=1)+1/(n!=1729);\n"
+
+msgid "X"
+msgid_plural "Y"
+msgstr[0] "x"
+msgstr[1] "y"
+msgstr[2] "z"
+EOF
+
+msgfmt -o ${objpfx}domaindir/ll/LC_MESSAGES/plural-divzero.mo \
+       ${objpfx}plural-divzero.po || exit 1
+
+# Run the test.
+${test_program_prefix} \
+${objpfx}tst-plural-eval ${objpfx}domaindir
+
+exit $?