Skip to content

[ATfE] Add KASan sample#878

Merged
voltur01 merged 8 commits into
arm:arm-softwarefrom
voltur01:kasan_sample
Jul 6, 2026
Merged

[ATfE] Add KASan sample#878
voltur01 merged 8 commits into
arm:arm-softwarefrom
voltur01:kasan_sample

Conversation

@voltur01

@voltur01 voltur01 commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

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.

@voltur01 voltur01 requested a review from a team as a code owner June 9, 2026 15:23

@smithp35 smithp35 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this have to be volatile? It looks like a constant.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 16 is used raw here. Could use a constant, possibly a macro that could be used in combination with out_of_bounds_index.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

__attribute__((noinline)) static void heap_overflow_test(void) {
uint8_t *buffer = static_cast<uint8_t *>(malloc(16));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another raw use of 16 that we could use a macro/constant for.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced with the same.


__attribute__((noinline)) static void heap_overflow_test(void) {
uint8_t *buffer = static_cast<uint8_t *>(malloc(16));
if (!buffer) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this was for a bit of extra coverage - I will add realloc() as well.

abort();
}

puts("Freeing a 16-byte allocation");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to say "Freeing an allocation", the size isn't particularly important.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the is irrelevant - let me update everywhere.

puts("Freeing a 16-byte allocation");
free(buffer);

puts("Writing to the freed allocation");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps "Use after free"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense - updated to refer to use-after-free directly.

int main(void) {
puts("C++ KASan shadow-memory sample");

#if KASAN_TEST == 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have 3 runs here, one for each KASAN_TEST={1,2,3} otherwise we could miss problems for the wrapping.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, adding all three.

@smithp35 smithp35 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 smithp35 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've managed to get through the runtime.

Comment thread arm-software/embedded/samples/src/cpp-baremetal-semihosting-kasan/Makefile Outdated

/** @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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use memset here,

memset(__kasan_shadow_start, 0, __kasan_shadow_end - __kasan_shadow_start);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the part below, I don't think it will have any effect on the code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, removed.


kasan_poison_with_value(base, header->total_size, KASAN_HEAP_FREED);

/* Keep freed blocks quarantined so use-after-free remains detectable. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

global isn't a great name for a plural. Perhaps call it asan_globals

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these necessary? We could just add a comment saying not all fields are used.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, a comment is easier to read - replaced.

(void)global[i].source_location;
(void)global[i].odr_indicator;

if (size_with_redzone <= size)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That looks like a serious error rather than a skip. Should that be an error message.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that there is an extra error reporting hook I added a message here using it - better to be loud.

@smithp35 smithp35 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@voltur01

Copy link
Copy Markdown
Contributor Author

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.

voltur01 added 7 commits June 30, 2026 17:42
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
@voltur01

Copy link
Copy Markdown
Contributor Author

Resolved CHANGELOG merge conflict.

@smithp35 smithp35 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could add a reference to comment in kasan_unpoison Each shadow byte describes one application granule ... to explain the values.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this wold fit the use case better - updated.

@voltur01

voltur01 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

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:

Good point, expanded.

- Added comment with shadow memory encoding
- heap_quarantine_head renamed to heap_quarantine_oldest
- All quarantine queue variables initialized to 0.
@voltur01

voltur01 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Thank you for the review - updated as per the comments.

@smithp35 smithp35 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks for the patience in waiting for the review.

@voltur01 voltur01 merged commit 3f342dd into arm:arm-software Jul 6, 2026
2 checks passed
@voltur01

voltur01 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Thank you for your help, @smithp35 !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants