Re: [PATCH] nfsd: reject out-of-range nseconds in NFSv3 nfstime3 decode

Jeff Layton <[email protected]>
Newsgroups gmane.linux.nfs
Message-ID <[email protected]>
On Thu, 2026-06-11 at 12:09 +0800, robbieko wrote:
> From: Robbie Ko <robbieko-UelDjCVBxVpWk0Htik3J/[email protected]>
> 
> The NFSv3 nfstime3 decoder svcxdr_decode_nfstime3() accepts the 32-bit
> nseconds field from the wire without validating its range. RFC 1813
> does not constrain the value, but a tv_nsec >= NSEC_PER_SEC is not a
> valid timespec64. The NFSv4 decoder nfsd4_decode_nfstime4() already
> rejects such values with NFS4ERR_INVAL; NFSv3 had no equivalent check.
> 
> A malicious or buggy client can therefore send a SETATTR carrying an
> out-of-range nseconds (up to 4294967295). The value flows through
> nfsd_setattr() -> notify_change() -> timestamp_truncate(), which does
> not clamp tv_nsec to < NSEC_PER_SEC when the filesystem supports
> nanosecond granularity (s_time_gran == 1). The inode atime/mtime
> setters store it verbatim (only ctime is normalized via
> inode_set_ctime_to_ts()).
> 
> The un-normalized value then corrupts on-disk metadata: ext4's
> ext4_encode_extra_time() shifts tv_nsec left by EXT4_EPOCH_BITS, which
> overflows the 32-bit extra field and clobbers the seconds-epoch bits,
> so the stored seconds (year) are wrong on read-back. XFS with bigtime
> mis-stores the timestamp for the same reason. This is silent, with no
> WARN_ON anywhere in the path to catch it.
> 
> Validate the decoded nseconds in svcxdr_decode_nfstime3() and fail the
> XDR decode if it is out of range, mirroring the NFSv4 behavior.
> 
> Signed-off-by: Robbie Ko <robbieko-UelDjCVBxVpWk0Htik3J/[email protected]>
> ---
>  fs/nfsd/nfs3xdr.c | 2 ++
>  1 file changed, 2 insertions(+)
> 
> diff --git a/fs/nfsd/nfs3xdr.c b/fs/nfsd/nfs3xdr.c
> index 2ff9a991a8fb..59e82ce03c20 100644
> --- a/fs/nfsd/nfs3xdr.c
> +++ b/fs/nfsd/nfs3xdr.c
> @@ -64,6 +64,8 @@ svcxdr_decode_nfstime3(struct xdr_stream *xdr, struct timespec64 *timep)
>  		return false;
>  	timep->tv_sec = be32_to_cpup(p++);
>  	timep->tv_nsec = be32_to_cpup(p);
> +	if (timep->tv_nsec >= (u32)1000000000)
> +		return false;
>  
>  	return true;
>  }

Conceptually this looks fine, but it should use the named constant
(NSEC_PER_SEC). Also, does v2 have a similar issue?
-- 
Jeff Layton <[email protected]>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.