Re: alx-0008 - Standardize strtoi(3) and strtou(3) from NetBSD

Alejandro Colomar <[email protected]> Wed, 19 Mar 2025 16:26:06 +0100
Newsgroups dev.linux.lists.liba2i
Message-ID <jx4664ishtl34eg2npdrv5fkfdiczqnlq3vjuacjrupjvh377x@gddcftzgwmfq>
--t3erkrnouubohdej
Content-Type: text/plain; protected-headers=v1; charset=utf-8
Content-Disposition: inline
Content-Transfer-Encoding: quoted-printable
From: Alejandro Colomar <[email protected]>
To: Bruno Haible <[email protected]>
Cc: [email protected], [email protected], 
	[email protected], [email protected], christos <[email protected]>, 
	=?utf-8?B?xJBvw6BuIFRy4bqnbiBDw7RuZw==?= Danh <[email protected]>, Paul Eggert <[email protected]>, 
	Eli Schwartz <[email protected]>, Guillem Jover <[email protected]>, 
	Iker Pedrosa <[email protected]>, Michael Vetter <[email protected]>, Robert Elz <[email protected]>, 
	[email protected], Sam James <[email protected]>, "Serge E. Hallyn" <[email protected]>
Subject: Re: alx-0008 - Standardize strtoi(3) and strtou(3) from NetBSD
References: <mgcfwxfmv3kpfnkkf6uj63kx5tdzl64p2zg2us4ntsu6q5xkwj@k52z5yyiywoo>
 <18739733.sWSEgdgrri@nimes>
 <mvwnrmk2xf45ivyk4kzxdxuwdk67666yt3kwafck6vo4vq2lru@wkqmoqsacqkf>
 <3237498.fEcJ0Lxnt5@nimes>
MIME-Version: 1.0
In-Reply-To: <3237498.fEcJ0Lxnt5@nimes>

Hi Bruno,

On Wed, Mar 19, 2025 at 01:15:30AM +0100, Bruno Haible wrote:
> Alejandro Colomar wrote:
> > > It would be useful to show how a success test looks like, after
> > >     strtoi (s, &end, base, min, max, &status)
> > > for each of the four frequent use-cases:
> > >   -a. expect to parse the initial portion of the string, no coercion,
> > >   -b. expect to parse the initial portion of the string, silent coerc=
ion,
> > >   -c. expect to parse the entire string, no coercion,
> > >   -d. expect to parse the entire string, silent coercion.
> > >=20
> > > AFAICS, the success tests are:
> > >   -a. status =3D=3D 0 || status =3D=3D ENOTSUP
> >=20
> > Correct.
> >=20
> > >   -b. status =3D=3D 0 || status =3D=3D ENOTSUP || status =3D=3D ERANGE
> >=20
> > Correct (but most likely a bug).

Actually, now I remember that status can be NULL, in which case it's not
reported.  This is a case where you could check for errors with a
simpler expression:

	end !=3D str

but (status =3D=3D 0 || status =3D=3D ENOTSUP || status =3D=3D ERANGE) is s=
till a
reasnoable one.

like you can do with strtol(3), but with portability guarantess
regarding EINVAL, because strtoi(3bsd) always writes *endp (if nonnull).

I need to update the specification to mention that status can be NULL.

> >=20
> > >   -c. status =3D=3D 0
> >=20
> > Correct.
> >=20
> > >   -d. status =3D=3D 0 || (status =3D=3D ERANGE && end > s && *end =3D=
=3D '\0')
> >=20
> > You don't need end>s, because that would preclude ERANGE.
> >=20
> > 	status =3D=3D 0 || (status =3D=3D ERANGE && end =3D=3D '\0')
> >=20
> > Aaand, most likely a bug.
>=20
> Cases b. and d. are not bugs. Often, the programmer knows that treating
> a value > ULONG_MAX is equivalent to treating the value ULONG_MAX. These
> are *normal* uses of strto[u]l[l]. Often it is the programmer's intent
> that the values "4294967297" and "4294967295" produce the same behaviour
> (the same error message, for example).

