Skip to contentSkip to conversation

kasan: test: add memcpy test that avoids out-of-bounds write

PATCH Applied Peter Collingbourne 4 participants 6 messages Sep 10, 2021 – Sep 13, 2021

Subsystems (2)
lore
Share & tools
lore
#0 lore raw

PATCH v2 1/1 kasan: test: add memcpy test that avoids out-of-bounds write

With HW tag-based KASAN, error checks are performed implicitly by the load and store instructions in the memcpy implementation. A failed check results in tag checks being disabled and execution will keep going. As a result, under HW tag-based KASAN, prior to commit 1b0668be62cf ("kasan:
test: disable kmalloc_memmove_invalid_size for HW_TAGS"), this memcpy would end up corrupting memory until it hits an inaccessible page and causes a kernel panic.

This is a pre-existing issue that was revealed by commit 285133040e6c ("arm64: Import latest memcpy()/memmove() implementation") which changed the memcpy implementation from using signed comparisons (incorrectly, resulting in the memcpy being terminated early for negative sizes) to using unsigned comparisons.

It is unclear how this could be handled by memcpy itself in a reasonable way. One possibility would be to add an exception handler that would force memcpy to return if a tag check fault is detected -- this would make the behavior roughly similar to generic and SW tag-based KASAN. However, this wouldn't solve the problem for asynchronous mode and also makes memcpy behavior inconsistent with manually copying data.

This test was added as a part of a series that taught KASAN to detect negative sizes in memory operations, see commit 8cceeff48f23 ("kasan:
detect negative size in memory operation function"). Therefore we should keep testing for negative sizes with generic and SW tag-based KASAN. But there is some value in testing small memcpy overflows, so let's add another test with memcpy that does not destabilize the kernel by performing out-of-bounds writes, and run it in all modes.

---
 lib/test_kasan.c | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)
lib/test_kasan.c +17 -1
index 8835e0784578..aa8e42250219 100644
@@ -493,7 +493,7 @@ static void kmalloc_oob_in_memset(struct kunit *test)
 	kfree(ptr);
 }
 
-static void kmalloc_memmove_invalid_size(struct kunit *test)
+static void kmalloc_memmove_negative_size(struct kunit *test)
 {
 	char *ptr;
 	size_t size = 64;
@@ -515,6 +515,21 @@ static void kmalloc_memmove_invalid_size(struct kunit *test)
 	kfree(ptr);
 }
 
