[PATCH] kasan: add KUnit tests for __counted_by and __counted_by_ptr

3 views
Skip to first unread message

Bill Wendling

unread,
Sep 28, 2026, 7:17:42 PM (10 days ago) Sep 28
to Andrey Ryabinin, Andrew Morton, Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov, Vincenzo Frascino, Kees Cook, Gustavo A. R. Silva, kasa...@googlegroups.com, linu...@kvack.org, linux-...@vger.kernel.org, linux-h...@vger.kernel.org, thomas.w...@linutronix.de, Bill Wendling
The '__counted_by' and '__counted_by_ptr' attributes associate a
flexible array member or pointer member with a struct field that holds
its element count. Supporting compilers use these annotations to
compute dynamic object sizes via '__builtin_dynamic_object_size()' and
perform runtime bounds checking with KASAN.

Add KUnit tests ('counted_by_flex_oob_access' and
'counted_by_ptr_oob_access', guarded by CONFIG_CC_HAS_COUNTED_BY and
CONFIG_CC_HAS_COUNTED_BY_PTR respectively) to verify that:

- '__builtin_dynamic_object_size()' returns the expected byte size for
annotated flexible array and pointer members.
- KASAN detects out-of-bounds read and write accesses beyond the
annotated count.

Allocate the test structures in 'noinline' helpers and hide the
returned pointers with OPTIMIZER_HIDE_VAR() so allocation-size
attributes and compiler optimizations do not mask the '__counted_by'
and '__counted_by_ptr' checks.

Signed-off-by: Bill Wendling <mo...@google.com>
---
mm/kasan/kasan_test_c.c | 100 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 100 insertions(+)

diff --git a/mm/kasan/kasan_test_c.c b/mm/kasan/kasan_test_c.c
index b9e167ed5be3..f481183c84f1 100644
--- a/mm/kasan/kasan_test_c.c
+++ b/mm/kasan/kasan_test_c.c
@@ -2201,6 +2201,100 @@ static void copy_user_test_oob(struct kunit *test)
unused = strncpy_from_user(kmem, usermem, size + 1));
}

