Why Should I Care About Sanitizers?

Achieving memory safety is hard. Code is written by humans, and humans are bound to make mistakes. Tack on pointer arithmetic and strict aliasing and the situation only becomes more complex. ZDNet recently posted an article stating that 70 percent of all (Microsoft) security bugs are memory safety issues, and I would guess that this statistic is not specific to Microsoft. In LLVM 3.1 and in GCC 4.8, the compilers introduced the -fsanitize=address option and the asan libraries for user-space applications to be built with the Address Sanitizer. The Address Sanitizer is a tool that helps detect use after free bugs, buffer overflows, use after return bugs, and memory leaks. While not suitable for a production environment, setting up an address sanitizer in a testing environment is easy when compared to the potential pay offs. In most cases one can benefit from address sanitizers by merely adding the -fsanitize=address -fno-omit-frame-pointer flags to CFLAGS, building the project, and running the tests. Both the LLVM and GCC implementations are great. There is an extensive pros and cons document maintained by Google that can be found on the the google sanitizers wiki.

KASAN: What is it?

Implementing support for address sanitizers for user-space applications has undoubtedly been a success, so in v5.0 of GCC the -fsanitize=kernel-address compiler flag was added, and in v4.0 of the Linux Kernel, support for building a kernel with this option was added with the CONFIG_KASAN Kconfig option. This option enables a fast and efficient memory error detector with minor performance degradation.

Implementation Details

It can be helpful to understand how KASAN is implemented when debugging issues discovered when using it. In this section we will discuss how KASAN_GENERIC is implemented. All examples will assume the x86_64 architecture.

Shadow Memory

The state of each 8 aligned bytes of memory is encoded in a byte in shadow memory. As a result, 1/8th of the kernel’s memory is dedicated to this shadow memory. The compiler instrumentation then relies on intrinsics provided by the kernel. The kernel intrinsics find the address of the corresponding shadow memory then determine if each access is valid based on the state stored in the shadow memory and the size requested. The translation function for finding the shadow region is kasan_mem_to_shadow which is implemented as:

static inline void *kasan_mem_to_shadow(const void *addr)
{
        return (void *)((unsigned long)addr >> KASAN_SHADOW_SCALE_SHIFT)
                + KASAN_SHADOW_OFFSET;
}

Each byte of the shadow region encodes the status of the address to be accessed as:

Value Meaning
0x00 All 8 bytes are accessible.
0x00 < N < 0x08 The lower N bytes are accessible. Bytes 8 - N to byte 8 are not.
0xff The page was freed.
0xfe A redzone for kmalloc_large.
0xfc A redzone for a slub object.
0xfb The object was freed.
0xf5 Stack use after return.
0xf8 Stack use after scope.

For example, let’s say we were to have some code like the following:

  u8 *ptr = kmalloc(12, GFP_KERNEL); /* ptr=0xffff8882245bc8c0 */
  ...
  kfree(ptr);

The address space and encoding in the shadow buffer would look something like the following:

Layout Before Free

In addition to setting up the shadow memory for encoding the state of the address, The kernel then must implement several __asan_load and __asan_store functions. Given an address and size, the kernel must check the shadow region to ensure the access is valid, returning true if it is and false if it is not. If the CONFIG_KASAN_INLINE option is set, the calls to these functions will be inlined causing a significant increase in performance at the cost of an increase in the size of the kernel image.

Redzone

Each shadow state area is surrounded by gaps called ‘redzones’, used to detect overflow in consecutive buffers. In order to see why this is necessary let us consider what the shadow state would contain for two consecutive heap allocations without a redzone surrounding them for an allocation of 16 bytes followed by an allocation of 12 bytes.

No Redzone

Note that this encoding is exactly what we would expect the shadow state to be for a buffer of 28 bytes in length. As a result, we might not catch buffer overflows of the first buffer.

Red Zone

With the introduction of the redzone gaps between buffer shadow states, we can easily detect buffer overflows even when a buffer is sized with 8 byte increments.

