Skip site navigation (1)Skip section navigation (2)
Date:      Sun, 1 May 2011 22:04:52 +1000 (EST)
From:      Bruce Evans <brde@optusnet.com.au>
To:        Rick Macklem <rmacklem@uoguelph.ca>
Cc:        rmacklem@freebsd.org, fs@freebsd.org
Subject:   Re: newnfs client and statfs
Message-ID:  <20110501184904.S975@besplex.bde.org>
In-Reply-To: <149943048.820546.1304211668413.JavaMail.root@erie.cs.uoguelph.ca>
References:  <149943048.820546.1304211668413.JavaMail.root@erie.cs.uoguelph.ca>

next in thread | previous in thread | raw e-mail | index | archive | help
On Sat, 30 Apr 2011, Rick Macklem wrote:

> Oops, I never noticed that the "struct statfs" fields had been bumped
> to 64bits. I've attached a patch for the client. Could you please test
> it? (I'll look in case the server has a similar problem.)

Sigh, bugs in this area are very old and still present.

% --- fs/nfsclient/nfs_clport.c.sav	2011-04-30 20:16:39.000000000 -0400
% +++ fs/nfsclient/nfs_clport.c	2011-04-30 20:45:16.000000000 -0400
% @@ -39,6 +39,7 @@ __FBSDID("$FreeBSD: head/sys/fs/nfsclien
%   * be the easiest way to handle the port.
%   */
%  #include <sys/hash.h>
% +#include <sys/limits.h>

Only needed to implement a bug.

%  #include <fs/nfs/nfsport.h>
%  #include <netinet/if_ether.h>
%  #include <net/if_types.h>
% @@ -838,20 +839,14 @@ void
%  nfscl_loadsbinfo(struct nfsmount *nmp, struct nfsstatfs *sfp, void *statfs)
%  {
%  	struct statfs *sbp = (struct statfs *)statfs;
% -	nfsquad_t tquad;
% 
%  	if (nmp->nm_flag & (NFSMNT_NFSV3 | NFSMNT_NFSV4)) {
%  		sbp->f_bsize = NFS_FABLKSIZE;
% -		tquad.qval = sfp->sf_tbytes;
% -		sbp->f_blocks = (long)(tquad.qval / ((u_quad_t)NFS_FABLKSIZE));
% -		tquad.qval = sfp->sf_fbytes;
% -		sbp->f_bfree = (long)(tquad.qval / ((u_quad_t)NFS_FABLKSIZE));
% -		tquad.qval = sfp->sf_abytes;
% -		sbp->f_bavail = (long)(tquad.qval / ((u_quad_t)NFS_FABLKSIZE));
% -		tquad.qval = sfp->sf_tfiles;
% -		sbp->f_files = (tquad.lval[0] & 0x7fffffff);
% -		tquad.qval = sfp->sf_ffiles;
% -		sbp->f_ffree = (tquad.lval[0] & 0x7fffffff);

This mail is too short to describe all the bugs on the above.  The old nfs
client still has the following ones:

- bogus variable tquad
- bogus and broken masking for f_files by 0x7fffffff.  v3 can pass us a
   count >= 2**31.  The bogus masking breaks such counts.  When f_files
   was only long, we had to do something for values larger than LONG_MAX.
   We should have clamped to LONG_MAX.  (See cvtstatfs() which does this
   now for the corresponding problem for ostatfs().)  Instead, we bogusly
   cast.  0x7fffffff is just a misspelling of LONG_MAX which happens to
   be correct for 32-bit 2's complement longs.  That combined with the
   server also being limited to 32 bits is the one case where the cast
   works as intended, and even then it is quite broken -- it just loses
   the top bit of values between 2**31 and 2**32-1.  Perhaps the protocol
   prohibits such values, but at least FreeBSD servers take null care not
   to send them -- see below.
- bogus and even more broken masking for f_ffree.  This was broken even
   when f_ffree was long and long was 32 bits.  Then the mask just
   destroys the sign bit, which a non-broken server will have passed us as
   the top bit in a 32-bit unsigned value.

% +		sbp->f_blocks = sfp->sf_tbytes / NFS_FABLKSIZE;
% +		sbp->f_bfree = sfp->sf_fbytes / NFS_FABLKSIZE;
% +		sbp->f_bavail = sfp->sf_abytes / NFS_FABLKSIZE;

The conversion for f_bavail still has sign extension bugs.  f_bavail
can be negative on the server.  A non-broken (FreeBSD) server passes
us this negative value as a uint64_t value with the top bit set.  It
will be >= 2**63 as an unsigned value and dividing by NFS_FABSLBKSIZE =
2**9 makes it between 2**54 and 2**55-1; all trace of its signeness is
lost, exccept we know that garbage values in the range 2**54 to 2**55-1
mean this overflow error.

The old nfs client still has this bug.

Old versions of the old nfs client had broken scaling which paniced
trying to use the values near 2**54 given by the above overflow bugs.
See statfs_scale_blocks() for non-broken scaling for the corresponding
problem for ostatfs().

Someone broke the old FreeBSD nfs server to work around the broken FreeBSD
nfs client.  It remains broken :-(.  This bug is missing in the new nfs
server -- it just passes the server's f_bavail :-), without paying quite
enough attention to the sign bit.

There is of cause a portability problem.  We need to export negative
values from FreeBSD servers to FreeBSD clients without breaking other
combinations.  The combination of the new nfs server with really old
old nfs clients is broken :-).  NetBSD's nfsclient has explicit code
to try to handle this problem.  I couldn't see how it could work --
negative values must be passed in some way, and there is nothing better
than passing them as (large) positive values mod 2**64.  IIRC, NetBSD
changes the values but this cannot work since it loses info.  Hmm,
there are 3 fields to use (f_blocks, f_bfree and f_bavail).  These
provide some redundancy, but neither NetBSD's code nor anything that
I could think of worked to recover ffs's negative avail counts from
nonnegative values in these fields, and frobbing these fields would
be unportable anyway.  Both the nfs protocol and POSIX's statvfs() (?)
API and types seem to be incapable of handing ffs's negative f_bavail
counts (POSIX only has unsigned block counts...).

% +		sbp->f_files = sfp->sf_tfiles;

Now correct, almost.  As for the other fields, it tacitly assumes that
the type of the lvalue is larger than the type of the lvalue.  Both
happen to be 64 bits here.  For the signed fields, there assumption
is strictly incorrect, since the lvalue is 64 bit signed while the
rvalue is 64 bits unsigned.  By "not paying quite enough attention to
the sign bit" in the above, I mean that it is tacitly assumed that
we can start with a 64-bit signed value, convert it to u_quad_t
(another type error -- should not assume that u_quad_t is uint64_t),
pass it through the nfs protocol, convert back to int64_t (or forget
to convert back, as above), and recover the original value.  This
deserves special care since it abuses the protocol.

% +		sbp->f_ffree = (sfp->sf_ffiles & OFF_MAX);

Any masking here is logically wrong, and in practice just destroys the
sign bit, as described above for the 0x7fffffff mask with old 32 bit
systems.  Masking with OFF_MAX has additional logic errors.  OFF_MAX
is the maximum value for an off_t, but none of the types here has
anything to do with off_t.

%  	} else if ((nmp->nm_flag & NFSMNT_NFSV4) == 0) {
%  		sbp->f_bsize = (int32_t)sfp->sf_bsize;
%  		sbp->f_blocks = (int32_t)sfp->sf_blocks;

I think this is just the v2 case.  The old nfs client uses essentially
the same bogus casts.  No casts should be used (clamping should be
used), but if we use casts it may be possible to use non-bogus ones.
I think these are just no casts for the unsigned fields but int32_t
for the signed ones.  The v2 protocol is limited to 32 bits, and we
can easily represent any 32-bit value since we have 64-bit fields for
the lvalues.  We just need to be careful with the sign bit (in the
31th bit of an unsigned value in the sfp fields), but can keep the
31th bit as an unsigned bit without problems now that the statfs fields
are 64 bits.  Casting for the unsigned fields now just breaks the value
unnecessarily if the protocol manages to pass the 31th bit as a value
bit for such fields.

Servers should pay even more attention to unrepresentable bits than to
sign bis, but pay considerably less.  Both the old and the new nfs server
blindly truncate f_bfree, etc., to 32 bits in the v2 case (except the old
nfs server corrupts negative f_bavail to 0).  (For v3, they tacitly assume
that no truncation occurs on conversion to 64 bits.)

The old nfs server also gratuitously breaks the file counts (f_files
etc.) for the v3 case.  It should use txdr_hyper(), but uses exdr_unsigned()
plus extra code to lose 32 bits.  This is fixed in NetBSD and in the
new nfs server.

Bruce



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