+#ifdef CONFIG_CC_HAS_COUNTED_BY
+struct counted_by_flex_struct {
+ size_t size;
+ int array[] __counted_by(size);
+};
+
+/*
+ * Allocate the struct out-of-line to prevent inherent attributes from
+ * affecting the '__builtin_dynamic_object_size' check.
+ */
+static noinline struct counted_by_flex_struct *
+alloc_counted_by_flex_struct(struct kunit *test, size_t size)
+{
+ struct counted_by_flex_struct *s;
+
+ s = kzalloc(sizeof(struct counted_by_flex_struct) +
+ size * sizeof(s->array[0]), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, s);
+
+ s->size = size;
+ return s;
+}
+
+static void counted_by_flex_oob_access(struct kunit *test)
+{
+ size_t size = 128;
+ struct counted_by_flex_struct *s;
+
+ s = alloc_counted_by_flex_struct(test, size);
+
+ OPTIMIZER_HIDE_VAR(s);
+
+ /* __builtin_dynamic_object_size() should return the correct length. */
+ KUNIT_EXPECT_EQ(test, size * sizeof(s->array[0]),
+ __builtin_dynamic_object_size(s->array, 0));
+
+ /* Out-of-bounds assignment. */
+ KUNIT_EXPECT_KASAN_FAIL(test, s->array[size + 1] = 42);
+
+ /* Out-of-bounds read. */
+ KUNIT_EXPECT_KASAN_FAIL_READ(test, s->array[0] = s->array[size + 13]);
+
+ kfree(s);
+}
+
+#ifdef CONFIG_CC_HAS_COUNTED_BY_PTR
+struct counted_by_ptr_struct {
+ char *ptr __counted_by_ptr(size);
+ size_t size;
+};
+
+/*
+ * Allocate the struct out-of-line to prevent inherent attributes from
+ * affecting the '__builtin_dynamic_object_size' check.
+ */
+static noinline struct counted_by_ptr_struct *
+alloc_counted_by_ptr_struct(struct kunit *test, size_t size)
+{
+ struct counted_by_ptr_struct *s;
+
+ s = kmalloc_obj(struct counted_by_ptr_struct);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, s);
+
+ s->size = size;
+ s->ptr = kzalloc(size, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, s->ptr);
+
+ return s;
+}
+
+static void counted_by_ptr_oob_access(struct kunit *test)
+{
+ size_t size = 128;
+ struct counted_by_ptr_struct *s;
+
+ s = alloc_counted_by_ptr_struct(test, size);
+
+ OPTIMIZER_HIDE_VAR(s);
+
+ /* __builtin_dynamic_object_size() should return the correct length. */
+ KUNIT_EXPECT_EQ(test, size, __builtin_dynamic_object_size(s->ptr, 0));
+
+ /* Out-of-bounds assignment. */
+ KUNIT_EXPECT_KASAN_FAIL(test, s->ptr[size + 1] = 42);
+
+ /* Out-of-bounds read. */
+ KUNIT_EXPECT_KASAN_FAIL_READ(test, s->ptr[0] = s->ptr[size + 13]);
+
+ kfree(s->ptr);
+ kfree(s);
+}
+#endif /* CONFIG_CC_HAS_COUNTED_BY_PTR */
+#endif /* CONFIG_CC_HAS_COUNTED_BY */
+
static struct kunit_case kasan_kunit_test_cases[] = {
KUNIT_CASE(kmalloc_oob_right),
KUNIT_CASE(kmalloc_oob_left),
@@ -2280,6 +2374,12 @@ static struct kunit_case kasan_kunit_test_cases[] = {
#endif
KUNIT_CASE(rust_uaf),
KUNIT_CASE(copy_user_test_oob),
+#ifdef CONFIG_CC_HAS_COUNTED_BY
+ KUNIT_CASE(counted_by_flex_oob_access),
+#ifdef CONFIG_CC_HAS_COUNTED_BY_PTR
+ KUNIT_CASE(counted_by_ptr_oob_access),
+#endif /* CONFIG_CC_HAS_COUNTED_BY_PTR */
+#endif /* CONFIG_CC_HAS_COUNTED_BY */
{}
};

--
2.56.0.rc1.315.gc6ed9934b7-goog

Andrew Morton

unread,
Sep 28, 2026, 7:42:40 PM (10 days ago) Sep 28
to Bill Wendling, Andrey Ryabinin, Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov, Vincenzo Frascino, Kees Cook, Gustavo A. R. Silva, kasa...@googlegroups.com, linu...@kvack.org, linux-...@vger.kernel.org, linux-h...@vger.kernel.org, thomas.w...@linutronix.de
On Mon, 28 Sep 2026 23:17:37 +0000 Bill Wendling <mo...@google.com> wrote:

> The '__counted_by' and '__counted_by_ptr' attributes associate a
> flexible array member or pointer member with a struct field that holds
> its element count. Supporting compilers use these annotations to
> compute dynamic object sizes via '__builtin_dynamic_object_size()' and
> perform runtime bounds checking with KASAN.
>
> Add KUnit tests ('counted_by_flex_oob_access' and
> 'counted_by_ptr_oob_access', guarded by CONFIG_CC_HAS_COUNTED_BY and
> CONFIG_CC_HAS_COUNTED_BY_PTR respectively) to verify that:
>
> - '__builtin_dynamic_object_size()' returns the expected byte size for
> annotated flexible array and pointer members.
> - KASAN detects out-of-bounds read and write accesses beyond the
> annotated count.
>
> Allocate the test structures in 'noinline' helpers and hide the
> returned pointers with OPTIMIZER_HIDE_VAR() so allocation-size
> attributes and compiler optimizations do not mask the '__counted_by'
> and '__counted_by_ptr' checks.

Thanks. Are any of Sashiko's comments pertinent?
https://sashiko.dev/#/patchset/20260928231737....@google.com

Bill Wendling

unread,
Sep 28, 2026, 8:59:55 PM (10 days ago) Sep 28
to Andrew Morton, Andrey Ryabinin, Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov, Vincenzo Frascino, Kees Cook, Gustavo A. R. Silva, kasa...@googlegroups.com, linu...@kvack.org, linux-...@vger.kernel.org, linux-h...@vger.kernel.org, thomas.w...@linutronix.de
Yes. I found a better place to put these tests. I'll send a v2.

-bw

Bill Wendling

unread,
Sep 28, 2026, 9:00:36 PM (10 days ago) Sep 28
to Andrey Ryabinin, Andrew Morton, Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov, Vincenzo Frascino, Kees Cook, Gustavo A. R. Silva, kasa...@googlegroups.com, linu...@kvack.org, linux-...@vger.kernel.org, linux-h...@vger.kernel.org, thomas.w...@linutronix.de, Bill Wendling
The '__counted_by' and '__counted_by_ptr' attributes associate a
flexible array member or pointer member with a struct field that holds
its element count. Supporting compilers use these annotations to
compute dynamic object sizes via '__builtin_dynamic_object_size()' for
runtime bounds checking with CONFIG_FORTIFY_SOURCE.

Add KUnit tests ('fortify_test_counted_by_flex' and
'fortify_test_counted_by_ptr', guarded by CONFIG_CC_HAS_COUNTED_BY and
CONFIG_CC_HAS_COUNTED_BY_PTR respectively) to verify that:

- '__builtin_dynamic_object_size()' (both types 0 and 1) returns the
expected logical byte size for annotated flexible array and pointer
members.
- Fortified operations ('memset()' and 'memchr()') succeed within the
logical bounds and detect out-of-bounds read and write accesses
beyond the annotated count.

Allocate the test buffers with extra physical capacity (2 * size) in
'noinline' helpers and hide the returned pointers with
OPTIMIZER_HIDE_VAR() so allocation-size attributes, physical slab
bounds, and compiler optimizations do not mask the '__counted_by' and
'__counted_by_ptr' checks.

Signed-off-by: Bill Wendling <mo...@google.com>
---
v2: Move tests to the 'fortify' KUnit tests. It uses UBSAN, which is
what gets triggered by 'counted_by'.
---
lib/tests/fortify_kunit.c | 119 ++++++++++++++++++++++++++++++++++++++
1 file changed, 119 insertions(+)

diff --git a/lib/tests/fortify_kunit.c b/lib/tests/fortify_kunit.c
index 413cdbf3dc0d..2335171f84d8 100644
--- a/lib/tests/fortify_kunit.c
+++ b/lib/tests/fortify_kunit.c
@@ -1011,6 +1011,119 @@ static void fortify_test_kmemdup(struct kunit *test)
kfree(copy);
}

