NetBSD-Bugs archive

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]

lib/60677: strfmon(3) produces incorrect output in the C [POSIX] locale



>Number:         60677
>Category:       lib
>Synopsis:       strfmon(3) produces incorrect output in the C [POSIX] locale
>Confidential:   no
>Severity:       non-critical
>Priority:       medium
>Responsible:    lib-bug-people
>State:          open
>Class:          sw-bug
>Submitter-Id:   net
>Arrival-Date:   Wed Sep 02 05:15:00 +0000 2026
>Originator:     Robert Elz
>Release:        NetBSD 11.99.6 (anything in recent memory)
>Organization:
Pedantic International Standards Scholars
>Environment:
System: NetBSD jacaranda.noi.kre.to 11.99.6 NetBSD 11.99.6 (JACARANDA:1.1-20260627) #287: Sun Jun 28 03:17:40 +07 2026 kre%jacaranda.noi.kre.to@localhost:/usr/obj/testing/kernels/amd64/JACARANDA amd64
Architecture: x86_64
Machine: amd64
>Description:
	The C (aka POSIX) locale for LC_MONETARY is all empty (specifies
	nothing at all).   That's what the standard specifies, and is also
	what our C locale contains for LC_MONETARY.

	According to the standard for strfmon() when that happens:

	If the '+' flag is included (requiring a sign, which the C locale
	does not have any definition for) EINVAL is the required result.

	If neither the '(' (requesting () around negative values) nor the '+'
	flag is given, then the locale default is used - the C locale does
	not have one of those, in which case strfmon() must assume '+', except
	if the locale hasn't specified what the sign strings should be, "" for
	positive (which is the only method to not specify something, so is
	what the C locale does), and "-" for negative (if the locale specifies
	the same value as for positive, which it does, it is also "") are used.
	There are also defaults for the positioning of the sign if the locale
	doesn't specify any (the C locale doesn't).

	There is no default monetary radix character in the C locale either,
	nor any default number of digits to come after it.   Unfortunately the
	standard has nothing to say about what should be done in that case,
	whether the "." is acceptable as a default radix character, and
	perhaps the common 2 for the default precision.

	Our strfmon includes the following (and more similar) in its
	__setup_vars() internal function:

                *signstr = (lc->negative_sign[0] == '\0') ? "-"
				    : lc->negative_sign;

	        /* Set default values for unspecified information. */
		if (*cs_precedes != 0)
			*cs_precedes = 1;
		if (*sep_by_space == CHAR_MAX)
			*sep_by_space = 0;
		if (*sign_posn == CHAR_MAX)
			*sign_posn = 0;

	All of which make it impossible for the rest of the code to do
	what the standard specifies, as the values that are actually
	in the locale are unavailable because of these default values
	being inserted (just as if the locale defined them that way).

	The internal __format_grouped_double() function (for formatting
	the actual numeric parameter) includes these:

		decimal_point = lc->mon_decimal_point;
		if (*decimal_point == '\0')
			decimal_point = lc->decimal_point;
		thousands_sep = lc->mon_thousands_sep;
		if (*thousands_sep == '\0')
			thousands_sep = lc->thousands_sep;

	which amount to "if the LC_MONETARY locale definition provides
	no values for the decimal-point (radix char) or the
	thousand_sep (grouping char left of the decimal point), just
	use the similar values from LC_NUMERIC instead.   While that
	is probably sane, I can find no justification for doing that
	anywhere in the standards.

	It also contains

		if (right_prec == CHAR_MAX)     /* POSIX locale ? */
			right_prec = 2;

	"right_prec" in this context is the number of digits to place
	after the decimal point ... the LC_NUMERIC locale has no
	concept of a default for that, so the code simply invents one.
	I can find no justification for that either.

	In general, using strfmon() at all in the C (or POSIX) locales
	makes little sense, as there is no C (or POSIX) currency, hence
	no standard (or even non-standard) way to format such a thing.
	That's what the default LC_MONETARY settings for the C locale
	are intended to convey.

	But strfmon() is supposed to work regardless, even regardless
	of the missing necessary information.   For the parts where
	the strfmon() standard gives no guidance, doing something reasonable
	as is done for the decimal_point setting shown above, is probably
	OK (for the C locale, the thousands_sep is irrelevant, as it doesn't
	define any grouping, so that one will never be used in this context).

	I am less certain about simply assuming that "it must mean 2 digits
	after the decimal point" when there is no specification, 0 would be
	just as good a choice.   It doesn't fit EUC, USD, GBP, etc
	(and of course, the LC_MONETARY locale definition is woefully
	deficient in handling formatting currency values that are not
	written like a floating point number, consider pounds, shillings,
	and pence for example - but nothing we can do about that here).

>How-To-Repeat:
	Use a small program like the following:

#include <err.h>
#include <locale.h>
#include <monetary.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>

int
main(int argc, char **argv)
{
	locale_t loc;
	char buf[80];
	ssize_t s;

	if (argc != 4)
		exit(1);

	loc = newlocale(LC_MONETARY_MASK, argv[1], 0);
	if (loc == NULL)
		errx(1, "Locale '%s' not found", argv[1]);

	s = strfmon_l(buf, sizeof buf, loc, argv[2], strtod(argv[3], NULL));
	if (s == -1)
		err(1, "strfmon() failed");

	printf("%zd: [[%s]]\n", s, buf);
}

	Its usage of strfmon_l() rather than strfmon() is irrelevant,
	that just alters the way the locale information to use is
	obtained, about which there are no issues I am aware of.

	Compile it (cc -o foo foo.c) and run it like

		./foo C %n -123.75

	and expect to see something, but certainly not what
	is produced:

		8: [[(123.75)]]

	I kind of expect the correct resulting string (forget
	the length result part, and the "[[" "]]" that are just
	noise, that's all always OK) should be:

		-124

	Certainly it should be using '-' instead of (), and
	without an explicit precision (as in %.3n) since the
	C locale doesn't specify one, a 0 default (or an error)
	would be better that simply inventing "2" out of nowhere.

	If run as

		./foo C %+n -1

	it should return EINVAL, as the C locale has no sign strings,
	and this usage explicitly requests one (the POSIX standard
	takes pains to point out this as a potential pitfall of
	using the '+' flag - any locale, not just the C locale might
	be missing sign strings if their normal usage is to use ()
	around negative numbers).   Instead we get:

		6: [[(1.00)]]

	which is so wrong it is just about unbelievable.   Even if
	the locale specified use of () for negative values, which it
	can do, explicitly asking for a +/- result should override
	that - and if there are no (differing) +/- sign strings to
	use, EINVAL is required.

>Fix:
	For some of this, coming soon.   For other parts, it will depend
	upon what kind of feedback this PR generates.

[Aside: all this comes from attempts to test the new fake
locale I have invented, which needed a LC_MONETARY definition, so
I gave it a strange one, and then started to look at the
ATF libc locale/t_strfmon test, and extending that to test
this new locale, along with similar extensions to the
locale/t_sprintf tests for its LC_NUMERIC definition, which were
the reason for creating it ... all for PR bin/60496.]




Home | Main Index | Thread Index | Old Index