Skip site navigation (1)Skip section navigation (2)
Date:      Mon, 10 Jan 2005 03:36:11 +1100 (EST)
From:      Bruce Evans <bde@zeta.org.au>
To:        Giorgos Keramidas <keramida@freebsd.org>
Cc:        cvs-src@freebsd.org
Subject:   Re: cvs commit: src/sbin/badsect badsect.c
Message-ID:  <20050110003627.T28619@delplex.bde.org>
In-Reply-To: <20050105150917.GA1592@orion.daedalusnetworks.priv>
References:  <200501031903.j03J3eBd044669@repoman.freebsd.org> <20050105150917.GA1592@orion.daedalusnetworks.priv>

next in thread | previous in thread | raw e-mail | index | archive | help
On Wed, 5 Jan 2005, Giorgos Keramidas wrote:

> On 2005-01-03 13:16, Scott Long <scottl@freebsd.org> wrote:
> >Robert Watson wrote:
> >>rwatson     2005-01-03 19:03:40 UTC
> >>
> >>  FreeBSD src repository
> >>
> >>  Modified files:
> >>    sbin/badsect         badsect.c
> >>  Log:
> >>  The badsect(8) utility uses atol(), which doesn't allow very good error
> >>  checking and only recognizes numbers in base 10.  The attached patch
> >>  checks errno after strtol() and uses a base of 0 to allow octal, or hex
> >>  sector numbers too.
> >>
> >>  PR:             73112
> >>  Submitted by:   keramida
> >>  MFC after:      2 weeks
> >>
> >>  Revision  Changes    Path
> >>  1.21      +4 -1      src/sbin/badsect/badsect.c
> >
> > I'd argue that atol() is buggy for a number of other reasons also,
> > mainly that it means that you're limited to 2^31 sectors.  Maybe this
> > should change to strtoull() ?
>
> Ideally, if that's not a problem, we should go for uintmax_t, since
> `number' in this case is then stored in a daddr_t, which is 64-bits
> wide.

Large block numbers still wouldn't actually work.  badsect might be
able to check that they are not in use, but when it tries to pass them
to mknod(2) it will find that they are too large and give up.  It must
give uo because bad block numbers must be representable as a dev_t and
must not equal NODEV.  Thus the range is currently limited to
[0..0xffffffffe] (32 bits unsigned for dev_t, less 1 value for NODEV).

badsect intentionally does the in-use check before the range check
since it used to be impossible for the range check to fail for a
physically possible block number so the range check never failed but
served as a bitrot check.  Now that the bits have rotted but are hard
to fix, there is little point in not doing an up-front range check and
just using strtoull().

The bits rotted when ffs2 made physical block numbers >= 2^31 possible.
The relevant changes to badsect for ffs2 support were just to change
daddr_t from 32 bits to 64 bits.  This might have allowed the in-use
check to work for block numbers between up to 2^63-1, but it could not
have extended the range of block numbers that would actually work to
any more than up to 2^32-2 (from 1TB to 2TB).

A previous lifetime of this bug started when disk sizes exceeded
32MB and ended when dev_t was expanded from 16 bits to 32.  Now we
have the same bug with the numbers changed from 2TB for disk sizes
and 32 bits for dev_t.  The previous lifetime was from approx. 1980
until 1994.  The length of this lifetime shows that badsect was rarely
used even when it was more needed 10-20 years ago.

Nearby bugs:
- there seems to be no check for block numbers < 0.  Negative block numbers
  might pass the in-use check; then when they are converted to a dev_t
  they break the overflow check for this conversion on machines with
  32-bit longs.
- mknod(2) is broken and accepts a dev_t of NODEV (0xffffffff), and IIRC
  converts it to a value of 0; 0 is an especially dangerous wrong
  number for a disk block.  badsect doesn't know about this bug so it
  doesn't try to avoid it.  A block number of NODEV can occur now that
  daddr_t is 64 bits, but only on 64-bit machines.  On 32-bit machines,
  a block number of NODEV is accidentally handled correctly as a side
  effect of breaking support for block numbers between 2^31 and 2^32-2
  because of the type mismatch between daddr_t and strtol(): strtol()
  is limited to 2^31-1 on 32-bit machines.  Using strtol() used to be
  perfectly correct since daddr_t is signed integral and long was the
  largest signed integral type, but C99 broke the latter.
- the result of strtol() is blindly assigned to a daddr_t.  This used to
  allow harmful overflow on 64-bit machines only -- daddr_t was 32 bits
  but the range checking for strtol() only limited the value to 2^63-1.
  It is dormant now that daddr_t is 64 bits and there are no 128 bit
  machines.

> How about something like this?  I could find no easy way to check for
> overflow of daddr_t, so this is still not quite perfect but it's an
> improvement that may help with Really-Huge(TM) disks.
>
> %%%
> Index: badsect.c
> ===================================================================
> RCS file: /home/ncvs/src/sbin/badsect/badsect.c,v
> retrieving revision 1.21
> diff -u -u -r1.21 badsect.c
> --- badsect.c	3 Jan 2005 19:03:40 -0000	1.21
> +++ badsect.c	5 Jan 2005 15:03:39 -0000
> @@ -62,6 +62,7 @@
>  #include <errno.h>
>  #include <dirent.h>
>  #include <fcntl.h>
> +#include <inttypes.h>
>  #include <libufs.h>
>  #include <paths.h>
>  #include <stdio.h>
> @@ -94,6 +95,7 @@
>  	DIR *dirp;
>  	char name[2 * MAXPATHLEN];
>  	char *name_dir_end;
> +	char *errp;

Style bugs:

(1) Unsorting of declarations.
(2) Declarations of the same type not on the same line.

>
>  	if (argc < 3)
>  		usage();
> @@ -124,8 +126,11 @@
>  			err(7, "%s", name);
>  	}
>  	for (argc -= 2, argv += 2; argc > 0; argc--, argv++) {
> -		number = strtol(*argv, NULL, 0);
> -		if (errno == EINVAL || errno == ERANGE)
> +		errp = NULL;

The variable pointed to by strto*()'s second parameter is an output
parameter; initializing it in the caller is bogus.

> +		errno = 0;

This initialization is necessary but is missing in the current version
(rev.1.21).

> +		number = strtoumax(*argv, &errp, 0);
> +		if (errno != 0 || errp != NULL || (errp != NULL &&

`errp' is guaranteed to be non-NULL here, so this check causes badsect
to always fail.  This bug may have been caused by the bad variable name
`errp'.  It is a "next" pointer, not an "error" pointer.

> +		    *errp != '\0' && *argv != NULL && **argv != '\0'))

If *argv is NULL, then we have already followed the null pointer in
strtoumax().  But it can't be null, since it is before the NULL at the
end of the args.  The **argv check has no effect (errp == *argv in
this case, so the check has already been done; anyway, this check is
strtoumax()'s responsibility and can't be done so simply since an
empty string for *argv is not the only possibility for a garbage arg).

A correct and portable strto*() check looks something like:

		if (errno != 0 || errp == *argv || *errp != '\0)

The current check is:

		if (errno == EINVAL || errno == ERANGE)

The (errp != *argv) part of the check can be written as (errno != EINVAL)
assuming POSIX extensions, but this just makes the code unportable and
takes 2 more bytes of source.  errno = EINVAL can also be set under POSIX
for unsupported bases.  Otherwise, the check in -current is incorrect in
omitting "*errp != '\0'" and verbose but not wrong in checking for a
limited set of errnos (it shouldn't check since it doesn't care about
details of the error, but EINVAL and ERANGE are the only possible values
in POSIX).

>  			err(8, "%s", *argv);
>  		if (chkuse(number, 1))
>  			continue;
> %%%

About checking for overflow of daddr_t: overflow of dev_t is checked by
casting to dev_t and checking if the value is unchanged.  For daddr_t,
this method can be used provided that the value is >= 0 (which needs to
be ensured earlier anyway).

Bruce



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?20050110003627.T28619>