Example

Let’s test out KASAN by creating a kernel module that creates a proc device that contains a buffer overflow.

Create the Module

Create a file test_kasan.c that contains something like this:

#include <linux/fs.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

static struct proc_dir_entry *entry;

static u8 *test_kasan_buffer;

static ssize_t test_kasan_write(
      struct file *filep, const char __user *buf,
      size_t length, loff_t *off)
{
    unsigned long n;
    n = copy_from_user(test_kasan_buffer, buf, length);
    return length;
}

static struct file_operations test_kasan_fops = {
    .owner = THIS_MODULE,
    .write = test_kasan_write
};

static int __init test_kasan_init(void)
{
    test_kasan_buffer = kmalloc(14, GFP_KERNEL);
    if (!test_kasan_buffer)
        return -ENOMEM;

    entry = proc_create("test_kasan", S_IWUSR, NULL, &test_kasan_fops);
    if (!entry)
        return -ENOMEM;

    return 0;
}

static void test_kasan_exit(void)
{
    kfree(test_kasan_buffer);
    remove_proc_entry("test_kasan", NULL);
}

MODULE_AUTHOR("Star Lab <[email protected]>");
MODULE_DESCRIPTION("Testing KASAN");
MODULE_LICENSE("GPL");

module_init(test_kasan_init);
module_exit(test_kasan_exit);

Note that our proc device’s write implementation test_kasan_write does not check the length of the write before attempting to write to our local buffer, test_kasan_buffer, with copy_from_user. Our local buffer points to 14 bytes of allocated memory, so any write to the /proc/test_kasan device that is over 14 bytes in length should trigger an error.

Triggering a KASAN Error

After compiling the module and creating a VM that boots a kernel with CONFIG_KASAN set, we should be able to trigger a KASAN error after loading the module with something like the following:

echo "Hello, KASAN error" > /proc/test_kasan

This should trigger an error and display a report.

Dissecting the Report

The load and store functions should also dump a report for the user if the memory check finds an issue with the memory access. The report provides information useful for debugging the error and provides a backtrace that should look familiar if you have seen a kernel oops or panic before. A simple example is shown below:

 ==================================================================
 BUG: KASAN: slab-out-of-bounds in _copy_from_user+0x51/0x90
 Write of size 19 at addr ffff888230c0d4a0 by task bash/879

 CPU: 3 PID: 879 Comm: bash Tainted: G           O      5.4.0-rc5+ #16
 Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS ?