If you want ULONG_MAX + 1 to be treated like ULONG_MAX, and both
result in an error, then you should probably clamp at ULONG_MAX - 1,
and consider anything above an error.

> It is for these cases that your specification contains the clamping /
> coercion behaviour.
>=20
> Now, when you look at the table of success tests:
>=20
>    -a. status =3D=3D 0 || status =3D=3D ENOTSUP
>    -b. status =3D=3D 0 || status =3D=3D ENOTSUP || status =3D=3D ERANGE
>    -c. status =3D=3D 0
>    -d. status =3D=3D 0 || (status =3D=3D ERANGE && *end =3D=3D '\0')
>=20
> it is immediately clear that the status return convention is ill-designed,
> because the returned 'status' is not the only thing a programmer has to t=
est
> after calling the function.
>=20
> > Cases b and d are not real, IMO.  I have never seen code where that is
> > wanted, AFAIR, and I analyzed the entire Debian and NetBSD code bases
> > looking precisely for that usage.
>=20
> I disagree.

I didn't find any occurence of 'd' in calls to strtoi(3)/strtou(3).
I didn't analyze calls to strtol(3) et al.

> Any use of strtoul that does not test errno wants overflow
> to be mapped to ULONG_MAX, that is, is in case b. or d.
> Just looking in gnulib and gettext, I find already 6 occurrences:
>   gnulib/lib/getaddrinfo.c:299

lib/getaddrinfo.c-297-          if (!(*servname >=3D '0' && *servname <=3D =
'9'))
lib/getaddrinfo.c-298-            return EAI_NONAME;
lib/getaddrinfo.c:299:          port =3D strtoul (servname, &c, 10);
lib/getaddrinfo.c-300-          if (*c || port > 0xffff)
lib/getaddrinfo.c-301-            return EAI_NONAME;
lib/getaddrinfo.c-302-          port =3D htons (port);

You could remove the preceding conditional if you don't want to avoid
leading whitespace.  You could merge that into the strtou(3) call, which
would report ECANCELED for non-numeric input).  Except that a negative
number is silently converted to a positive large value.  This is why I
use a wrapper function strtou_noneg() that rejects negative numbers.

You could rewrite it as:

	port =3D strtou_noneg(servname, NULL, 10, 0, UINT16_MAX, &status);
	if (status !=3D 0)
		return EAI_NONAME;
	port =3D htons(port);

where strtou_noneg() is:

	uintmax_t
	strtou_noneg(const char *s, char **restrict endp, int base,
	    uintmax_t min, uintmax_t max, int *restrict status)
	{
		int  st;

		if (status =3D=3D NULL)
			status =3D &st;
		if (strtoi(s, endp, base, 0, 1, status) =3D=3D 0 && *status =3D=3D ERANGE)
			return min;

		return strtou(s, endp, base, min, max, status);
	}

I think this is not one case where you want silent saturation.  You're
indeed doing range checks [0, UINT16_MAX].

>   gnulib/lib/nproc.c:402

