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>
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
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
$(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)
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))
$(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)"' \
--- /dev/null
+# 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\""
+}
--- /dev/null
+/* 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>
--- /dev/null
+#!/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 $?