Date: Thu, 22 Aug 2013 20:51:15 +1000 (EST) From: Bruce Evans <brde@optusnet.com.au> To: Jilles Tjoelker <jilles@stack.nl> Cc: svn-src-head@FreeBSD.org, svn-src-all@FreeBSD.org, Sergey Kandaurov <pluknet@FreeBSD.org>, Andrey Chernov <ache@FreeBSD.org>, src-committers@FreeBSD.org Subject: Re: svn commit: r254600 - head/lib/libutil Message-ID: <20130822185756.Y1297@besplex.bde.org> In-Reply-To: <20130821213755.GA8052@stack.nl> References: <201308211646.r7LGk6eV051215@svn.freebsd.org> <5214F72B.7070006@freebsd.org> <20130821190309.GB52908@omg> <20130821202725.GA4991@stack.nl> <20130821212413.GC52908@omg> <20130821213755.GA8052@stack.nl>
next in thread | previous in thread | raw e-mail | index | archive | help
On Wed, 21 Aug 2013, Jilles Tjoelker wrote: > On Thu, Aug 22, 2013 at 01:24:13AM +0400, Sergey Kandaurov wrote: >> On Wed, Aug 21, 2013 at 10:27:25PM +0200, Jilles Tjoelker wrote: >>> On Wed, Aug 21, 2013 at 11:03:10PM +0400, Sergey Kandaurov wrote: >>>> On Wed, Aug 21, 2013 at 09:21:47PM +0400, Andrey Chernov wrote: >>>>> On 21.08.2013 20:46, Sergey Kandaurov wrote: >>>>>> number = strtoumax(buf, &endptr, 0); >>>>>> >>>>>> + if (number == UINTMAX_MAX && errno == ERANGE) { >>>>>> + return (-1); >>>>>> + } > >>>>> You need to reset errno before strtoumax() call (errno = 0), because any >>>>> of previous functions may left it as ERANGE. This also has a bogus test for UINTMAX_MAX wheich prevents input of the number UINTMAX_MAX on systems where the limit for the function (UINT64_T_MAX) is the same as the limit for strtoumax(). This test is a wrong way of doing the errno thing. This also has a high density of style bugs: - excessive braces - extra blank line expand_number() remains a very badly designed and implemented function. Its design errors start with its name. It doesn't expand numbers. It converts strings to numbers. The strtoumax() family is missing this bug and others. Its design errors continue with it taking a uint64_t arg and not a uint64_t arg. Its implementation errors begin with broken range checking. The above is an incomplete fix for this. Unless uintmax_t is the same as uint64_t, there are still the following bugs: - the return value of strotumax() is assigned to a uint64_t. This may overflow. After overflow, the bogus (number == UINTMAX_MAX) check has no effect, since when overflow is possible 'number' cannot equal UINTMAX_MAX. - values that exceed the maximum supported (UINT64_MAX) are not detected. They are silently truncated to uint64_t. >>>> Thanks for pointing out. >>>> Does the patch look good? No. It adds many more style bugs... >>>> Index: expand_number.c >>>> =================================================================== >>>> --- expand_number.c (revision 254600) >>>> +++ expand_number.c (working copy) >>>> @@ -53,6 +53,8 @@ >>>> unsigned shift; >>>> char *endptr; >>>> >>>> + errno = 0; >>>> + Another extra blank line. It is worse than most, since it separates the initialization for calling stroumax() from the call. >>>> number = strtoumax(buf, &endptr, 0); >>>> This extra blank line separates the call from the error checking for the function. It was added in the commit that started this thread. The previous version had a bogus extra blank line to separate the result of the error checking from the call. >>>> if (number == UINTMAX_MAX && errno == ERANGE) { > >>> This may cause the function to set errno=0 if it is successful, which is >>> not allowed for standard library functions from C and POSIX. There may >>> be a problem not only if expand_number() is standardized but also if it >>> is used in the implementation of a standard library function. The best >>> solution is to save and restore errno around this (if [ERANGE] is >>> detected, that is a valid errno value to keep). This is painful, but seems to be necessary. >>> In an application it is acceptable to set errno=0 without further ado. > >> What about this change? >> It changes errno only if it was modified from zero in strtoumax(). >> Unconditionally restoring errno after strtoumax() unless ERANGE is >> probably not good as strtoumax() might set different errno (e.g. EINVAL) >> and we might want to keep it as well. Please correct me, if I'm wrong. > >> Index: lib/libutil/expand_number.c >> =================================================================== >> --- lib/libutil/expand_number.c (revision 254600) >> +++ lib/libutil/expand_number.c (working copy) >> @@ -50,15 +50,22 @@ >> expand_number(const char *buf, uint64_t *num) >> { >> uint64_t number; >> + int saved_errno; Style bugs: - declarations more unsorted than before. Previously, only endptr was unsorted. - the variable for this is normally names serrno. libutil mostly follows this convention -- it has several serrno's and no saved_errno's, but it does have some error's (error = errno; ...), mainly in pidfile.c. Only login_cap.c is notkkk careful about clobbering errno to 0. >> unsigned shift; >> char *endptr; >> >> + saved_errno = errno; >> + errno = 0; >> + >> number = strtoumax(buf, &endptr, 0); >> >> if (number == UINTMAX_MAX && errno == ERANGE) { >> return (-1); >> } Style bugs, as above. >> >> + if (errno == 0) >> + errno = saved_errno; >> + Yet another style bug. >> if (endptr == buf) { >> /* No valid digits. */ >> errno = EINVAL; >> Another style bug is the placement of the error checking. Normally the endptr check is done before the range check. > > This looks good to me. This looks not so good to me. Some of the other bugs in the old version: % /* % * Convert an expression of the following forms to a uint64_t. % * 1) A positive decimal number. % * 2) A positive decimal number followed by a 'b' or 'B' (mult by 1). % * 3) A positive decimal number followed by a 'k' or 'K' (mult by 1 << 10). % * 4) A positive decimal number followed by a 'm' or 'M' (mult by 1 << 20). % * 5) A positive decimal number followed by a 'g' or 'G' (mult by 1 << 30). % * 6) A positive decimal number followed by a 't' or 'T' (mult by 1 << 40). % * 7) A positive decimal number followed by a 'p' or 'P' (mult by 1 << 50). % * 8) A positive decimal number followed by a 'e' or 'E' (mult by 1 << 60). This garbage was copied from a similar comment in dd/args.c. The number is actually neither positive nor decimal, especially in dd where there is a variant of this function that returns negative numbers. Another design error in this function is that it can't return negative numbers. % */ % This bogus blank line detaches the comment from the code that it describes, resulting in the comment describing null code. % int % expand_number(const char *buf, uint64_t *num) % { % uint64_t number; % unsigned shift; % char *endptr; % % number = strtoumax(buf, &endptr, 0); This supports non-positive non-decimal numbers. The support for non-decimal numbers is more intentional (it is given by the base 0 in the call). Negative numbers are only supported by strtoumax() allowing a minus sign and returning a nonnegative number equal to 1 larger than UINTMAX_MAX less the number without the minus sign. % % if (endptr == buf) { % /* No valid digits. */ % errno = EINVAL; % return (-1); % } Most of the bugs except ones in the comment are missing in dd/args.c: % static uintmax_t % get_num(const char *val) % { % uintmax_t num, mult, prevnum; By using variables of the same type that strtoumax() returns, it avoids overflow bugs in the overflow checking... % char *expr; % % errno = 0; This was fixed 14 years ago in dd. Perhaps earlier in my version. For variations on the bugs, see the 4.4Lite version. It was missing this errno setting. Since this is an application, it can clobber errno like this. % num = strtouq(val, &expr, 0); This is still broken. It unnecessarily assumes that uquad_t = uintmax_t. 4.4BSD-Lite uses strtoul() here, and using strtouq() is a wrong fix. % if (errno != 0) /* Overflow or underflow. */ % err(1, "%s", oper); The error handling is simpler since this is not a library function. The comment about underflow is wrong. Underflow can't happen. What the comment means by underflow is "overflow, with a negative sign". 4.4BSD-Lite was missing this bug. But 4.4BSD-Lite had a wrong range check (num == ULONG_MAX, essentially the same as expand_number() now). % 4.4BSD-Lite was missing this bug (extra blank line). % if (expr == val) /* No valid digits. */ % errx(1, "%s: illegal numeric value", oper); - this code, with its more specific error message, was broken by a POSIX bug. It is now unreachable (EINVAL is returned if no number is found, and for some other cases like an invalid base which can't happen for the above call). It is not bad to break it becauses its message is of low quality in a different way than the strerror(errno) message from err(). There are no lawyers here. strerror(EINVAL) is just too generic. % [...] dd's function is very careful to avoid overflow in shifts and multiplications. expand_number() just allows things to overflow. Since the variables are unsigned, overflow just causes garbage results. Not good enough for a library function. dd's function also supports multipliers. The overflow avoidance is more complication for multiplication than for shifting. Multipliers would be useful in a library function too. expand_number() doesn't support them. Another of its design errors is that it does less than dd's functions, so it cannot be used to replace them. % } % % /* % * Convert an expression of the following forms to an off_t. This is the % * same as get_num(), but it uses signed numbers. % * % * The major problem here is that an off_t may not necessarily be a intmax_t. % */ % static off_t % get_off_t(const char *val) % { % intmax_t num, mult, prevnum; % char *expr; % % errno = 0; % num = strtoq(val, &expr, 0); % if (errno != 0) /* Overflow or underflow. */ % err(1, "%s", oper); % % if (expr == val) /* No valid digits. */ % errx(1, "%s: illegal numeric value", oper); % ... % } dd also has this misimplemented function. It is supposed to fix the problem that get_num() only returns unsigned values. But it is misimplemented by making a copy of get_num() and changing some unsigned types to signed types. The result is mostly worse than casting the result of get_num(). The careful overflow checking no longer works, since it is actually not so careful and does things like checking for overflow after overflow has already occurred. Overflow already occurring gives underfined behaviour with signed types. So the duplicated function differs from just casting the result of get_num() as follows: - it gives undefined behaviour on overflow - strtoq() clamps to QUAD_MAX_/MIN where strtouq() clamps to UQUAD_MAX/MIN(). This mainly amplifies the bugs seeking to large offsets. The kernel supports 64-bit unsigned offsets for some defice files and these are needed for /dev/kmem. But QUAD_MAX is smaller than the necessary offsets, so oseek and iseek in bytes are broken. You have to trick dd into working by using non-byte offsets. Some tricks (bogus casts/conversions in various places) are needed anyway since signed 64-bit off_t can't hold the necessary offsets. The function is still missing checks that quad_t is not too large for offsets. My fix for get_off_t() is to just make it (off_t)get_num() with a check that the result is actually representable as an off_t. This only depends on 2's complement magic. To start fixing expand_number(): /* Fix comment here. */ int expand_number(const char *buf, uint64_t *num) { char *endptr; uintmax_t num; uint64_t number; unsigned shift; int serrno; serrno = errno; errno = 0; num = strtoumax(buf, &endptr, 0); if (num > UINT64_MAX) errno = ERANGE; if (errno != 0) return (-1); errno = serrno; number = num; ... } This depends on the POSIX bug for detecting the no-digits case. Fortunately, the early error handling of this function is simple and compatible with that of strtoumax(), so we don't need to translate strtoumax()'s errors except for adjusting its range error. Similarly in dd: /* Fix comment here. */ static uintmax_t get_num(const char *val) { char *expr; uintmax_t num, mult, prevnum; errno = 0; num = strtoumax(val, &expr, 0); if (errno != 0) err(1, "%s", oper); ... } /* * Convert an expression of the above form to an off_t. This version does * not handle negative numbers perfectly. It assumes 2's complement and * maybe nonnegative multipliers. I hope perfect handling is not necessary * for dd. */ static off_t get_off_t(const char *val) { uintmax_t num; num = get_num(val); if (num != (u_quad_t)(off_t)num) errx(1, "%s: offset too large", oper); return ((off_t)num); } Bruce
Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?20130822185756.Y1297>