+#ifdef CONFIG_CC_HAS_COUNTED_BY
+struct counted_by_flex_struct {
+ size_t size;
+ int array[] __counted_by(size);
+};
+
+/*
+ * Allocate the struct out-of-line with extra physical capacity so that
+ * __alloc_size() and physical slab bounds do not mask the __counted_by()
+ * logical bounds check.
+ */
+static noinline struct counted_by_flex_struct *
+alloc_counted_by_flex_struct(struct kunit *test, size_t size)
+{
+ struct counted_by_flex_struct *s;
+
+ s = kzalloc(sizeof(*s) + 2 * size * sizeof(s->array[0]), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, s);
+
+ s->size = size;
+ return s;
+}
+
+static void fortify_test_counted_by_flex(struct kunit *test)
+{
+ size_t size = 128;
+ struct counted_by_flex_struct *s;
+ size_t elem_bytes = size * sizeof(s->array[0]);
+
+ s = alloc_counted_by_flex_struct(test, size);
+
+ OPTIMIZER_HIDE_VAR(s);
+ OPTIMIZER_HIDE_VAR(elem_bytes);
+
+ /* __builtin_dynamic_object_size() should return the logical length. */
+ KUNIT_EXPECT_EQ(test, elem_bytes,
+ __builtin_dynamic_object_size(s->array, 0));
+ KUNIT_EXPECT_EQ(test, elem_bytes,
+ __builtin_dynamic_object_size(s->array, 1));
+
+ /* Within-bounds write and read succeed. */
+ memset(s->array, 0x42, elem_bytes);
+ KUNIT_EXPECT_EQ(test, fortify_write_overflows, 0);
+ KUNIT_EXPECT_NOT_NULL(test, memchr(s->array, 0x42, elem_bytes));
+ KUNIT_EXPECT_EQ(test, fortify_read_overflows, 0);
+
+ /* Out-of-bounds write and read past logical size are caught. */
+ memset(s->array, 0x42, elem_bytes + 1);
+ KUNIT_EXPECT_EQ(test, fortify_write_overflows, 1);
+ KUNIT_EXPECT_NULL(test, memchr(s->array, 0x42, elem_bytes + 1));
+ KUNIT_EXPECT_EQ(test, fortify_read_overflows, 1);
+
+ kfree(s);
+}
+
+#ifdef CONFIG_CC_HAS_COUNTED_BY_PTR
+struct counted_by_ptr_struct {
+ char *ptr __counted_by_ptr(size);
+ size_t size;
+};
+
+/*
+ * Allocate the struct out-of-line with extra physical capacity so that
+ * __alloc_size() and physical slab bounds do not mask the __counted_by_ptr()
+ * logical bounds check.
+ */
+static noinline struct counted_by_ptr_struct *
+alloc_counted_by_ptr_struct(struct kunit *test, size_t size)
+{
+ struct counted_by_ptr_struct *s;
+
+ s = kmalloc_obj(struct counted_by_ptr_struct);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, s);
+
+ s->size = size;
+ s->ptr = kzalloc(2 * size, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, s->ptr);
+
+ return s;
+}
+
+static void fortify_test_counted_by_ptr(struct kunit *test)
+{
+ size_t size = 128;
+ struct counted_by_ptr_struct *s;
+
+ s = alloc_counted_by_ptr_struct(test, size);
+
+ OPTIMIZER_HIDE_VAR(s);
+ OPTIMIZER_HIDE_VAR(size);
+
+ /* __builtin_dynamic_object_size() should return the logical length. */
+ KUNIT_EXPECT_EQ(test, size, __builtin_dynamic_object_size(s->ptr, 0));
+ KUNIT_EXPECT_EQ(test, size, __builtin_dynamic_object_size(s->ptr, 1));
+
+ /* Within-bounds write and read succeed. */
+ memset(s->ptr, 0x42, size);
+ KUNIT_EXPECT_EQ(test, fortify_write_overflows, 0);
+ KUNIT_EXPECT_NOT_NULL(test, memchr(s->ptr, 0x42, size));
+ KUNIT_EXPECT_EQ(test, fortify_read_overflows, 0);
+
+ /* Out-of-bounds write and read past logical size are caught. */
+ memset(s->ptr, 0x42, size + 1);
+ KUNIT_EXPECT_EQ(test, fortify_write_overflows, 1);
+ KUNIT_EXPECT_NULL(test, memchr(s->ptr, 0x42, size + 1));
+ KUNIT_EXPECT_EQ(test, fortify_read_overflows, 1);
+
+ kfree(s->ptr);
+ kfree(s);
+}
+#endif /* CONFIG_CC_HAS_COUNTED_BY_PTR */
+#endif /* CONFIG_CC_HAS_COUNTED_BY */
+
static int fortify_test_init(struct kunit *test)
{
if (!IS_ENABLED(CONFIG_FORTIFY_SOURCE))
@@ -1054,6 +1167,12 @@ static struct kunit_case fortify_test_cases[] = {
KUNIT_CASE(fortify_test_memchr_inv),
KUNIT_CASE(fortify_test_memcmp),
KUNIT_CASE(fortify_test_kmemdup),
+#ifdef CONFIG_CC_HAS_COUNTED_BY
+ KUNIT_CASE(fortify_test_counted_by_flex),
+#ifdef CONFIG_CC_HAS_COUNTED_BY_PTR
+ KUNIT_CASE(fortify_test_counted_by_ptr),

Thomas Weißschuh

unread,
Sep 29, 2026, 2:23:06 AM (9 days ago) Sep 29
to Bill Wendling, Andrey Ryabinin, Andrew Morton, Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov, Vincenzo Frascino, Kees Cook, Gustavo A. R. Silva, kasa...@googlegroups.com, linu...@kvack.org, linux-...@vger.kernel.org, linux-h...@vger.kernel.org
Hi Bill,

this looks much better. Thanks for the rework.

On Tue, Sep 29, 2026 at 01:00:31AM +0000, Bill Wendling wrote:
> The '__counted_by' and '__counted_by_ptr' attributes associate a
> flexible array member or pointer member with a struct field that holds
> its element count. Supporting compilers use these annotations to
> compute dynamic object sizes via '__builtin_dynamic_object_size()' for
> runtime bounds checking with CONFIG_FORTIFY_SOURCE.
>
> Add KUnit tests ('fortify_test_counted_by_flex' and
> 'fortify_test_counted_by_ptr', guarded by CONFIG_CC_HAS_COUNTED_BY and
> CONFIG_CC_HAS_COUNTED_BY_PTR respectively) to verify that:
>
> - '__builtin_dynamic_object_size()' (both types 0 and 1) returns the
> expected logical byte size for annotated flexible array and pointer
> members.
> - Fortified operations ('memset()' and 'memchr()') succeed within the
> logical bounds and detect out-of-bounds read and write accesses
> beyond the annotated count.
>
> Allocate the test buffers with extra physical capacity (2 * size) in
> 'noinline' helpers and hide the returned pointers with
> OPTIMIZER_HIDE_VAR() so allocation-size attributes, physical slab
> bounds, and compiler optimizations do not mask the '__counted_by' and
> '__counted_by_ptr' checks.
>
> Signed-off-by: Bill Wendling <mo...@google.com>
> ---
> v2: Move tests to the 'fortify' KUnit tests. It uses UBSAN, which is
> what gets triggered by 'counted_by'.

v2 is missing in subject.

> ---
> lib/tests/fortify_kunit.c | 119 ++++++++++++++++++++++++++++++++++++++
> 1 file changed, 119 insertions(+)
>
> diff --git a/lib/tests/fortify_kunit.c b/lib/tests/fortify_kunit.c
> index 413cdbf3dc0d..2335171f84d8 100644
> --- a/lib/tests/fortify_kunit.c
> +++ b/lib/tests/fortify_kunit.c
> @@ -1011,6 +1011,119 @@ static void fortify_test_kmemdup(struct kunit *test)
> kfree(copy);
> }
>
> +#ifdef CONFIG_CC_HAS_COUNTED_BY

The ugly ifdeffery can be replaced by IS_ENABLED():

if (!IS_ENABLED(CONFIG_FOO))
kunit_skip(test, "Not built with CONFIG_FOO=y");

It makes the code cleaner and gives some useful feedback at runtime.

> +struct counted_by_flex_struct {
> + size_t size;
> + int array[] __counted_by(size);
> +};
> +
> +/*
> + * Allocate the struct out-of-line with extra physical capacity so that
> + * __alloc_size() and physical slab bounds do not mask the __counted_by()
> + * logical bounds check.
> + */
> +static noinline struct counted_by_flex_struct *
> +alloc_counted_by_flex_struct(struct kunit *test, size_t size)
> +{
> + struct counted_by_flex_struct *s;
> +
> + s = kzalloc(sizeof(*s) + 2 * size * sizeof(s->array[0]), GFP_KERNEL);

kunit_kzalloc() to automatically free the allocation again.
struct_size() for the size calculation.
In the other structure the arguments where swapped, intentional?

> +
> +/*
> + * Allocate the struct out-of-line with extra physical capacity so that
> + * __alloc_size() and physical slab bounds do not mask the __counted_by_ptr()
> + * logical bounds check.
> + */
> +static noinline struct counted_by_ptr_struct *
> +alloc_counted_by_ptr_struct(struct kunit *test, size_t size)
> +{
> + struct counted_by_ptr_struct *s;
> +
> + s = kmalloc_obj(struct counted_by_ptr_struct);

We should probably also get kunit_kmalloc_obj() at some point.

> + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, s);
> +
> + s->size = size;
> + s->ptr = kzalloc(2 * size, GFP_KERNEL);
> + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, s->ptr);
> +
> + return s;
> +}
> +
> +static void fortify_test_counted_by_ptr(struct kunit *test)
> +{
> + size_t size = 128;
> + struct counted_by_ptr_struct *s;
> +
> + s = alloc_counted_by_ptr_struct(test, size);
> +
> + OPTIMIZER_HIDE_VAR(s);
> + OPTIMIZER_HIDE_VAR(size);
> +
> + /* __builtin_dynamic_object_size() should return the logical length. */
> + KUNIT_EXPECT_EQ(test, size, __builtin_dynamic_object_size(s->ptr, 0));
> + KUNIT_EXPECT_EQ(test, size, __builtin_dynamic_object_size(s->ptr, 1));

Is this builtin guaranteed to be available?
We have a wrapper KUNIT_EXPECT_BDOS() above.
The nesting of these conditionals looks unnecessary.

Bill Wendling

unread,
Sep 29, 2026, 2:53:27 AM (9 days ago) Sep 29
to Thomas Weißschuh, Andrey Ryabinin, Andrew Morton, Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov, Vincenzo Frascino, Kees Cook, Gustavo A. R. Silva, kasa...@googlegroups.com, linu...@kvack.org, linux-...@vger.kernel.org, linux-h...@vger.kernel.org
Hi Thomas,
Doh!

> > ---
> > lib/tests/fortify_kunit.c | 119 ++++++++++++++++++++++++++++++++++++++
> > 1 file changed, 119 insertions(+)
> >
> > diff --git a/lib/tests/fortify_kunit.c b/lib/tests/fortify_kunit.c
> > index 413cdbf3dc0d..2335171f84d8 100644
> > --- a/lib/tests/fortify_kunit.c
> > +++ b/lib/tests/fortify_kunit.c
> > @@ -1011,6 +1011,119 @@ static void fortify_test_kmemdup(struct kunit *test)
> > kfree(copy);
> > }
> >
> > +#ifdef CONFIG_CC_HAS_COUNTED_BY
>
> The ugly ifdeffery can be replaced by IS_ENABLED():
>
> if (!IS_ENABLED(CONFIG_FOO))
> kunit_skip(test, "Not built with CONFIG_FOO=y");
>
> It makes the code cleaner and gives some useful feedback at runtime.
>
Ah yes! This is much nicer. It'll also fix up the #ifdef stuff at the
end as well.

