Memory Detection Tool KASAN in the Linux Kernel (7) – Implementation Based on Slab

Technical experience sharing, welcome to follow and provide guidance.

In the previous article, we detailed the layout of KASAN shadow memory. This article analyzes the implementation principles of KASAN under slab.

kmem_cache Layout

For the slab layout with KASAN enabled, the structure has undergone some changes, as shown below:

struct kmem_cache {
......
#ifdef CONFIG_KASAN
    struct kasan_cache kasan_info;
#endif
......
};
struct kasan_cache {
    int alloc_meta_offset;
    int free_meta_offset;
    bool is_kmalloc;
};

The above structure members are reserved for debugging information when debugging the slab. Next, we will analyze the code calls during slab creation.

kmem_cache_create
    kmem_cache_create_usercopy
        create_cache
            __kmem_cache_create
                kmem_cache_open
                    calculate_sizes
                        kasan_cache_create
                            __kasan_cache_create

We focus on the slab creation of kmalloc, which is created at boot time. The code calls are as follows:

start_kernel
    mm_init
        kmem_cache_init
            create_kmalloc_caches
                create_boot_cache
                    __kmem_cache_create
                        kmem_cache_open
                            calculate_sizes
                                kasan_cache_create
                                    __kasan_cache_create

We focus on the key function<span>__kasan_cache_create</span>, whose source code is as follows:

void __kasan_cache_create(struct kmem_cache *cache, unsigned int *size,
              slab_flags_t *flags)
{
    unsigned int ok_size;
    unsigned int optimal_size;
    *flags |= SLAB_KASAN;

    if (!kasan_stack_collection_enabled())
        return;

    ok_size = *size;

    /* Add alloc meta into redzone. */
    cache->kasan_info.alloc_meta_offset = *size;
    *size += sizeof(struct kasan_alloc_meta);
    if (*size > KMALLOC_MAX_SIZE) {
        cache->kasan_info.alloc_meta_offset = 0;
        *size = ok_size;
        /* Continue, since free meta might still fit. */
    }

    optimal_size = cache->object_size + optimal_redzone(cache->object_size);
    /* Limit it with KMALLOC_MAX_SIZE (relevant for SLAB only). */
    if (optimal_size > KMALLOC_MAX_SIZE)
        optimal_size = KMALLOC_MAX_SIZE;
    /* Use optimal size if the size with added metas is not large enough. */
    if (*size < optimal_size)
        *size = optimal_size;
}

It can be seen that three main modifications were made:

  • flags
  • kasan_info
  • size

To verify, we can confirm in crash as follows:

crash> kmem -S kmalloc-128
CACHE             OBJSIZE  ALLOCATED     TOTAL  SLABS  SSIZE  NAME
ffffff8007003c80      128     140707    141056   4408     8k  kmalloc-128
crash> struct kmem_cache.name ffffff8007003c80
  name = 0xffffffd00c59e708 "kmalloc-128"
crash> struct kmem_cache ffffff8007003c80
struct kmem_cache {
  cpu_slab = 0xffffffd00ca37070, 
  flags = 1207959552, 
  min_partial = 5, 
  size = 256, 
  object_size = 128, 
  reciprocal_size = {
    m = 1, 
    sh1 = 1'\001', 
    sh2 = 7'\a'
  }, 
  offset = 64, 
  cpu_partial = 13, 
  oo = {
    x = 65568
  }, 
  max = {
    x = 65568
  }, 
  min = {
    x = 16
  }, 
  allocflags = 262144, 
  refcount = 1, 
  ctor = 0x0, 
  inuse = 128, 
  align = 128, 
  red_left_pad = 0, 
  name = 0xffffffd00c59e708 "kmalloc-128", 
  list = {
    next = 0xffffff8007003e68, 
    prev = 0xffffff8007003b68
  }, 
  kobj = {
    name = 0xffffffd00c59e708 "kmalloc-128", 
    entry = {
      next = 0xffffff8007003e80, 
      prev = 0xffffff8007003b80
    }, 
    parent = 0xffffff800a200228, 
    kset = 0xffffff800a200200, 
    ktype = 0xffffffd00ddee9a8 , 
    sd = 0xffffff800a2799c0, 
    kref = {
      refcount = {
        refs = {
          counter = 1
        }
      }
    }, 
    state_initialized = 1, 
    state_in_sysfs = 1, 
    state_add_uevent_sent = 0, 
    state_remove_uevent_sent = 0, 
    uevent_suppress = 0
  }, 
  kasan_info = {
    alloc_meta_offset = 128, 
    free_meta_offset = 0, 
    is_kmalloc = true
  }, 
  useroffset = 0, 
  usersize = 128, 
  node = {0xffffff8007000f80}
}

Extracting the data we are concerned about, the explanation is as follows:

  • size: 256 <span>cache->object_size + optimal_redzone(cache->object_size);</span> obtained
  • alloc_meta_offset: <span>cache->kasan_info.alloc_meta_offset = *size;</span> obtained
  • is_kmalloc: <span>cache->kasan_info.is_kmalloc = true;</span> obtained
  • flags: <span>*flags |= SLAB_KASAN;</span> obtained, where 1207959552 is<span>48000000</span>, and SLAB_KASAN is<span>8000000</span>

kmalloc Poison

To understand the poisoning steps of kmalloc, we can first analyze the code flow of kmalloc as follows:

kmalloc
    __kmalloc
        kasan_kmalloc
            ____kasan_kmalloc
    slab_alloc
        slab_alloc_node
            __slab_alloc
                ___slab_alloc
                    new_slab_objects
                        new_slab
                            allocate_slab
                                kasan_poison_slab

If a new slab needs to be added, the calling flow is as follows:

Here,<span>____kasan_kmalloc</span> mainly serves to poison the memory of the shadow area. Similarly, allocate_slab calls kasan_poison_slab to poison, which is relatively simple. We can look at the implementation of<span>____kasan_kmalloc</span>:

static inline void *____kasan_kmalloc(struct kmem_cache *cache,
                const void *object, size_t size, gfp_t flags)
{
    unsigned long redzone_start;
    unsigned long redzone_end;

    if (gfpflags_allow_blocking(flags))
        kasan_quarantine_reduce();

    if (unlikely(object == NULL))
        return NULL;

    if (is_kfence_address(kasan_reset_tag(object)))
        return (void *)object;

    /*
     * The object has already been unpoisoned by kasan_slab_alloc() for
     * kmalloc() or by kasan_krealloc() for krealloc().
     */

    /*
     * The redzone has byte-level precision for the generic mode.
     * Partially poison the last object granule to cover the unaligned
     * part of the redzone.
     */
    if (IS_ENABLED(CONFIG_KASAN_GENERIC))
        kasan_poison_last_granule((void *)object, size);

    /* Poison the aligned part of the redzone. */
    redzone_start = round_up((unsigned long)(object + size),
                KASAN_GRANULE_SIZE);
    redzone_end = round_up((unsigned long)(object + cache->object_size),
                KASAN_GRANULE_SIZE);
    kasan_poison((void *)redzone_start, redzone_end - redzone_start,
               KASAN_KMALLOC_REDZONE, false);

    /*
     * Save alloc info (if possible) for kmalloc() allocations.
     * This also rewrites the alloc info when called from kasan_krealloc().
     */
    if (kasan_stack_collection_enabled())
        set_alloc_info(cache, (void *)object, flags, true);

    /* Keep the tag that was set by kasan_slab_alloc(). */
    return (void *)object;
}

This function focuses on two points:

  1. kasan_poison_last_granule: poisons the shadow area that is less than 8 bytes.
  2. kasan_poison: poisons other areas with the red zone 0xfc.

Based on my test code “KASAN(2) – Self-Test Module” logs, we focus on the shadow area printed under the kmalloc_oob_right function, as follows:

[   10.441646] Memory state around the buggy address:
[   10.441684]  ffffff8011758000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   10.441723]  ffffff8011758080: fcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfc
[   10.441760] >ffffff8011758100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03
[   10.441792]                                                                 ^
[   10.441829]  ffffff8011758180: fcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfc
[   10.441866]  ffffff8011758200: fcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfc

It can be seen that here 0xfc and 0x3 are the poisoned values written by the above code.

In summary, every slab cache allocation is poisoned by default, and each allocated object has already been unpoisoned in advance. Each time kmalloc is called, it will be poisoned again based on the actual situation. This effectively checks for out-of-bounds (oob) issues. We see that the value of fc at address ffffff8011758080 is the poison set for us when allocating the slab, and kmalloc just re-applies the poison when the memory is actually allocated.

kfree Poison

For the kfree page, it mainly detects use-after-free (uaf) issues, currently based on one path of kernel free for code analysis, as follows:

kfree
    slab_free
        slab_free_freelist_hook
            slab_free_hook
                kasan_slab_free
                    __kasan_slab_free
                        ____kasan_slab_free      

<span>____kasan_slab_free</span> code is as follows:

static inline bool ____kasan_slab_free(struct kmem_cache *cache, void *object,
                unsigned long ip, bool quarantine, bool init)
{
    u8 tag;
    void *tagged_object;

    tag = get_tag(object);
    tagged_object = object;
    object = kasan_reset_tag(object);

    if (is_kfence_address(object))
        return false;

    if (unlikely(nearest_obj(cache, virt_to_head_page(object), object) !=
        object)) {
        kasan_report_invalid_free(tagged_object, ip);
        return true;
    }

    /* RCU slabs could be legally used after free within the RCU period */
    if (unlikely(cache->flags & SLAB_TYPESAFE_BY_RCU))
        return false;

    if (!kasan_byte_accessible(tagged_object)) {
        kasan_report_invalid_free(tagged_object, ip);
        return true;
    }

    kasan_poison(object, round_up(cache->object_size, KASAN_GRANULE_SIZE),
            KASAN_KMALLOC_FREE, init);

    if ((IS_ENABLED(CONFIG_KASAN_GENERIC) && !quarantine))
        return false;

    if (kasan_stack_collection_enabled())
        kasan_set_free_info(cache, object, tag);

    return kasan_quarantine_put(cache, object);
}

The main logic is as follows:

  1. Ignore kfence
  2. Invalid free not in the same object (KASAN(3) – Test Analysis has examples)
  3. Within the RCU grace period (KASAN(3) – Test Analysis has examples)
  4. Pointer not within kernel range
  5. Poison the object with KASAN_KMALLOC_FREE
  6. If stack tracing is supported, poison with KASAN_KMALLOC_FREETRACK
  7. Create a quarantine list, delaying the call to ___cache_free to release memory

At this point, we can see the values of the shadow area as follows:

[   10.444125] Memory state around the buggy address:
[   10.444162]  ffffff8011758080: fcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfc
[   10.444199]  ffffff8011758100: fa fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[   10.444237]  ffffff8011758180: fcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfc
[   10.444269]                                                                 ^
[   10.444306]  ffffff8011758200: 00 07 fcfcfcfcfcfcfcfcfcfcfcfcfcfc

At the position ffffff8011758100, we see that fa and fb are set, which are the poisoned values for the free area set by this code.

Conclusion

This article analyzes how KASAN poisons in the kmalloc and kfree functions by reading the code. It can be understood that in the slab allocator, KASAN poisons all required objects, allowing for proper instruction instrumentation to correctly determine whether the current code has out-of-bounds (oob) and use-after-free (uaf) errors. For instruction instrumentation, please refer to “KASAN(4) – INLINE Instrumentation Analysis” and “KASAN(5) – OUTLINE Instrumentation Analysis”.

Reference Documents

This article has detailed references to the following documents. If you need to understand the original meaning, you can check the following link:

http://www.wowotech.net/memory_management/424.html

Leave a Comment