Message ID | 20250417152808.722409-1-mykyta.yatsenko5@gmail.com (mailing list archive) |
---|---|
State | New |
Headers | show |
Series | [mm] maccess: fix strncpy_from_user_nofault empty string handling | expand |
On Thu, 17 Apr 2025 16:28:08 +0100 Mykyta Yatsenko <mykyta.yatsenko5@gmail.com> wrote: > strncpy_from_user_nofault should return the length of the copied string > including the trailing NUL, but if the argument unsafe_addr points to > an empty string ({'\0'}), the return value is 0. > > This happens as strncpy_from_user copies terminal symbol into dst > and returns 0 (as expected), but strncpy_from_user_nofault does not > modify ret as it is not equal to count and not greater than 0, so 0 is > returned, which contradicts the contract. Looks right, I think. But why do strncpy_from_user() and strncpy_from_user_nofault() have different interfaces? /** * strncpy_from_user: - Copy a NUL terminated string from userspace. * ... * On success, returns the length of the string (not including the trailing * NUL). /** * strncpy_from_user_nofault: - Copy a NUL terminated string from unsafe user * address. * ... * On success, returns the length of the string INCLUDING the trailing NUL. This is surprising and I'm wondering what led us to do this?
On Thu, Apr 17, 2025 at 8:28 AM Mykyta Yatsenko <mykyta.yatsenko5@gmail.com> wrote: > > From: Mykyta Yatsenko <yatsenko@meta.com> > > strncpy_from_user_nofault should return the length of the copied string > including the trailing NUL, but if the argument unsafe_addr points to > an empty string ({'\0'}), the return value is 0. > > This happens as strncpy_from_user copies terminal symbol into dst > and returns 0 (as expected), but strncpy_from_user_nofault does not > modify ret as it is not equal to count and not greater than 0, so 0 is > returned, which contradicts the contract. > > Signed-off-by: Mykyta Yatsenko <yatsenko@meta.com> > --- > kernel/trace/trace_events_filter.c | 10 ++++++++-- > mm/maccess.c | 2 +- > 2 files changed, 9 insertions(+), 3 deletions(-) > > diff --git a/kernel/trace/trace_events_filter.c b/kernel/trace/trace_events_filter.c > index 0993dfc1c5c1..86b7e5a4e235 100644 > --- a/kernel/trace/trace_events_filter.c > +++ b/kernel/trace/trace_events_filter.c > @@ -800,6 +800,7 @@ static __always_inline char *test_string(char *str) > { > struct ustring_buffer *ubuf; > char *kstr; > + int cnt; > > if (!ustring_per_cpu) > return NULL; > @@ -808,7 +809,9 @@ static __always_inline char *test_string(char *str) > kstr = ubuf->buffer; > > /* For safety, do not trust the string pointer */ > - if (!strncpy_from_kernel_nofault(kstr, str, USTRING_BUF_SIZE)) > + cnt = strncpy_from_kernel_nofault(kstr, str, USTRING_BUF_SIZE); > + /* Return null if empty string or error */ > + if (cnt <= 1) > return NULL; I wouldn't touch this part and leave it up to Steven to fix (if he agrees it needs fixing). Current logic seems wrong already, as it won't correctly handle -EFAULT. And, on the other hand, there is nothing wrong or special about empty string, so I don't think it needs special handling. Let's drop these changes in trace_events_filter.c? > return kstr; > } > @@ -818,6 +821,7 @@ static __always_inline char *test_ustring(char *str) > struct ustring_buffer *ubuf; > char __user *ustr; > char *kstr; > + int cnt; > > if (!ustring_per_cpu) > return NULL; > @@ -827,7 +831,9 @@ static __always_inline char *test_ustring(char *str) > > /* user space address? */ > ustr = (char __user *)str; > - if (!strncpy_from_user_nofault(kstr, ustr, USTRING_BUF_SIZE)) > + cnt = strncpy_from_user_nofault(kstr, ustr, USTRING_BUF_SIZE); > + /* Return null if empty string or error */ > + if (cnt <= 1) > return NULL; ditto > > return kstr; > diff --git a/mm/maccess.c b/mm/maccess.c > index 8f0906180a94..831b4dd7296c 100644 > --- a/mm/maccess.c > +++ b/mm/maccess.c > @@ -196,7 +196,7 @@ long strncpy_from_user_nofault(char *dst, const void __user *unsafe_addr, > if (ret >= count) { > ret = count; > dst[ret - 1] = '\0'; > - } else if (ret > 0) { > + } else if (ret >= 0) { > ret++; > } > This part looks good and does indeed fix the issue. Good catch! Reviewed-by: Andrii Nakryiko <andrii@kernel.org> > -- > 2.49.0 >
On Thu, Apr 17, 2025 at 1:44 PM Andrii Nakryiko <andrii.nakryiko@gmail.com> wrote: > > On Thu, Apr 17, 2025 at 8:28 AM Mykyta Yatsenko > <mykyta.yatsenko5@gmail.com> wrote: > > > > From: Mykyta Yatsenko <yatsenko@meta.com> > > > > strncpy_from_user_nofault should return the length of the copied string > > including the trailing NUL, but if the argument unsafe_addr points to > > an empty string ({'\0'}), the return value is 0. > > > > This happens as strncpy_from_user copies terminal symbol into dst > > and returns 0 (as expected), but strncpy_from_user_nofault does not > > modify ret as it is not equal to count and not greater than 0, so 0 is > > returned, which contradicts the contract. > > > > Signed-off-by: Mykyta Yatsenko <yatsenko@meta.com> > > --- > > kernel/trace/trace_events_filter.c | 10 ++++++++-- > > mm/maccess.c | 2 +- > > 2 files changed, 9 insertions(+), 3 deletions(-) > > > > diff --git a/kernel/trace/trace_events_filter.c b/kernel/trace/trace_events_filter.c > > index 0993dfc1c5c1..86b7e5a4e235 100644 > > --- a/kernel/trace/trace_events_filter.c > > +++ b/kernel/trace/trace_events_filter.c > > @@ -800,6 +800,7 @@ static __always_inline char *test_string(char *str) > > { > > struct ustring_buffer *ubuf; > > char *kstr; > > + int cnt; > > > > if (!ustring_per_cpu) > > return NULL; > > @@ -808,7 +809,9 @@ static __always_inline char *test_string(char *str) > > kstr = ubuf->buffer; > > > > /* For safety, do not trust the string pointer */ > > - if (!strncpy_from_kernel_nofault(kstr, str, USTRING_BUF_SIZE)) > > + cnt = strncpy_from_kernel_nofault(kstr, str, USTRING_BUF_SIZE); > > + /* Return null if empty string or error */ > > + if (cnt <= 1) > > return NULL; > > I wouldn't touch this part and leave it up to Steven to fix (if he > agrees it needs fixing). Current logic seems wrong already, as it > won't correctly handle -EFAULT. And, on the other hand, there is > nothing wrong or special about empty string, so I don't think it needs > special handling. Let's drop these changes in trace_events_filter.c? Actually, you are not even touching strncpy_from_kernel_nofault(), so yeah, definitely let's not do this (at least not in this patch). > > > return kstr; > > } > > @@ -818,6 +821,7 @@ static __always_inline char *test_ustring(char *str) > > struct ustring_buffer *ubuf; > > char __user *ustr; > > char *kstr; > > + int cnt; > > > > if (!ustring_per_cpu) > > return NULL; > > @@ -827,7 +831,9 @@ static __always_inline char *test_ustring(char *str) > > > > /* user space address? */ > > ustr = (char __user *)str; > > - if (!strncpy_from_user_nofault(kstr, ustr, USTRING_BUF_SIZE)) > > + cnt = strncpy_from_user_nofault(kstr, ustr, USTRING_BUF_SIZE); > > + /* Return null if empty string or error */ > > + if (cnt <= 1) > > return NULL; > > ditto > > > > > return kstr; > > diff --git a/mm/maccess.c b/mm/maccess.c > > index 8f0906180a94..831b4dd7296c 100644 > > --- a/mm/maccess.c > > +++ b/mm/maccess.c > > @@ -196,7 +196,7 @@ long strncpy_from_user_nofault(char *dst, const void __user *unsafe_addr, > > if (ret >= count) { > > ret = count; > > dst[ret - 1] = '\0'; > > - } else if (ret > 0) { > > + } else if (ret >= 0) { > > ret++; > > } > > > > This part looks good and does indeed fix the issue. Good catch! > > Reviewed-by: Andrii Nakryiko <andrii@kernel.org> > > > -- > > 2.49.0 > >
On Thu, Apr 17, 2025 at 1:40 PM Andrew Morton <akpm@linux-foundation.org> wrote: > > On Thu, 17 Apr 2025 16:28:08 +0100 Mykyta Yatsenko <mykyta.yatsenko5@gmail.com> wrote: > > > strncpy_from_user_nofault should return the length of the copied string > > including the trailing NUL, but if the argument unsafe_addr points to > > an empty string ({'\0'}), the return value is 0. > > > > This happens as strncpy_from_user copies terminal symbol into dst > > and returns 0 (as expected), but strncpy_from_user_nofault does not > > modify ret as it is not equal to count and not greater than 0, so 0 is > > returned, which contradicts the contract. > > Looks right, I think. > > But why do strncpy_from_user() and strncpy_from_user_nofault() have > different interfaces? > > /** > * strncpy_from_user: - Copy a NUL terminated string from userspace. > * ... > * On success, returns the length of the string (not including the trailing > * NUL). > > /** > * strncpy_from_user_nofault: - Copy a NUL terminated string from unsafe user > * address. > * ... > * On success, returns the length of the string INCLUDING the trailing NUL. > > This is surprising and I'm wondering what led us to do this? Agreed, this is very surprising and error-prone. strncpy_from_user() semantics is a bit better, IMO, in that it allows to "detect" empty string even if buffer size is 1 byte. And there isn't a lot of places where we use strncpy_from_user_nofault (only 6, it seems). Maybe we should just change the semantics of strncpy_from_user_nofault?
On Thu, 17 Apr 2025 13:44:48 -0700 Andrii Nakryiko <andrii.nakryiko@gmail.com> wrote: > > @@ -808,7 +809,9 @@ static __always_inline char *test_string(char *str) > > kstr = ubuf->buffer; > > > > /* For safety, do not trust the string pointer */ > > - if (!strncpy_from_kernel_nofault(kstr, str, USTRING_BUF_SIZE)) > > + cnt = strncpy_from_kernel_nofault(kstr, str, USTRING_BUF_SIZE); > > + /* Return null if empty string or error */ > > + if (cnt <= 1) > > return NULL; > > I wouldn't touch this part and leave it up to Steven to fix (if he > agrees it needs fixing). Current logic seems wrong already, as it > won't correctly handle -EFAULT. And, on the other hand, there is > nothing wrong or special about empty string, so I don't think it needs > special handling. Let's drop these changes in trace_events_filter.c? Bah, it is wrong. I don't usually use filtering on strings much, but come to think of it, the last time I tried, it didn't work, but I found another way to get what I was looking for, and didn't look deeper into it. I only care if it faulted or not. I don't care about it just copying zero bytes. It should have been: if (strncpy_from_kernel_nofault(kstr, str, USTRING_BUF_SIZE) < 0) > > > return kstr; > > } > > @@ -818,6 +821,7 @@ static __always_inline char *test_ustring(char *str) > > struct ustring_buffer *ubuf; > > char __user *ustr; > > char *kstr; > > + int cnt; > > > > if (!ustring_per_cpu) > > return NULL; > > @@ -827,7 +831,9 @@ static __always_inline char *test_ustring(char *str) > > > > /* user space address? */ > > ustr = (char __user *)str; > > - if (!strncpy_from_user_nofault(kstr, ustr, USTRING_BUF_SIZE)) This is broken too. As this isn't relying on the other change in this patch, I'll just fix it myself. I'm getting a pull request ready anyway. Thanks! -- Steve > > + cnt = strncpy_from_user_nofault(kstr, ustr, USTRING_BUF_SIZE); > > + /* Return null if empty string or error */ > > + if (cnt <= 1) > > return NULL; > > ditto > > >
diff --git a/kernel/trace/trace_events_filter.c b/kernel/trace/trace_events_filter.c index 0993dfc1c5c1..86b7e5a4e235 100644 --- a/kernel/trace/trace_events_filter.c +++ b/kernel/trace/trace_events_filter.c @@ -800,6 +800,7 @@ static __always_inline char *test_string(char *str) { struct ustring_buffer *ubuf; char *kstr; + int cnt; if (!ustring_per_cpu) return NULL; @@ -808,7 +809,9 @@ static __always_inline char *test_string(char *str) kstr = ubuf->buffer; /* For safety, do not trust the string pointer */ - if (!strncpy_from_kernel_nofault(kstr, str, USTRING_BUF_SIZE)) + cnt = strncpy_from_kernel_nofault(kstr, str, USTRING_BUF_SIZE); + /* Return null if empty string or error */ + if (cnt <= 1) return NULL; return kstr; } @@ -818,6 +821,7 @@ static __always_inline char *test_ustring(char *str) struct ustring_buffer *ubuf; char __user *ustr; char *kstr; + int cnt; if (!ustring_per_cpu) return NULL; @@ -827,7 +831,9 @@ static __always_inline char *test_ustring(char *str) /* user space address? */ ustr = (char __user *)str; - if (!strncpy_from_user_nofault(kstr, ustr, USTRING_BUF_SIZE)) + cnt = strncpy_from_user_nofault(kstr, ustr, USTRING_BUF_SIZE); + /* Return null if empty string or error */ + if (cnt <= 1) return NULL; return kstr; diff --git a/mm/maccess.c b/mm/maccess.c index 8f0906180a94..831b4dd7296c 100644 --- a/mm/maccess.c +++ b/mm/maccess.c @@ -196,7 +196,7 @@ long strncpy_from_user_nofault(char *dst, const void __user *unsafe_addr, if (ret >= count) { ret = count; dst[ret - 1] = '\0'; - } else if (ret > 0) { + } else if (ret >= 0) { ret++; }