svn commit: r273295 - in head/sbin: ping ping6

Bruce Evans brde at optusnet.com.au
Mon Oct 20 12:47:29 UTC 2014


On Mon, 20 Oct 2014, Hiroki Sato wrote:

> Thank you for your review.  I fixed lines you pointed out and
> attached a patch candidate.  I will show each change line by line in
> this email, too:

Thanks.

> Bruce Evans <brde at optusnet.com.au> wrote
>  in <20141020140848.D935 at besplex.bde.org>:
>
> br> > -			if (cc - ICMP_MINLEN - phdr_len >= sizeof(tv1)) {
> br> > +			if ((size_t)(cc - ICMP_MINLEN - phdr_len) >=
> br> > +			    sizeof(tv1)) {
> br> > 				/* Copy to avoid alignment problems: */
> br> > 				memcpy(&tv32, tp, sizeof(tv32));
> br> > 				tv1.tv_sec = ntohl(tv32.tv32_sec);
> br>
> br> This breaks the warning, and breaks the code on exotic (unsupported)
> br> where it used to work accidentally.  The code was already broken on
> br> non-exotic arches.
>
> -                       if ((size_t)(cc - ICMP_MINLEN - phdr_len) >=
> -                           sizeof(tv1)) {
> +                       if (cc - ICMP_MINLEN - phdr_len >= (int)sizeof(tv1)) {

I tested this.  I couldn't get it to fail, but noticed another type
error and re-noticed associated style bugs.  The packet format is a
network 32+32-bit timeval, and that is what is copied, but the bounds
check is for a host timeval.  On i386, network timeval == host timeval
so there is no problem.  On other 32-bit systems with 32+32-bit
timevals, ntohl() fixes up the byte order if necessary.  But on N-bit
systems with >64-bit timevals, ping -s 8 is broken.  It should send a
32+32-bit timeval but either doesn't send it or can't receive it.  Such
systems include:
- most 64-bit systems.  All supported ones.  Most have a 64-bit time_t,
   and if they didn't then all supported ones have 64-bit longs so a
   timeval would consist of a 32-bit time_t, 32 bits of padding, and a
   64-bit long.  ping -s 15 doesn't work on these systems, but ping -s 16
   does.
- 32-bit systems with 64-bit time_t.  All supported ones have only 32-bit
   32-bit longs and probably have no padding, so they have 96-bit time_t
   and ping -s 12 would work.

ping -s 8 worked on i386 in my simple tests because phdr_len is normally
0 (I don't know how to make it nonzero).  The full packet size is then
36.  This consists of hlen = 20, ICMP_MINLEN = 8, phdr_len = 0, and a
payload consisting of the network 8-byte timeval.

The style bugs here are that there is a macro TIMEVAL_LEN that is used in
some places to obfuscate the problem.  I don't know much about networking
but have hacked on ping a bit and have old code with comments about this:

% diff -u2 ping.c~ ping.c
*** ping.c~	Sat Apr 10 12:10:59 2004
--- ping.c	Sat Apr 10 12:11:00 2004
% @@ -93,4 +93,9 @@
% 
%  #define	INADDR_LEN	((int)sizeof(in_addr_t))
% +/*
% + * XXX this macro used to hide the details of the packet layout, but it now
% + * just obfuscates timevals and is not even consistently used for the size
% + * of a timeval.
% + */
%  #define	TIMEVAL_LEN	((int)sizeof(struct timeval))
%  #define	MASK_LEN	(ICMP_MASKLEN - ICMP_MINLEN)

The obfuscation has increased in -current.  The above is broken since it
uses host timevals.  This has been fixed in -current by replacing 'timeval'
by 'tv32', but that increases the obfuscation because the macro still
looks like it is for a host timeval.

Note that the macro already has a cast to int in it (presumably to fix
warnings), so changing the patch to use it will fix both problems.  But
I don't like the obfuscation of that.  It is good practice to use
sizeof(var) instead of sizeof(typedef_name) like the code does now.
This gives a better chance of applying sizeof() to the right variable
although that is not done now.  It was done in the old version that was
broken by using host timevals.  That version was consistently broken --
it didn't have tv2, and it both the sizeof and the copy were done on tv1.

Further review showed that TIMEVAL_LEN is used a lot and is clearer
than '(int)sizeof(struct tv32)' all over.  Just rename it to something
like NTIMEVAL_LEN so that it is clear that it is for a network timeval.

Grepping for sizeof shows just one place where sizeof() is applied to
struct timeval.  This allocates space for a message.  I think this
should use struct tv32 too, but allocating more space than is needed
and sending a larger message than needed is fairly harmless.  Only
6 out of 32 lines have the style bug of following sizeof by a space.
All of these also have the style bug of not parenthesizing the arg.

ping6.c doesn't have TIMEVAL, and also doesn't seem to have the bug
-- ping6 -s 8 works on freefall.

> br> > +static u_int8_t nonce[8];	/* nonce field for node information */
> br>
> br> This uses the deprecated nonstandard spelling of uint8_t.
>
> s/u_int_{8,16,32}_t/uint_{8,16,32}_t/ in various places.

I don't like changing old code like that.  Makes it hard to see
important changes.  You actually changed u_int8_t to u_char.

> br> > -			sockbufsize = lsockbufsize;
> br> > +			sockbufsize = (int)lsockbufsize;
> br> > 			if (errno || !*optarg || *e ||
> br> > -			    sockbufsize != lsockbufsize)
> br> > +			    lsockbufsize > INT_MAX)
> br> > 				errx(1, "invalid socket buffer size");
>
> br> I don't like warnings about hackish but correct code like the previous
> br> version of the above.  Does the compiler actually complain about
> br> comparing
> br> ints with unsigned longs for equality?
>
> Yes, this part caused the following warning:
>
> /usr/src/head/sbin/ping6/ping6.c:395:20: error: comparison of integers of
>      different signs: 'int' and 'u_long' (aka 'unsigned long')
>      [-Werror,-Wsign-compare]
>                            sockbufsize != lsockbufsize)

ping has much better args handling than ping6.  wollman changed it from
sloppy atoi()s to reasonable strtoul()s in 1997.  strtoul() is easier
to use than usual because most of the args have a lower limit of 0 and
an upper limit of less than ULONG_MAX so simple range checks work.
The methods of setting errno to 0 before the call and checking it
afterwards are never needed or used.  Most cases seem to be correct.

ping6 was apparently fixed separatlely, not as well, and never merged.
It uses errno or strtoul() only in the code quoted above.  It mostly
uses strtol() with the base restricted to 10 instead of strtoul() with
the base unrestricted as in ping.  Most cases are broken using the
technique of assigning the long result to an int variable before
checking it.

% Index: ping6.c
% ===================================================================
% --- ping6.c	(revision 273295)
% +++ ping6.c	(working copy)
% @@ -136,8 +137,8 @@
%  #include <md5.h>
% 
%  struct tv32 {
% -	u_int32_t tv32_sec;
% -	u_int32_t tv32_usec;
% +	uint32_t tv32_sec;
% +	uint32_t tv32_usec;
%  };

tv32 actually consists of a 32-bit time_t and a 32-bit long.  The first
is signed on all supported arches.  The second is always signed.

This is surely harmless.  There are lots of type puns no matter how this
is declared.  The host values started as signed, but are usually
nonnegative so there is no problem.  Even htonl() to a host time_t or
long is a type error.  It depends on 2's complement to work, but you
get that using int32_t.

% @@ -316,7 +316,7 @@
%  	char *policy_out = NULL;
%  #endif
%  	double t;
% -	u_long alarmtimeout;
% +	u_long alarmtimeout, ultmp;
%  	size_t rthlen;
%  #ifdef IPV6_USE_MIN_MTU
%  	int mflag = 0;

u_long is a bogus type for the alarm timeout.  alarm() takes a u_int arg.
ping.c has this bug too.  All the alarmtimeout code in ping.c was obviously
not cleaned up by Wollman in 1997, since has a different style and lower
quality.  u_long is used here mainly to assign the result of alarmtimeout
directly to it, instead of using ultmp like almost everywhere else.  Other
style bugs include using excessive parentheses not checking for null
operands in the usual way (this is only a style bug since null operands
give an out-of-range operand of 0) and a bogus ULONG_MAX check (this is
only a style bug since ULONG_MAX fails a correct range check later).  Other
bugs include not checking for garbage after the operand.  Implementing these
bugs takes about 50% more code (mainly to duplicate the err() message).

That was for the arg parsing of alarmtimeout.  Then when it is used, it
is bogusly cast to int.  The prototype would convert it to the correct
type, u_int.  Compilers should warn about either or none of the the
explicit conversion to int or the prototype's conversion to u_int,
depending on the warning level and whether thay can see that the range
checking ensures that that the value always fits in an int.


% @@ -388,11 +388,10 @@
%  #if defined(SO_SNDBUF) && defined(SO_RCVBUF)
%  			errno = 0;

This can probably be removed too.

%  			e = NULL;

This was nonsense and can certainly be removed.  The endptr parameter in
the strtol() family is output-only.

% -			lsockbufsize = strtoul(optarg, &e, 10);
% -			sockbufsize = (int)lsockbufsize;
% -			if (errno || !*optarg || *e ||
% -			    lsockbufsize > INT_MAX)
% +			ultmp = strtoul(optarg, &e, 10);
% +			if (errno || !*optarg || *e || ultmp > INT_MAX)
%  				errx(1, "invalid socket buffer size");
% +			sockbufsize = ultmp;

Compare with 1997 ping code.  It is similar after removing errno.  Other
differences:
- ping spells the endptr variable ep
- ping doesn't limit the base to 10
- ping orders the test with *ep first
- both use the boolean test for the non-boolean *ep, but only ping6 uses
   it for !*optarg.  Anyway, the !*optarg test is broken.  It only detects
   empty strings, while the correct test ep == optarg that is used by ping
   detects other garbage like strings consisting of only leading whitespace
   (maybe followed by a sign).

%  #else
%  			errx(1,
%  "-b option ignored: SO_SNDBUF/SO_RCVBUF socket options not supported");
% @@ -522,14 +521,15 @@
%  			options |= F_SRCADDR;
%  			break;
%  		case 's':		/* size of packet to send */
% -			datalen = strtol(optarg, &e, 10);
% -			if (datalen <= 0 || *optarg == '\0' || *e != '\0')
% -				errx(1, "illegal datalen value -- %s", optarg);
% -			if (datalen > MAXDATALEN) {
% -				errx(1,
% -				    "datalen value too large, maximum is %d",
% +			ultmp = strtoul(optarg, &e, 10);
% +			if (*e || e == optarg)
% +				errx(EX_USAGE, "invalid packet size: `%s'",
% +				    optarg);
% +			if (ultmp > MAXDATALEN)
% +				errx(EX_USAGE,
% +				    "packetsize too large, maximum is %d",
%  				    MAXDATALEN);
% -			}
% +			datalen = ultmp;
%  			break;

Here you seem to have mostly copied ping.  ping requires privileges for
sizes larger than DEFDATALEN.  In the privileged case it is broken by not
having a bounds check.  In the unprivileged case, its error is EPERM,
while the error is always EX_USAGE in the above.

I don't like the restriction for ping.  Is inet6 so much different that
it doesn't need the restriction?  I think inet4 never needed it after
about 1990 in the US and a bit later here when anyone could afford a
system for spamming the internet.  Anyone could exercise the winping
bug, but such bugs should have been fixed by now.

The separate context-dependent messages in the above and in ping are
not very useful.  The first one is often incorrect.  It says that the
packet size is invalid, but the size has not been checked yet.  Only
the format of the arg has been checked.  The second message is missing
a space in "packet size" and has a grammar error (comman splice) in
the above only.  ping.c's second message also prints the value that
is too large (this is more than echoing the arg, since it removes
leading spaces and converts -1 to a huge u_long value).

I wouldn't try hard to generate correct context-sensitive messages for
all types of errors from strto*().

% @@ -718,7 +718,7 @@
%  		errx(1, "-f and -i incompatible options");
% 
%  	if ((options & F_NOUSERDATA) == 0) {
% -		if (datalen >= sizeof(struct tv32)) {
% +		if (datalen >= (int)sizeof(struct tv32)) {

Another use for TIMEVAL_LEN.

% @@ -1419,7 +1419,7 @@
%  	while (cp < ep) {
%  		i = *cp;
%  		if (i == 0 || cp != *sp) {
% -			if (strlcat((char *)buf, ".", bufsiz) >= bufsiz)
% +			if ((int)strlcat((char *)buf, ".", bufsiz) >= bufsiz)
%  				return NULL;	/*result overrun*/
%  		}
%  		if (i == 0)

I don't like strlcat() and haven't thought about code like this much.  It
returns size_t where snprintf() returns int.  This causes them to have
the opposite sign extension and possibly representability problems.  I
think the case is safe because all possible return values are small.

% @@ -1812,7 +1813,7 @@
%  	 * Bounds checking on the ancillary data buffer:
%  	 *     subtract the size of a cmsg structure from the buffer size.
%  	 */
% -	if (bufsize < (extlen  + CMSG_SPACE(0))) {
% +	if (bufsize < (int)(extlen  + CMSG_SPACE(0))) {
%  		extlen = bufsize - CMSG_SPACE(0);
%  		warnx("options truncated, showing only %u (total=%u)",
%  		    (unsigned int)(extlen / 8 - 1),

Oops, it wasn't such a good idea to change bufsize from size_t.  Now
it interacts badly with strlcat() and CMSG*().

% @@ -1835,7 +1836,7 @@
%  			offset = inet6_opt_get_val(databuf, offset,
%  			    &value4, sizeof(value4));
%  			printf("    Jumbo Payload Opt: Length %u\n",
% -			    (u_int32_t)ntohl(value4));
% +			    (uint32_t)ntohl(value4));

This case is bogus.  ntohl() already returns uint32_t, so the cast has
no effect.  However it is only accidental that uint32_t matches the
format arg.  The correct cast is to u_int, but it is painful to fix
all the print formats that have this bug (similarly for signed int).

% @@ -2027,7 +2028,7 @@
% 
%  		for (off = 0; off < clen; off += sizeof(v)) {
%  			memcpy(&v, cp + off, sizeof(v));
% -			v = (u_int32_t)ntohl(v);
% +			v = (uint32_t)ntohl(v);

This bogus cast has no effect at all.  ntohl() returning uint32_t has
been in POSIX since at least 2001, and BSD always assumed 32-bit longs 
so it didn't need this cast before 2001.

% @@ -2072,16 +2073,16 @@
%  	 * by the length of the data, but note that the detection algorithm
%  	 * is incomplete. We assume the latest draft by default.
%  	 */
% -	if (nilen % (sizeof(u_int32_t) + sizeof(struct in6_addr)) == 0)
% +	if (nilen % (sizeof(uint32_t) + sizeof(struct in6_addr)) == 0)
%  		withttl = 1;
%  	while (nilen > 0) {
% -		u_int32_t ttl;
% +		uint32_t ttl;
% 
%  		if (withttl) {
%  			/* XXX: alignment? */
% -			ttl = (u_int32_t)ntohl(*(u_int32_t *)cp);
% -			cp += sizeof(u_int32_t);
% -			nilen -= sizeof(u_int32_t);
% +			ttl = (uint32_t)ntohl(*(uint32_t *)cp);
% +			cp += sizeof(uint32_t);
% +			nilen -= sizeof(uint32_t);

Lots more bogus casts.  Why spell 4 as sizeof(uint32_t)?  Spelling it as
sizeof(var), where var is the uint32_t variable copied to would be a more
useful spelling.

% @@ -2527,7 +2528,7 @@
% 
%  	printf("Vr TC  Flow Plen Nxt Hlim\n");
%  	printf(" %1x %02x %05x %04x  %02x   %02x\n",
% -	    (ip6->ip6_vfc & IPV6_VERSION_MASK) >> 4, tc, (u_int32_t)ntohl(flow),
% +	    (ip6->ip6_vfc & IPV6_VERSION_MASK) >> 4, tc, (uint32_t)ntohl(flow),
%  	    ntohs(ip6->ip6_plen), ip6->ip6_nxt, ip6->ip6_hlim);
%  	if (!inet_ntop(AF_INET6, &ip6->ip6_src, ntop_buf, sizeof(ntop_buf)))
%  		strlcpy(ntop_buf, "?", sizeof(ntop_buf));

Looks like most of the bogus casts were from a time when ntohl() returned
long on some systems, but instead of fixing the format to match the type,
the type was broken to match the format.

Bruce


More information about the svn-src-all mailing list