lib/nproc.c-383-/* Parse OMP environment variables without dependence on OM=
P.
lib/nproc.c-384-   Return 0 for invalid values.  */
lib/nproc.c-385-static unsigned long int
lib/nproc.c:386:parse_omp_threads (char const* threads)
lib/nproc.c-387-{

=2E..

lib/nproc.c-398-  /* Convert it from positive decimal to 'unsigned long'.  =
*/
lib/nproc.c-399-  if (c_isdigit (*threads))
lib/nproc.c-400-    {
lib/nproc.c-401-      char *endptr =3D NULL;
lib/nproc.c:402:      unsigned long int value =3D strtoul (threads, &endptr=
, 10);
lib/nproc.c-403-
lib/nproc.c-404-      if (endptr !=3D NULL)
lib/nproc.c-405-        {
lib/nproc.c-406-          while (*endptr !=3D '\0' && c_isspace (*endptr))
lib/nproc.c-407-            endptr++;
lib/nproc.c-408-          if (*endptr =3D=3D '\0')
lib/nproc.c-409-            return value;
lib/nproc.c-410-          /* Also accept the first value in a nesting level,
lib/nproc.c-411-             since we can't determine the nesting level fro=
m env vars.  */
lib/nproc.c-412-          else if (*endptr =3D=3D ',')
lib/nproc.c-413-            return value;
lib/nproc.c-414-        }
lib/nproc.c-415-    }

First of all, the endptr!=3DNULL test seems misplaced.  The only way that
could be true is if the base is unsupported, and 10 is necessarily
supported.  You should remove the initialization '=3D NULL', and the
check, since both are dead code, IIRC.  That's one of the things you
don't need to care with strtoi(3), because it _always_ sets *endp.

And you could probably remove the isdigit test by calling
strtou_noneg().

This could be something like this (fixing the bugs reported above):

	char    *end;
	u_long  value;

	value =3D strtou_noneg(threads, &end, 10, 0, ULONG_MAX, NULL);
	if (end !=3D threads) {
		end +=3D strspn(end, " \t\n");
		if (streq(end, "")
			return value;
		if (strprefix(end, ","))
			return value;
	}


This is one case where you seem to silently ignore saturation.  Why
don't you have any diagnostic message?

>   gnulib/lib/omp-init.c:48

lib/omp-init.c-47-      char *endptr =3D NULL;
lib/omp-init.c:48:      unsigned long int value =3D strtoul (threads, &endp=
tr, 10);
lib/omp-init.c-49-
lib/omp-init.c-50-      if (endptr !=3D NULL)
lib/omp-init.c-51-        {
lib/omp-init.c-52-          while (*endptr !=3D '\0' && c_isspace (*endptr))
lib/omp-init.c-53-            endptr++;
lib/omp-init.c-54-          if (*endptr =3D=3D '\0')
lib/omp-init.c-55-            return value;
lib/omp-init.c-56-          /* Also accept the first value in a nesting lev=
el,
lib/omp-init.c-57-             since we can't determine the nesting level f=
rom env vars.  */
lib/omp-init.c-58-          else if (*endptr =3D=3D ',')
lib/omp-init.c-59-            return value;
lib/omp-init.c-60-        }

This seems identical to the previous case.

>   gettext/gettext-tools/src/msgfmt.c:287

gettext-tools/src/msgfmt.c-286-          char *endp;
gettext-tools/src/msgfmt.c:287:          size_t new_align =3D strtoul (opta=
rg, &endp, 0);
gettext-tools/src/msgfmt.c-288-
gettext-tools/src/msgfmt.c-289-          if (endp !=3D optarg)
gettext-tools/src/msgfmt.c-290-            alignment =3D new_align;

This code will misbehave badly on platforms where size_t is narrower
than u_long.  Consider the case where you parse a high u_long, let's say
SIZE_MAX + 1ul.  It will be converted to 1.  There's a bug due to a
missing range check (but orthogonal to saturation).

You also don't reject negative numbers, which I expect to be a bug,
connected with the one from above.

This could be rewritten to (fixing the bugs reported above):

	char    *end;
	size_t  new_align;

	new_align =3D strtou_noneg(optarg, &end, 0, 0, SIZE_MAX, NULL);
	if (optarg !=3D end)
		alignment =3D new_align;

This seems another case where you silently saturate.  Why don't you have
a diagnostic message for invalid input?

>   gettext/gettext-tools/src/msgl-check.c:379

gettext-tools/src/msgl-check.c-374-          while (*nplurals !=3D '\0' && =
c_isspace ((unsigned char) *nplurals))
gettext-tools/src/msgl-check.c-375-            ++nplurals;
gettext-tools/src/msgl-check.c-376-          endp =3D nplurals;
gettext-tools/src/msgl-check.c-377-          nplurals_value =3D 0;
gettext-tools/src/msgl-check.c-378-          if (*nplurals >=3D '0' && *npl=
urals <=3D '9')
gettext-tools/src/msgl-check.c:379:            nplurals_value =3D strtoul (=
nplurals, (char **) &endp, 10);
gettext-tools/src/msgl-check.c-380-          if (nplurals =3D=3D endp)
gettext-tools/src/msgl-check.c-381-            {
gettext-tools/src/msgl-check.c-382-              const char *msg =3D _("inv=
alid nplurals value");
gettext-tools/src/msgl-check.c-383-              char *help =3D plural_help=
 (nullentry);
gettext-tools/src/msgl-check.c-384-
gettext-tools/src/msgl-check.c-385-              if (help !=3D NULL)
gettext-tools/src/msgl-check.c-386-                {
gettext-tools/src/msgl-check.c-387-                  char *msgext =3D xaspr=
intf ("%s\n%s", msg, help);
gettext-tools/src/msgl-check.c-388-                  xeh->xerror (CAT_SEVER=
ITY_ERROR, header, NULL, 0, 0, true,
gettext-tools/src/msgl-check.c-389-                               msgext);
gettext-tools/src/msgl-check.c-390-                  free (msgext);
gettext-tools/src/msgl-check.c-391-                  free (help);
gettext-tools/src/msgl-check.c-392-                }
gettext-tools/src/msgl-check.c-393-              else
gettext-tools/src/msgl-check.c-394-                xeh->xerror (CAT_SEVERIT=
Y_ERROR, header, NULL, 0, 0, false,
gettext-tools/src/msgl-check.c-395-                             msg);
gettext-tools/src/msgl-check.c-396-
gettext-tools/src/msgl-check.c-397-              seen_errors++;
gettext-tools/src/msgl-check.c-398-            }

You could get rid of a lot of code preceding the strtoul(3) call by
calling strtou_noneg() instead.  And strspn(3) would also help.

	nplurals +=3D strspn(nplurals, " \t\n");
	nplurals_value =3D strtou_noneg(nplurals, (char **) &end, 10, 0, ULONG_MAX=
);
	if (nplurals =3D=3D end)
		...

On the other hand, I also wonder why you don't diagnose invalid input.
Why is -10 an "invalid nplurals value", but ULONG_MAX+10 is a valid
(albeit clamped) one?

All of these cases look like missing error handling, IMO.

>   gettext/gettext-tools/src/read-stringtable.c:561

gettext-tools/src/read-stringtable.c-553-    {
gettext-tools/src/read-stringtable.c-554-      char *last_colon;
gettext-tools/src/read-stringtable.c-555-      unsigned long number;
gettext-tools/src/read-stringtable.c-556-      char *endp;
gettext-tools/src/read-stringtable.c-557-
gettext-tools/src/read-stringtable.c-558-      if (strlen (line) >=3D 6 && =
memcmp (line, "File: ", 6) =3D=3D 0
gettext-tools/src/read-stringtable.c-559-          && (last_colon =3D strrc=
hr (line + 6, ':')) !=3D NULL
gettext-tools/src/read-stringtable.c-560-          && *(last_colon + 1) !=
=3D '\0'
gettext-tools/src/read-stringtable.c:561:          && (number =3D strtoul (=
last_colon + 1, &endp, 10), *endp =3D=3D '\0'))
gettext-tools/src/read-stringtable.c-562-        {
gettext-tools/src/read-stringtable.c-563-          /* A "File: <filename>:<=
number>" type comment.  */
gettext-tools/src/read-stringtable.c-564-          *last_colon =3D '\0';
gettext-tools/src/read-stringtable.c-565-          catalog_reader_seen_comm=
ent_filepos (catr, line + 6, number);
gettext-tools/src/read-stringtable.c-566-        }
gettext-tools/src/read-stringtable.c-567-      else
gettext-tools/src/read-stringtable.c-568-        catalog_reader_seen_commen=
t (catr, line);
gettext-tools/src/read-stringtable.c-569-    }

Let me try to rewrite it for readability first.

	char        *filepos, *last_colon;
	u_long      n;
	const char  *numstr, *end;

	filepos =3D strprefix(line, "File: ") ?: (char []){""};
	last_colon =3D strrchr(filepos, ':') ?: (char []){":"};
	numstr =3D strprefix(last_colon, ":");

	n =3D strtoul(numstr, (char **) &end, 10);
	if (numstr !=3D end && streq(end, "")) {
		/* A "File: <filename>:<number>" type comment.  */
		strcpy(last_colon, "");
		catalog_reader_seen_comment_filepos(catr, filepos, n);

	} else {
		catalog_reader_seen_comment(catr, line);
	}

You're forgetting about negative numbers?  Or are you certain that they
can't happen?  How about huge values?  Assuming you want compatible code
calling strtou():

	n =3D strtou(numstr, (char **) &end, 10, 0, ULONG_MAX, NULL);
	if (status =3D=3D 0 || status =3D=3D ENOTSUP || status =3D=3D ERANGE) {
		...
	} else {
		...
	}

But again, I wonder why you don't do range checks.

> > > I would therefore propose to change the status value to a bit mask, s=
o that
> > > the error conditions "The converted value was out of range and has be=
en
> > > coerced" and "The given string contains characters that did not get c=
onverted"
> > > can be both returned together, without conflicting.
> >=20
> > Because it is theoretical conditions that a real program never wants,
> > let's not do that.
>=20
> If you don't want to do that, I can only repeat what I said in the previo=
us
> mail: The proposal *does not achieve the goal* of avoiding the most common
> programmer mistakes. For a robust API, the success test should *only* inv=
olve
> testing the returned 'status', nothing else.

Let's discuss this after your responses to the above.

>=20
> Bruno
>=20
>=20
>=20
>=20

--=20
<https://www.alejandro-colomar.es/>

--t3erkrnouubohdej
Content-Type: application/pgp-signature; name="signature.asc"

-----BEGIN PGP SIGNATURE-----

iQIzBAABCgAdFiEES7Jt9u9GbmlWADAi64mZXMKQwqkFAmfa4gcACgkQ64mZXMKQ
wqnqzBAAs/xz7eFiNdAFvsA3Tc2iJHzciX3qJWUMBBZjahAE78fSH+GBte7JFBwS
fjcuRBIgYIpEG3RYmGT1lgmvag3BIQTRKMvRarSPq9vILclbbFmdDnoSM80iBfmM
7TQbzY50vhU280xcrBoRWDvtReRDnp9V3i0hQWexs7b/BTYn9jvsfMyie1osYmcC
ULZ03TnKywC+JgkIqEPyvVir/q8iOB1Qin9mYUil2/uvUOQpWb8QY12b4jCWT407
IBeqmznoRkJBvABcuqjaMhIvK0dRm6E2tKtQkay7qnexB3xNqZ+unwKgMaupfG/F
IhX7ZeuBwN/UHXJS9E9qhkStFblOy4T9xBOOMAQpTU0hsKTQvGPqzqpR9AZEC7yf
p3a5JbVVXyharZnwXo1taotZyZdA472Dubt4+xUNtQYwYh1xt9gWOGXGNQdy8MOA
i+1pC1CS7XYdjt4hsz8Rmn7aJhWwB9JssMphdXDzowvVMRHsuAgwkFioo6lxSris
7IWjKgR1pq5O/Zfq6mYhIWxRZSSIjqG9DrhG/vnzURgrGB1PXSVAgpxtVEqmY+ZV
8mrX2PUcSj5m97Q6RFB3yodD2t4lnv88hdqv9TqLXus5zRqD86eE0nE09Bw/1LxA
CQt17WHFgh3+oltFZdyV7EbGQEjr6qmouY4fjTG4N7rGmRfYBdQ=
=eMi4
-----END PGP SIGNATURE-----

--t3erkrnouubohdej--