\-20190711_202441-buildvm-armv7-10.arm.fedoraproject.org-2.fc31 04/01/2014
 Call Trace:
  dump_stack+0x5b/0x90
  print_address_description.constprop.0+0x16/0x200
  ? _copy_from_user+0x51/0x90
  ? _copy_from_user+0x51/0x90
  __kasan_report.cold+0x1a/0x41
  ? _copy_from_user+0x51/0x90
  kasan_report+0xe/0x20
  check_memory_region+0x130/0x1a0
  _copy_from_user+0x51/0x90
  test_kasan_write+0x11/0x30 [test_kasan]
  proc_reg_write+0x110/0x160
  ? proc_reg_unlocked_ioctl+0x150/0x150
  ? __pmd_alloc+0x150/0x150
  ? __audit_syscall_entry+0x18e/0x1f0
  ? ktime_get_coarse_real_ts64+0x46/0x60
  ? security_file_permission+0x66/0x190
  vfs_write+0xed/0x240
  ksys_write+0xb4/0x150
  ? __ia32_sys_read+0x40/0x40
  ? up_read+0x10/0x70
  ? do_user_addr_fault+0x3da/0x560
  do_syscall_64+0x5e/0x190
  entry_SYSCALL_64_after_hwframe+0x44/0xa9
 RIP: 0033:0x7fe39aa3c150
 Code: 73 01 c3 48 8b 0d 20 6d 2d 00 f7 d8 64 89 01 48 83 c8 ff c3\
 66 0f 1f 44 00 00 83 3d 4d ce 2d 00 00 75 10 b8 01 00 00 00 0f 05\
 <48> 3d 01 f0 ff ff 73 31 c3 48 83 ec 08 e8 ee cb 01 00 48 89 04 24
 RSP: 002b:00007fff5b7839d8 EFLAGS: 00000246 ORIG_RAX: 0000000000000001
 RAX: ffffffffffffffda RBX: 0000000000000011 RCX: 00007fe39aa3c150
 RDX: 0000000000000011 RSI: 00007fe39b366000 RDI: 0000000000000001
 RBP: 00007fe39b366000 R08: 000000000000000a R09: 00007fe39b35c740
 R10: 0000000000000022 R11: 0000000000000246 R12: 00007fe39ad14400
 R13: 0000000000000011 R14: 0000000000000001 R15: 0000000000000000

 Allocated by task 894:
  save_stack+0x1b/0x80
  __kasan_kmalloc.constprop.0+0xc2/0xd0
  0xffffffffc0008022
  do_one_initcall+0x86/0x29f
  do_init_module+0xf8/0x350
  load_module+0x3e57/0x4120
  __do_sys_finit_module+0x162/0x190
  do_syscall_64+0x5e/0x190
  entry_SYSCALL_64_after_hwframe+0x44/0xa9

 Freed by task 614:
  save_stack+0x1b/0x80
  __kasan_slab_free+0x12c/0x170
  kfree+0x90/0x240
  xdr_free_bvec+0x1a/0x30
  xprt_release+0x10a/0x270
  rpc_release_resources_task+0x14/0x70
  __rpc_execute+0x253/0x590
  rpc_async_schedule+0x44/0x70
  process_one_work+0x476/0x760
  worker_thread+0x73/0x680
  kthread+0x18c/0x1e0
  ret_from_fork+0x35/0x40

 The buggy address belongs to the object at ffff888230c0d4a0
  which belongs to the cache kmalloc-16 of size 16
 The buggy address is located 0 bytes inside of
  16-byte region [ffff888230c0d4a0, ffff888230c0d4b0)
 The buggy address belongs to the page:
 page:ffffea0008c30340 refcount:1 mapcount:0
mapping:ffff888236403b80 index:0xffff888230c0d9c0
 flags: 0x200000000000200(slab)
 raw: 0200000000000200 ffffea0008b86700 0000001200000012 ffff888236403b80
 raw: ffff888230c0d9c0 0000000080800079 00000001ffffffff 0000000000000000
 page dumped because: kasan: bad access detected

 Memory state around the buggy address:
  ffff888230c0d380: 00 00 fc fc fb fb fc fc fb fb fc fc fb fb fc fc
  ffff888230c0d400: fb fb fc fc fb fb fc fc fb fb fc fc fb fb fc fc
 >ffff888230c0d480: fb fb fc fc 00 06 fc fc fb fb fc fc 00 00 fc fc
                                   ^
  ffff888230c0d500: 00 00 fc fc 00 00 fc fc 00 00 fc fc 00 00 fc fc
  ffff888230c0d580: 00 00 fc fc 00 00 fc fc 00 00 fc fc 00 00 fc fc
 ==================================================================

Note that the last section of the report includes a hex dump of the associated shadow memory for the access that triggered the error. The line of interest to us in the hex dump is indicated by a >. The exact byte that encodes the 8 byte section the address was located in is indicated by a ^.

 >ffff888230c0d480: fb fb fc fc 00 06 fc fc fb fb fc fc 00 00 fc fc
                                   ^

We can use this information along with the cause of the error shown in line 2 (Write of size 19 at addr ffff888230c0d4a0) to gain some information about the error.

In the example above, we see the buffer is encoded with the bytes 00 06. This byte sequence encodes an 8 byte region followed by a 6 byte region which would be the encoding for a buffer of 14 bytes in length. This checks with the error provided as a write of size 19 to this buffer would write beyond the end of the buffer.