> > +struct counted_by_flex_struct {
> > + size_t size;
> > + int array[] __counted_by(size);
> > +};
> > +
> > +/*
> > + * Allocate the struct out-of-line with extra physical capacity so that
> > + * __alloc_size() and physical slab bounds do not mask the __counted_by()
> > + * logical bounds check.
> > + */
> > +static noinline struct counted_by_flex_struct *
> > +alloc_counted_by_flex_struct(struct kunit *test, size_t size)
> > +{
> > + struct counted_by_flex_struct *s;
> > +
> > + s = kzalloc(sizeof(*s) + 2 * size * sizeof(s->array[0]), GFP_KERNEL);
>
> kunit_kzalloc() to automatically free the allocation again.
> struct_size() for the size calculation.
>
Oh cool! done.
Yes. It's a minor test to make sure that the attribute can refer to a
field that's defined after the pointer.
I suppose not. I'll use the macros instead.
-bw

Bill Wendling

unread,
Sep 29, 2026, 3:20:20 AM (9 days ago) Sep 29
to Andrey Ryabinin, Andrew Morton, Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov, Vincenzo Frascino, Kees Cook, Gustavo A. R. Silva, kasa...@googlegroups.com, linu...@kvack.org, linux-...@vger.kernel.org, linux-h...@vger.kernel.org, thomas.w...@linutronix.de, Bill Wendling
The '__counted_by' and '__counted_by_ptr' attributes associate a
flexible array member or pointer member with a struct field that holds
its element count. Supporting compilers use these annotations to
compute dynamic object sizes via '__builtin_dynamic_object_size()' for
runtime bounds checking with CONFIG_FORTIFY_SOURCE.

Add KUnit tests ('fortify_test_counted_by_flex' and
'fortify_test_counted_by_ptr', guarded by CONFIG_CC_HAS_COUNTED_BY and
CONFIG_CC_HAS_COUNTED_BY_PTR respectively) to verify that:

- '__builtin_dynamic_object_size()', if available, returns the expected
logical byte size for annotated flexible array and pointer members.
- Fortified operations ('memset()' and 'memchr()') succeed within the
logical bounds and detect out-of-bounds read and write accesses
beyond the annotated count.

Allocate the test buffers with extra physical capacity (2 * size) in
'noinline' helpers and hide the returned pointers with
OPTIMIZER_HIDE_VAR() so allocation-size attributes, physical slab
bounds, and compiler optimizations do not mask the '__counted_by' and
'__counted_by_ptr' checks.

Signed-off-by: Bill Wendling <mo...@google.com>
---
v3: - Use "IS_ENABLED(CONFIG...)" instead of "#ifdefs". It's a lot cleaner
and documents better when skipped.
- Use "kunit_kzalloc" and "struct_size" for the flexible array member.
- Use KUNIT_EXPECT_BDOS which skips the test if BDOS isn't available.
v2: - Move tests to the 'fortify' KUnit tests. It uses UBSAN, which is
what gets triggered by 'counted_by'.
---
lib/tests/fortify_kunit.c | 114 ++++++++++++++++++++++++++++++++++++++
1 file changed, 114 insertions(+)

diff --git a/lib/tests/fortify_kunit.c b/lib/tests/fortify_kunit.c
index 413cdbf3dc0d..8baf6dc96bab 100644
--- a/lib/tests/fortify_kunit.c
+++ b/lib/tests/fortify_kunit.c
@@ -1011,6 +1011,118 @@ static void fortify_test_kmemdup(struct kunit *test)
kfree(copy);
}