+static void kmalloc_memmove_invalid_size(struct kunit *test)
+{
+	char *ptr;
+	size_t size = 64;
+	volatile size_t invalid_size = size;
+
+	ptr = kmalloc(size, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ptr);
+
+	memset((char *)ptr, 0, 64);
+	KUNIT_EXPECT_KASAN_FAIL(test,
+		memmove((char *)ptr, (char *)ptr + 4, invalid_size));
+	kfree(ptr);
+}
+
 static void kmalloc_uaf(struct kunit *test)
 {
 	char *ptr;
@@ -1129,6 +1144,7 @@ static struct kunit_case kasan_kunit_test_cases[] = {
 	KUNIT_CASE(kmalloc_oob_memset_4),
 	KUNIT_CASE(kmalloc_oob_memset_8),
 	KUNIT_CASE(kmalloc_oob_memset_16),
+	KUNIT_CASE(kmalloc_memmove_negative_size),
 	KUNIT_CASE(kmalloc_memmove_invalid_size),
 	KUNIT_CASE(kmalloc_uaf),
 	KUNIT_CASE(kmalloc_uaf_memset),
Signature · 1 line
2.33.0.309.g3052b89438-goog
Reviewed-by on v2 1/1
#1 lore raw
Quoted 80 lines · Peter Collingbourne

With HW tag-based KASAN, error checks are performed implicitly by the load and store instructions in the memcpy implementation. A failed check results in tag checks being disabled and execution will keep going. As a result, under HW tag-based KASAN, prior to commit 1b0668be62cf ("kasan:
test: disable kmalloc_memmove_invalid_size for HW_TAGS"), this memcpy would end up corrupting memory until it hits an inaccessible page and causes a kernel panic.

This is a pre-existing issue that was revealed by commit 285133040e6c ("arm64: Import latest memcpy()/memmove() implementation") which changed the memcpy implementation from using signed comparisons (incorrectly, resulting in the memcpy being terminated early for negative sizes) to using unsigned comparisons.

It is unclear how this could be handled by memcpy itself in a reasonable way. One possibility would be to add an exception handler that would force memcpy to return if a tag check fault is detected -- this would make the behavior roughly similar to generic and SW tag-based KASAN. However, this wouldn't solve the problem for asynchronous mode and also makes memcpy behavior inconsistent with manually copying data.

This test was added as a part of a series that taught KASAN to detect negative sizes in memory operations, see commit 8cceeff48f23 ("kasan:
detect negative size in memory operation function"). Therefore we should keep testing for negative sizes with generic and SW tag-based KASAN. But there is some value in testing small memcpy overflows, so let's add another test with memcpy that does not destabilize the kernel by performing out-of-bounds writes, and run it in all modes.

---
 lib/test_kasan.c | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)
lib/test_kasan.c +17 -2
index 8835e0784578..aa8e42250219 100644
@@ -493,7 +493,7 @@ static void kmalloc_oob_in_memset(struct kunit *test)
        kfree(ptr);
 }
 
-static void kmalloc_memmove_invalid_size(struct kunit *test)
+static void kmalloc_memmove_negative_size(struct kunit *test)
 {
        char *ptr;
        size_t size = 64;
@@ -515,6 +515,21 @@ static void kmalloc_memmove_invalid_size(struct kunit *test)
        kfree(ptr);
 }
 
+static void kmalloc_memmove_invalid_size(struct kunit *test)
+{
+       char *ptr;
+       size_t size = 64;
+       volatile size_t invalid_size = size;
+
+       ptr = kmalloc(size, GFP_KERNEL);
+       KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ptr);
+
+       memset((char *)ptr, 0, 64);
+       KUNIT_EXPECT_KASAN_FAIL(test,
+               memmove((char *)ptr, (char *)ptr + 4, invalid_size));
+       kfree(ptr);
+}
+
 static void kmalloc_uaf(struct kunit *test)
 {
        char *ptr;
@@ -1129,6 +1144,7 @@ static struct kunit_case kasan_kunit_test_cases[] = {
        KUNIT_CASE(kmalloc_oob_memset_4),
        KUNIT_CASE(kmalloc_oob_memset_8),
        KUNIT_CASE(kmalloc_oob_memset_16),
+       KUNIT_CASE(kmalloc_memmove_negative_size),
        KUNIT_CASE(kmalloc_memmove_invalid_size),
        KUNIT_CASE(kmalloc_uaf),
        KUNIT_CASE(kmalloc_uaf_memset),
--
Quoted 2 lines

2.33.0.309.g3052b89438-goog

Thanks!

Acked-by
#2 lore raw
Quoted 78 lines · Andrey Konovalov
Quoted 76 lines · Peter Collingbourne

With HW tag-based KASAN, error checks are performed implicitly by the load and store instructions in the memcpy implementation. A failed check results in tag checks being disabled and execution will keep going. As a result, under HW tag-based KASAN, prior to commit 1b0668be62cf ("kasan:
test: disable kmalloc_memmove_invalid_size for HW_TAGS"), this memcpy would end up corrupting memory until it hits an inaccessible page and causes a kernel panic.

This is a pre-existing issue that was revealed by commit 285133040e6c ("arm64: Import latest memcpy()/memmove() implementation") which changed the memcpy implementation from using signed comparisons (incorrectly, resulting in the memcpy being terminated early for negative sizes) to using unsigned comparisons.

It is unclear how this could be handled by memcpy itself in a reasonable way. One possibility would be to add an exception handler that would force memcpy to return if a tag check fault is detected -- this would make the behavior roughly similar to generic and SW tag-based KASAN. However, this wouldn't solve the problem for asynchronous mode and also makes memcpy behavior inconsistent with manually copying data.

This test was added as a part of a series that taught KASAN to detect negative sizes in memory operations, see commit 8cceeff48f23 ("kasan:
detect negative size in memory operation function"). Therefore we should keep testing for negative sizes with generic and SW tag-based KASAN. But there is some value in testing small memcpy overflows, so let's add another test with memcpy that does not destabilize the kernel by performing out-of-bounds writes, and run it in all modes.

---
 lib/test_kasan.c | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)
lib/test_kasan.c +17 -1
index 8835e0784578..aa8e42250219 100644
@@ -493,7 +493,7 @@ static void kmalloc_oob_in_memset(struct kunit *test)
        kfree(ptr);
 }
 
-static void kmalloc_memmove_invalid_size(struct kunit *test)
+static void kmalloc_memmove_negative_size(struct kunit *test)
 {
        char *ptr;
        size_t size = 64;
@@ -515,6 +515,21 @@ static void kmalloc_memmove_invalid_size(struct kunit *test)
        kfree(ptr);
 }
 
+static void kmalloc_memmove_invalid_size(struct kunit *test)
+{
+       char *ptr;
+       size_t size = 64;
+       volatile size_t invalid_size = size;
+
+       ptr = kmalloc(size, GFP_KERNEL);
+       KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ptr);
+
+       memset((char *)ptr, 0, 64);
+       KUNIT_EXPECT_KASAN_FAIL(test,
+               memmove((char *)ptr, (char *)ptr + 4, invalid_size));
+       kfree(ptr);
+}
+
 static void kmalloc_uaf(struct kunit *test)
 {
        char *ptr;
@@ -1129,6 +1144,7 @@ static struct kunit_case kasan_kunit_test_cases[] = {
        KUNIT_CASE(kmalloc_oob_memset_4),
        KUNIT_CASE(kmalloc_oob_memset_8),
        KUNIT_CASE(kmalloc_oob_memset_16),
+       KUNIT_CASE(kmalloc_memmove_negative_size),
Quoted 8 lines
Quoted 6 lines
lib/test_kasan.c +0 -1
        KUNIT_CASE(kmalloc_memmove_invalid_size),
        KUNIT_CASE(kmalloc_uaf),
        KUNIT_CASE(kmalloc_uaf_memset),
--

2.33.0.309.g3052b89438-goog

Do you intend this patch to go through the arm64 or mm tree?

Quoted 1 line

Thanks!

#3 lore raw
Quoted 89 lines · Marco Elver
Quoted 78 lines · Andrey Konovalov
Quoted 76 lines · Peter Collingbourne

With HW tag-based KASAN, error checks are performed implicitly by the load and store instructions in the memcpy implementation. A failed check results in tag checks being disabled and execution will keep going. As a result, under HW tag-based KASAN, prior to commit 1b0668be62cf ("kasan:
test: disable kmalloc_memmove_invalid_size for HW_TAGS"), this memcpy would end up corrupting memory until it hits an inaccessible page and causes a kernel panic.

This is a pre-existing issue that was revealed by commit 285133040e6c ("arm64: Import latest memcpy()/memmove() implementation") which changed the memcpy implementation from using signed comparisons (incorrectly, resulting in the memcpy being terminated early for negative sizes) to using unsigned comparisons.

It is unclear how this could be handled by memcpy itself in a reasonable way. One possibility would be to add an exception handler that would force memcpy to return if a tag check fault is detected -- this would make the behavior roughly similar to generic and SW tag-based KASAN. However, this wouldn't solve the problem for asynchronous mode and also makes memcpy behavior inconsistent with manually copying data.

This test was added as a part of a series that taught KASAN to detect negative sizes in memory operations, see commit 8cceeff48f23 ("kasan:
detect negative size in memory operation function"). Therefore we should keep testing for negative sizes with generic and SW tag-based KASAN. But there is some value in testing small memcpy overflows, so let's add another test with memcpy that does not destabilize the kernel by performing out-of-bounds writes, and run it in all modes.

---
 lib/test_kasan.c | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)
lib/test_kasan.c +17 -1
index 8835e0784578..aa8e42250219 100644
@@ -493,7 +493,7 @@ static void kmalloc_oob_in_memset(struct kunit *test)
        kfree(ptr);
 }
 
-static void kmalloc_memmove_invalid_size(struct kunit *test)
+static void kmalloc_memmove_negative_size(struct kunit *test)
 {
        char *ptr;
        size_t size = 64;
@@ -515,6 +515,21 @@ static void kmalloc_memmove_invalid_size(struct kunit *test)
        kfree(ptr);
 }
 
+static void kmalloc_memmove_invalid_size(struct kunit *test)
+{
+       char *ptr;
+       size_t size = 64;
+       volatile size_t invalid_size = size;
+
+       ptr = kmalloc(size, GFP_KERNEL);
+       KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ptr);
+
+       memset((char *)ptr, 0, 64);
+       KUNIT_EXPECT_KASAN_FAIL(test,
+               memmove((char *)ptr, (char *)ptr + 4, invalid_size));
+       kfree(ptr);
+}
+
 static void kmalloc_uaf(struct kunit *test)
 {
        char *ptr;
@@ -1129,6 +1144,7 @@ static struct kunit_case kasan_kunit_test_cases[] = {
        KUNIT_CASE(kmalloc_oob_memset_4),
        KUNIT_CASE(kmalloc_oob_memset_8),
        KUNIT_CASE(kmalloc_oob_memset_16),
+       KUNIT_CASE(kmalloc_memmove_negative_size),
Quoted 8 lines
Quoted 6 lines
lib/test_kasan.c +0 -1
        KUNIT_CASE(kmalloc_memmove_invalid_size),
        KUNIT_CASE(kmalloc_uaf),
        KUNIT_CASE(kmalloc_uaf_memset),
--

2.33.0.309.g3052b89438-goog

Quoted 3 lines

Do you intend this patch to go through the arm64 or mm tree?

Let's take it through the mm tree.

Peter

#4 lore raw
In reply to Peter Collingbourne
Quoted 21 lines · Peter Collingbourne

With HW tag-based KASAN, error checks are performed implicitly by the load and store instructions in the memcpy implementation. A failed check results in tag checks being disabled and execution will keep going. As a result, under HW tag-based KASAN, prior to commit 1b0668be62cf ("kasan:
test: disable kmalloc_memmove_invalid_size for HW_TAGS"), this memcpy would end up corrupting memory until it hits an inaccessible page and causes a kernel panic.

This is a pre-existing issue that was revealed by commit 285133040e6c ("arm64: Import latest memcpy()/memmove() implementation") which changed the memcpy implementation from using signed comparisons (incorrectly, resulting in the memcpy being terminated early for negative sizes) to using unsigned comparisons.

It is unclear how this could be handled by memcpy itself in a reasonable way. One possibility would be to add an exception handler that would force memcpy to return if a tag check fault is detected -- this would make the behavior roughly similar to generic and SW tag-based KASAN. However, this wouldn't solve the problem for asynchronous mode and also makes memcpy behavior inconsistent with manually copying data.

Quoted 7 lines

This test was added as a part of a series that taught KASAN to detect negative sizes in memory operations, see commit 8cceeff48f23 ("kasan:
detect negative size in memory operation function"). Therefore we should keep testing for negative sizes with generic and SW tag-based KASAN. But there is some value in testing small memcpy overflows, so let's add another test with memcpy that does not destabilize the kernel by performing out-of-bounds writes, and run it in all modes.

The only thing is, that's nonsense. You can't pass a negative size to memmove()/memcpy(), any more than you could pass a negative address. You can use the usual integer conversions to pass a very large size, but that's no different from just passing a very large size, and the language does not make any restrictions on the validity of very large sizes. Indeed in general a 32-bit program could legitimately memcpy() exactly half its address space to the other half, or memmove() a 3GB buffer a small distance.

I'm not sure what we're trying to enforce there, other than arbitrary restrictions on how we think it makes sense to call library functions. The only way to say that a size is actually invalid is if it leads to an out-of-bounds access relative to the source or destination buffer, but to provoke that the given size only ever needs to be at least 1 byte larger than the object - making it excessively large only generates excessively large numbers of invalid accesses, and I fail to see what use that has. By all means introduce KAROHWTIMSTCLFSAN, but I'm not convinced it's meaningfully within the scope of *address* sanitisation.

Thanks,
Robin.

Quoted 50 lines
---
  lib/test_kasan.c | 18 +++++++++++++++++-
  1 file changed, 17 insertions(+), 1 deletion(-)
lib/test_kasan.c +17 -1
index 8835e0784578..aa8e42250219 100644
@@ -493,7 +493,7 @@ static void kmalloc_oob_in_memset(struct kunit *test)
  	kfree(ptr);
  }
 
-static void kmalloc_memmove_invalid_size(struct kunit *test)
+static void kmalloc_memmove_negative_size(struct kunit *test)
  {
  	char *ptr;
  	size_t size = 64;
@@ -515,6 +515,21 @@ static void kmalloc_memmove_invalid_size(struct kunit *test)
  	kfree(ptr);
  }
 
+static void kmalloc_memmove_invalid_size(struct kunit *test)
+{
+	char *ptr;
+	size_t size = 64;
+	volatile size_t invalid_size = size;
+
+	ptr = kmalloc(size, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ptr);
+
+	memset((char *)ptr, 0, 64);
+	KUNIT_EXPECT_KASAN_FAIL(test,
+		memmove((char *)ptr, (char *)ptr + 4, invalid_size));
+	kfree(ptr);
+}
+
  static void kmalloc_uaf(struct kunit *test)
  {
  	char *ptr;
@@ -1129,6 +1144,7 @@ static struct kunit_case kasan_kunit_test_cases[] = {
  	KUNIT_CASE(kmalloc_oob_memset_4),
  	KUNIT_CASE(kmalloc_oob_memset_8),
  	KUNIT_CASE(kmalloc_oob_memset_16),
+	KUNIT_CASE(kmalloc_memmove_negative_size),
  	KUNIT_CASE(kmalloc_memmove_invalid_size),
  	KUNIT_CASE(kmalloc_uaf),
  	KUNIT_CASE(kmalloc_uaf_memset),
#5 lore raw
Quoted 40 lines · Robin Murphy
Quoted 21 lines · Peter Collingbourne

With HW tag-based KASAN, error checks are performed implicitly by the load and store instructions in the memcpy implementation. A failed check results in tag checks being disabled and execution will keep going. As a result, under HW tag-based KASAN, prior to commit 1b0668be62cf ("kasan:
test: disable kmalloc_memmove_invalid_size for HW_TAGS"), this memcpy would end up corrupting memory until it hits an inaccessible page and causes a kernel panic.

This is a pre-existing issue that was revealed by commit 285133040e6c ("arm64: Import latest memcpy()/memmove() implementation") which changed the memcpy implementation from using signed comparisons (incorrectly, resulting in the memcpy being terminated early for negative sizes) to using unsigned comparisons.

It is unclear how this could be handled by memcpy itself in a reasonable way. One possibility would be to add an exception handler that would force memcpy to return if a tag check fault is detected -- this would make the behavior roughly similar to generic and SW tag-based KASAN. However, this wouldn't solve the problem for asynchronous mode and also makes memcpy behavior inconsistent with manually copying data.

Quoted 7 lines

This test was added as a part of a series that taught KASAN to detect negative sizes in memory operations, see commit 8cceeff48f23 ("kasan:
detect negative size in memory operation function"). Therefore we should keep testing for negative sizes with generic and SW tag-based KASAN. But there is some value in testing small memcpy overflows, so let's add another test with memcpy that does not destabilize the kernel by performing out-of-bounds writes, and run it in all modes.

The only thing is, that's nonsense. You can't pass a negative size to memmove()/memcpy(), any more than you could pass a negative address. You can use the usual integer conversions to pass a very large size, but that's no different from just passing a very large size, and the language does not make any restrictions on the validity of very large sizes. Indeed in general a 32-bit program could legitimately memcpy() exactly half its address space to the other half, or memmove() a 3GB buffer a small distance.

Quoted 9 lines

I'm not sure what we're trying to enforce there, other than arbitrary restrictions on how we think it makes sense to call library functions. The only way to say that a size is actually invalid is if it leads to an out-of-bounds access relative to the source or destination buffer, but to provoke that the given size only ever needs to be at least 1 byte larger than the object - making it excessively large only generates excessively large numbers of invalid accesses, and I fail to see what use that has. By all means introduce KAROHWTIMSTCLFSAN, but I'm not convinced it's meaningfully within the scope of *address* sanitisation.

This is an orthogonal issue, isn't it? It may make sense to make the memmove()/memcpy() behavior controllable separately, but that can be done separately from this change.

Peter