[ATfE] Add KASan sample#878
Conversation
smithp35
left a comment
There was a problem hiding this comment.
I've got as far looking through the tests. Will try and get through the runtime later.
| #include <stdio.h> | ||
| #include <stdlib.h> | ||
|
|
||
| #ifndef KASAN_TEST |
There was a problem hiding this comment.
Can we link to the readme that explains the test https://github.com/arm/arm-toolchain/pull/878/changes#diff-1186f8147b7f42d9e2e8e03156980977b0f5b007b35214181a28df5b70f30c69 or perhaps inline the bits that describe the test operation here.
There was a problem hiding this comment.
I will move it to where it is used in the main() - the list of tests will follow, each of them has a descriptive puts() call.
| #define KASAN_TEST 1 | ||
| #endif | ||
|
|
||
| static volatile unsigned out_of_bounds_index = 16; |
There was a problem hiding this comment.
Why does this have to be volatile? It looks like a constant.
There was a problem hiding this comment.
This is to prevent the compiler from considering this a constant and reasoning about the UB below - without volatile the stack test does not crash.
| static volatile unsigned out_of_bounds_index = 16; | ||
|
|
||
| __attribute__((noinline)) static void stack_overflow_test(void) { | ||
| uint8_t buffer[16] = {0}; |
There was a problem hiding this comment.
The 16 is used raw here. Could use a constant, possibly a macro that could be used in combination with out_of_bounds_index.
| } | ||
|
|
||
| __attribute__((noinline)) static void heap_overflow_test(void) { | ||
| uint8_t *buffer = static_cast<uint8_t *>(malloc(16)); |
There was a problem hiding this comment.
Another raw use of 16 that we could use a macro/constant for.
There was a problem hiding this comment.
Replaced with the same.
|
|
||
| __attribute__((noinline)) static void heap_overflow_test(void) { | ||
| uint8_t *buffer = static_cast<uint8_t *>(malloc(16)); | ||
| if (!buffer) { |
There was a problem hiding this comment.
This is correct, and could help detect a misconfigured heap, but it is additional boilerplate that is obscuring what we're testing for.
We could get rid of this part.
There was a problem hiding this comment.
I removed the puts() message to remove the noise, but kept the abort() - just to make sure that we are not masking other issues and not making debugging this sample harder.
| } | ||
|
|
||
| __attribute__((noinline)) static void heap_use_after_free_test(void) { | ||
| uint8_t *buffer = static_cast<uint8_t *>(calloc(16, 1)); |
There was a problem hiding this comment.
Any particular reason to use calloc rather than malloc? I don't think it affect the test case given we are immediately free'ing the memory.
There was a problem hiding this comment.
I guess calloc is part of what the runtime has to wrap, perhaps if we're doing this for test completeness of the runtime then we should also have a realloc test case.
There was a problem hiding this comment.
Yes, this was for a bit of extra coverage - I will add realloc() as well.
| abort(); | ||
| } | ||
|
|
||
| puts("Freeing a 16-byte allocation"); |
There was a problem hiding this comment.
I think it would be better to say "Freeing an allocation", the size isn't particularly important.
There was a problem hiding this comment.
Yes, the is irrelevant - let me update everywhere.
| puts("Freeing a 16-byte allocation"); | ||
| free(buffer); | ||
|
|
||
| puts("Writing to the freed allocation"); |
There was a problem hiding this comment.
Make sense - updated to refer to use-after-free directly.
| int main(void) { | ||
| puts("C++ KASan shadow-memory sample"); | ||
|
|
||
| #if KASAN_TEST == 1 |
There was a problem hiding this comment.
I guess this is because KASAN will abort() if it detects failure.
Given that the runtime is for tests, perhaps the KASAN runtime could follow the same precedent as UBSAN minimal runtime and call a user defined function that could report and attempt to continue. We may be able to get rid of the MACROs and do all the tests in one go.
There was a problem hiding this comment.
Yes, each test crashes, thus needs to be run individually.
I will make the KASan error handler overridable by the user and see if I can make all the tests run at once by overriding it in hello.cpp.
| @@ -0,0 +1,7 @@ | |||
| # RUN: make -C %samples_dir/src/cpp-baremetal-semihosting-kasan clean | |||
| # RUN: not make -C %samples_dir/src/cpp-baremetal-semihosting-kasan run BIN_PATH=%unpack_directory/bin KASAN_TEST=1 > %t.out 2>&1 | |||
There was a problem hiding this comment.
We should have 3 runs here, one for each KASAN_TEST={1,2,3} otherwise we could miss problems for the wrapping.
There was a problem hiding this comment.
Good point, adding all three.
smithp35
left a comment
There was a problem hiding this comment.
A small suggestion to rearrange the constants to show how they can be derived. Not had a chance to have gone through the runtime in detail yet.
| __flash_size = 0x3d000; /* length of the remaining flash */ | ||
|
|
||
| /* | ||
| * KASan uses shadow = (address >> 3) + 0x1c003800 for this sample. |
There was a problem hiding this comment.
I think we could rearrange some of the variables here to derive 0x1c003800. It may be helpful as an example as someone with a different RAM configuration will need to alter the values.
For example:
__ram = 0x20000000; /* starting address of application RAM */
/* Kasan uses 1-byte of shadow memory for 8-bytes of program use
__kasan_scale = 3;
/* How much RAM we have on the device */
__hw_ram_size = 0x4000;
__kasan_shadow_size = __hw_ram_size >> __kasan_scale;
__kasan_shadow_start = __ram + __hw_ram_size - __kasan_shadow_size;
__kasan_shadow_end = __kasan_shadow_start + __kasan_shadow_size;
/* We must map first byte of __ram to __kasan_shadow_start;
__kasan_shadow_offset = __kasan_shadow_start - (__ram >> __kasan_scale)
There was a problem hiding this comment.
Ah, it is a bit messy - I tried to rearrange it completely and align it with the FVP sample approach: calculate all of this in the Makefile and avoid duplication in the linker script by passing the symbols on the command line. Hope this looks more reasonable.
smithp35
left a comment
There was a problem hiding this comment.
I've managed to get through the runtime.
|
|
||
| /** @brief Clear the reserved shadow RAM so all covered memory starts valid. */ | ||
| static void clear_shadow(void) { | ||
| for (uint8_t *shadow = __kasan_shadow_start; shadow < __kasan_shadow_end; |
There was a problem hiding this comment.
Could we use memset here,
memset(__kasan_shadow_start, 0, __kasan_shadow_end - __kasan_shadow_start);
There was a problem hiding this comment.
This is called very early on at startup, however there do not seem to be any unwanted dependencies in memset() implementations of picolibc and LLVM libc, so should be OK - replaced.
| kasan_poison_with_value(base, header->total_size, KASAN_HEAP_FREED); | ||
|
|
||
| /* Keep freed blocks quarantined so use-after-free remains detectable. */ | ||
| (void)__real_calloc; |
There was a problem hiding this comment.
I don't understand the part below, I don't think it will have any effect on the code.
|
|
||
| kasan_poison_with_value(base, header->total_size, KASAN_HEAP_FREED); | ||
|
|
||
| /* Keep freed blocks quarantined so use-after-free remains detectable. */ |
There was a problem hiding this comment.
I guess this means that a program could easily run out of heap space. If we don't declare this in the readme then could be worth doing so.
A possibly more sophisticated implementation could keep track of all the freed blocks, and if the heap were full it could release those blocks.
There was a problem hiding this comment.
Yes, this was on my to-do: I added a simple queue of blocks waiting to be freed and a CMake variable to configure how many of them to keep.
| * @param n Number of descriptors in the array. | ||
| */ | ||
| void __asan_register_globals(uintptr_t globals, uintptr_t n) { | ||
| struct asan_global *global = (struct asan_global *)globals; |
There was a problem hiding this comment.
global isn't a great name for a plural. Perhaps call it asan_globals
There was a problem hiding this comment.
Good suggestion - renamed here and below.
| uintptr_t size = global[i].size; | ||
| uintptr_t size_with_redzone = global[i].size_with_redzone; | ||
|
|
||
| (void)global[i].name; |
There was a problem hiding this comment.
Are these necessary? We could just add a comment saying not all fields are used.
There was a problem hiding this comment.
Yes, a comment is easier to read - replaced.
| (void)global[i].source_location; | ||
| (void)global[i].odr_indicator; | ||
|
|
||
| if (size_with_redzone <= size) |
There was a problem hiding this comment.
That looks like a serious error rather than a skip. Should that be an error message.
There was a problem hiding this comment.
Now that there is an extra error reporting hook I added a message here using it - better to be loud.
smithp35
left a comment
There was a problem hiding this comment.
I've resolved the comments that I can based on the upstream commits. Probably best click request review on my username (like in llvm-project) when you're ready for the next round.
Thank you, @smithp35 ! I processed all runtime comments and updated the PR, please have a look. |
Add a sample to show how to use Kernel Address Sanitizer (KASan) in a bare-metal project.
- Kept only the shadow memory sample - the other one is only useful for heap that embedded users try to avoid. - Moved the shadow memory to the end of RAM to avoid changing user code/layout too much. - Replaced printf() with simple retargetable hooks. - Added a bit of explanation what KASan is in the README.
Key changes: - Provided overridable report handlers that allow the sample to continue after KASan reported problem - This enabled running all the checks (tests) in one go, thus test numbers removed - Added realloc() test - Tidied up test function bodies to remove unnecessary messages/clutter - Removed LLVM lib-only limitation - tested that it works with picolibc as well - Updated the README accordingly
- Added kasan_shadow_runtime.h for the hook declarations - Added offset in report hook - Added shadow update validation and error reporting - Removed unused ASan compatibility stubs and unused helpers - free() wrapper extended with quarantine queue - memset/memcpy used instead of raw loops - Improved comments and README
|
Resolved CHANGELOG merge conflict. |
smithp35
left a comment
There was a problem hiding this comment.
Only a small number of nits remaining.
Could we add some more detail to the commit message so that someone reading the commit log can know the bounds of what's been added. For example:
Add a sample to show how to use Kernel Address Sanitizer (KASan) in a bare-metal project.
The sample supplies an implementation of the KASan runtime tailored for the memory model used by the sample. To adapt the runtime for other examples please see the
README.md in the directory containing the sample.
| uint8_t value = *shadow; | ||
| if (value == 0) | ||
| return false; | ||
|
|
There was a problem hiding this comment.
Could add a reference to comment in kasan_unpoison Each shadow byte describes one application granule ... to explain the values.
There was a problem hiding this comment.
Added just the encoding here - it is easier to read, then a forward reference.
| #if KASAN_HEAP_QUARANTINE_SIZE > 0 | ||
| static struct heap_header | ||
| *heap_quarantine[KASAN_HEAP_QUARANTINE_SIZE]; | ||
| static size_t heap_quarantine_head; |
There was a problem hiding this comment.
I think it would be useful to explicitly initialise these to 0 rather than rely on the default.
heap_quarantine_oldest might be a better name.
There was a problem hiding this comment.
Yes, this wold fit the use case better - updated.
Good point, expanded. |
- Added comment with shadow memory encoding - heap_quarantine_head renamed to heap_quarantine_oldest - All quarantine queue variables initialized to 0.
|
Thank you for the review - updated as per the comments. |
smithp35
left a comment
There was a problem hiding this comment.
LGTM. Thanks for the patience in waiting for the review.
|
Thank you for your help, @smithp35 ! |
Add a sample to show how to use Kernel Address Sanitizer (KASan) in a bare-metal project.
The sample supplies an implementation of the KASan runtime tailored for the memory model used by the sample.
To adapt the runtime for other examples please see the README.md in the directory containing the sample.