+struct counted_by_flex_struct {
+ size_t size;
+ int array[] __counted_by(size);
+};
+
+/*
+ * Allocate the struct out-of-line with extra physical capacity so that
+ * __alloc_size() and physical slab bounds do not mask the __counted_by()
+ * logical bounds check.
+ */
+static noinline struct counted_by_flex_struct *
+alloc_counted_by_flex_struct(struct kunit *test, size_t size)
+{
+ struct counted_by_flex_struct *s;
+
+ s = kunit_kzalloc(test, struct_size(s, array, 2 * size), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, s);
+
+ /* Intentionally fake the size so that we can trigger a trap. */
+ s->size = size;
+ return s;
+}
+
+static void fortify_test_counted_by_flex(struct kunit *test)
+{
+ size_t size = 128;
+ struct counted_by_flex_struct *s;
+ size_t elem_bytes = size * sizeof(s->array[0]);
+
+ if (!IS_ENABLED(CONFIG_CC_HAS_COUNTED_BY))
+ kunit_skip(test, "requires CONFIG_CC_HAS_COUNTED_BY");
+
+ s = alloc_counted_by_flex_struct(test, size);
+
+ OPTIMIZER_HIDE_VAR(s);
+ OPTIMIZER_HIDE_VAR(elem_bytes);
+
+ /* __builtin_dynamic_object_size() should return the logical length. */
+ KUNIT_EXPECT_BDOS(test, s->array, elem_bytes,
+ "struct counted_by_flex_struct");
+
+ /* Within-bounds write and read succeed. */
+ memset(s->array, 0x42, elem_bytes);
+ KUNIT_EXPECT_EQ(test, fortify_write_overflows, 0);
+ KUNIT_EXPECT_NOT_NULL(test, memchr(s->array, 0x42, elem_bytes));
+ KUNIT_EXPECT_EQ(test, fortify_read_overflows, 0);
+
+ /* Out-of-bounds write and read past logical size are caught. */
+ memset(s->array, 0x42, elem_bytes + 1);
+ KUNIT_EXPECT_EQ(test, fortify_write_overflows, 1);
+ KUNIT_EXPECT_NULL(test, memchr(s->array, 0x42, elem_bytes + 1));
+ KUNIT_EXPECT_EQ(test, fortify_read_overflows, 1);
+}
+
+struct counted_by_ptr_struct {
+ char *ptr __counted_by_ptr(size);
+ size_t size;
+};
+
+/*
+ * Allocate the struct out-of-line with extra physical capacity so that
+ * __alloc_size() and physical slab bounds do not mask the __counted_by_ptr()
+ * logical bounds check.
+ */
+static noinline struct counted_by_ptr_struct *
+alloc_counted_by_ptr_struct(struct kunit *test, size_t size)
+{
+ struct counted_by_ptr_struct *s;
+
+ s = kmalloc_obj(struct counted_by_ptr_struct);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, s);
+
+ /* Intentionally fake the size so that we can trigger a trap. */
+ s->size = size;
+ s->ptr = kzalloc(2 * size, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, s->ptr);
+
+ return s;
+}
+
+static void fortify_test_counted_by_ptr(struct kunit *test)
+{
+ size_t size = 128;
+ struct counted_by_ptr_struct *s;
+
+ if (!IS_ENABLED(CONFIG_CC_HAS_COUNTED_BY_PTR))
+ kunit_skip(test, "requires CONFIG_CC_HAS_COUNTED_BY_PTR");
+
+ s = alloc_counted_by_ptr_struct(test, size);
+
+ OPTIMIZER_HIDE_VAR(s);
+ OPTIMIZER_HIDE_VAR(size);
+
+ /* __builtin_dynamic_object_size() should return the logical length. */
+ KUNIT_EXPECT_BDOS(test, s->ptr, size, "struct counted_by_ptr_struct");
+
+ /* Within-bounds write and read succeed. */
+ memset(s->ptr, 0x42, size);
+ KUNIT_EXPECT_EQ(test, fortify_write_overflows, 0);
+ KUNIT_EXPECT_NOT_NULL(test, memchr(s->ptr, 0x42, size));
+ KUNIT_EXPECT_EQ(test, fortify_read_overflows, 0);
+
+ /* Out-of-bounds write and read past logical size are caught. */
+ memset(s->ptr, 0x42, size + 1);
+ KUNIT_EXPECT_EQ(test, fortify_write_overflows, 1);
+ KUNIT_EXPECT_NULL(test, memchr(s->ptr, 0x42, size + 1));
+ KUNIT_EXPECT_EQ(test, fortify_read_overflows, 1);
+
+ kfree(s->ptr);
+ kfree(s);
+}
+
static int fortify_test_init(struct kunit *test)
{
if (!IS_ENABLED(CONFIG_FORTIFY_SOURCE))
@@ -1054,6 +1166,8 @@ static struct kunit_case fortify_test_cases[] = {
KUNIT_CASE(fortify_test_memchr_inv),
KUNIT_CASE(fortify_test_memcmp),
KUNIT_CASE(fortify_test_kmemdup),
+ KUNIT_CASE(fortify_test_counted_by_flex),
+ KUNIT_CASE(fortify_test_counted_by_ptr),
{}
};

--
2.56.0.rc1.315.gc6ed9934b7-goog

Reply all
Reply to author
Forward
0 new messages