lockd doesn't currently vet the start and length in nlm4 requests like
it should, and can end up requesting locks with arguments that overflow
to the filesystem.
The NLM4 protocol unsigned 64-bit arguments for both start and length,
whereas struct file_lock tracks the start and end as loff_t values. By
the time we get around to calling nlm4svc_retrieve_args, we've lost the
information that would allow us to determine if there was an overflow.
Track whether the arguments will overflow as a boolean in struct
nlm_lock, and test for that in nlm4svc_retrieve_args. While we're in
here, get rid of the useless s64_to_loff_t helper and just directly cast
the values.
Link: https://bugzilla.linux-nfs.org/show_bug.cgi?id=392
Reported-by: Jan Kasiak <[email protected]>
Signed-off-by: Jeff Layton <[email protected]>
---
fs/lockd/svc4proc.c | 3 +++
fs/lockd/xdr4.c | 26 ++++++++++++--------------
include/linux/lockd/xdr.h | 1 +
3 files changed, 16 insertions(+), 14 deletions(-)
diff --git a/fs/lockd/svc4proc.c b/fs/lockd/svc4proc.c
index 176b468a61c7..e781ac747961 100644
--- a/fs/lockd/svc4proc.c
+++ b/fs/lockd/svc4proc.c
@@ -32,6 +32,9 @@ nlm4svc_retrieve_args(struct svc_rqst *rqstp, struct nlm_args *argp,
if (!nlmsvc_ops)
return nlm_lck_denied_nolocks;
+ if (lock->overflow)
+ return nlm4_fbig;
+
/* Obtain host handle */
if (!(host = nlmsvc_lookup_host(rqstp, lock->caller, lock->len))
|| (argp->monitor && nsm_monitor(host) < 0))
diff --git a/fs/lockd/xdr4.c b/fs/lockd/xdr4.c
index 856267c0864b..fd5de5481a6f 100644
--- a/fs/lockd/xdr4.c
+++ b/fs/lockd/xdr4.c
@@ -20,13 +20,6 @@
#include "svcxdr.h"
-static inline loff_t
-s64_to_loff_t(__s64 offset)
-{
- return (loff_t)offset;
-}
-
-
static inline s64
loff_t_to_s64(loff_t offset)
{
@@ -71,7 +64,6 @@ svcxdr_decode_lock(struct xdr_stream *xdr, struct nlm_lock *lock)
{
struct file_lock *fl = &lock->fl;
u64 len, start;
- s64 end;
if (!svcxdr_decode_string(xdr, &lock->caller, &lock->len))
return false;
@@ -86,15 +78,21 @@ svcxdr_decode_lock(struct xdr_stream *xdr, struct nlm_lock *lock)
if (xdr_stream_decode_u64(xdr, &len) < 0)
return false;
+ /*
+ * We don't preserve the actual arguments in the call, so once
+ * the decoding is done, it's too late to detect overflow. Thus,
+ * we track it here and check for it later.
+ */
+ if (start > OFFSET_MAX || (len && ((len - 1) > (OFFSET_MAX - start)))) {
+ lock->overflow = true;
+ return true;
+ }
+
locks_init_lock(fl);
fl->fl_flags = FL_POSIX;
fl->fl_type = F_RDLCK;
- end = start + len - 1;
- fl->fl_start = s64_to_loff_t(start);
- if (len == 0 || end < 0)
- fl->fl_end = OFFSET_MAX;
- else
- fl->fl_end = s64_to_loff_t(end);
+ fl->fl_start = (loff_t)start;
+ fl->fl_end = len ? (loff_t)(start + len - 1) : OFFSET_MAX;
return true;
}
diff --git a/include/linux/lockd/xdr.h b/include/linux/lockd/xdr.h
index 398f70093cd3..a80112d18658 100644
--- a/include/linux/lockd/xdr.h
+++ b/include/linux/lockd/xdr.h
@@ -39,6 +39,7 @@ struct nlm_lock {
char * caller;
unsigned int len; /* length of "caller" */
struct nfs_fh fh;
+ bool overflow; /* lock range overflow */
struct xdr_netobj oh;
u32 svid;
struct file_lock fl;
--
2.37.1
> On Aug 1, 2022, at 1:18 PM, Jeff Layton <[email protected]> wrote:
>
> lockd doesn't currently vet the start and length in nlm4 requests like
> it should, and can end up requesting locks with arguments that overflow
> to the filesystem.
>
> The NLM4 protocol unsigned 64-bit arguments for both start and length,
> whereas struct file_lock tracks the start and end as loff_t values. By
> the time we get around to calling nlm4svc_retrieve_args, we've lost the
> information that would allow us to determine if there was an overflow.
>
> Track whether the arguments will overflow as a boolean in struct
> nlm_lock, and test for that in nlm4svc_retrieve_args. While we're in
> here, get rid of the useless s64_to_loff_t helper and just directly cast
> the values.
>
> Link: https://bugzilla.linux-nfs.org/show_bug.cgi?id=392
> Reported-by: Jan Kasiak <[email protected]>
> Signed-off-by: Jeff Layton <[email protected]>
Thanks for tracking this down, Jeff!
I've always felt that XDR decoders should be responsible only for
parsing wire data (syntax). Architecturally it feels to me that
the conversion from protocol to local data structures should be
handled in *_retrieve_args() (semantics).
What would you think of moving @start and @len to struct nlm_lock
and handling the conversion in nlm4svc_retrieve_args() ? One
benefit of that would be to make those raw values available for
BPF and systemtap, for instance. I'm guessing it would be about
the same number of lines changed.
> ---
> fs/lockd/svc4proc.c | 3 +++
> fs/lockd/xdr4.c | 26 ++++++++++++--------------
> include/linux/lockd/xdr.h | 1 +
> 3 files changed, 16 insertions(+), 14 deletions(-)
>
> diff --git a/fs/lockd/svc4proc.c b/fs/lockd/svc4proc.c
> index 176b468a61c7..e781ac747961 100644
> --- a/fs/lockd/svc4proc.c
> +++ b/fs/lockd/svc4proc.c
> @@ -32,6 +32,9 @@ nlm4svc_retrieve_args(struct svc_rqst *rqstp, struct nlm_args *argp,
> if (!nlmsvc_ops)
> return nlm_lck_denied_nolocks;
>
> + if (lock->overflow)
> + return nlm4_fbig;
> +
> /* Obtain host handle */
> if (!(host = nlmsvc_lookup_host(rqstp, lock->caller, lock->len))
> || (argp->monitor && nsm_monitor(host) < 0))
> diff --git a/fs/lockd/xdr4.c b/fs/lockd/xdr4.c
> index 856267c0864b..fd5de5481a6f 100644
> --- a/fs/lockd/xdr4.c
> +++ b/fs/lockd/xdr4.c
> @@ -20,13 +20,6 @@
>
> #include "svcxdr.h"
>
> -static inline loff_t
> -s64_to_loff_t(__s64 offset)
> -{
> - return (loff_t)offset;
> -}
> -
> -
> static inline s64
> loff_t_to_s64(loff_t offset)
> {
> @@ -71,7 +64,6 @@ svcxdr_decode_lock(struct xdr_stream *xdr, struct nlm_lock *lock)
> {
> struct file_lock *fl = &lock->fl;
> u64 len, start;
> - s64 end;
>
> if (!svcxdr_decode_string(xdr, &lock->caller, &lock->len))
> return false;
> @@ -86,15 +78,21 @@ svcxdr_decode_lock(struct xdr_stream *xdr, struct nlm_lock *lock)
> if (xdr_stream_decode_u64(xdr, &len) < 0)
> return false;
>
> + /*
> + * We don't preserve the actual arguments in the call, so once
> + * the decoding is done, it's too late to detect overflow. Thus,
> + * we track it here and check for it later.
> + */
> + if (start > OFFSET_MAX || (len && ((len - 1) > (OFFSET_MAX - start)))) {
> + lock->overflow = true;
> + return true;
> + }
> +
> locks_init_lock(fl);
> fl->fl_flags = FL_POSIX;
> fl->fl_type = F_RDLCK;
> - end = start + len - 1;
> - fl->fl_start = s64_to_loff_t(start);
> - if (len == 0 || end < 0)
> - fl->fl_end = OFFSET_MAX;
> - else
> - fl->fl_end = s64_to_loff_t(end);
> + fl->fl_start = (loff_t)start;
> + fl->fl_end = len ? (loff_t)(start + len - 1) : OFFSET_MAX;
>
> return true;
> }
> diff --git a/include/linux/lockd/xdr.h b/include/linux/lockd/xdr.h
> index 398f70093cd3..a80112d18658 100644
> --- a/include/linux/lockd/xdr.h
> +++ b/include/linux/lockd/xdr.h
> @@ -39,6 +39,7 @@ struct nlm_lock {
> char * caller;
> unsigned int len; /* length of "caller" */
> struct nfs_fh fh;
> + bool overflow; /* lock range overflow */
> struct xdr_netobj oh;
> u32 svid;
> struct file_lock fl;
> --
> 2.37.1
>
--
Chuck Lever