一个程序员的辩白

18 Aug 2018

HOWTO avoid memory leak in kext development

Memory leakage is a major issue in software development, let alone happens in kernel extension(kext) layer. Also, sometimes it’s hard to track down memory leakage, it’s an invisible bomb, it won’t cause kernel panic in most cases, yet it unstabilize the whole kernel.

Specially, I’ll take XNU kernel driver(kernel extension) as our topic.

When possible, avoid using _MALLOC or _FREE. if you don’t free the memory allocated by _MALLOC, it certainly memory leaked, it won’t kernel panic even when you unload the kext.

/**
 * A _MALLOC leakage demo kext
 */
#include <mach/mach_types.h>
#include <sys/systm.h>
#include <sys/malloc.h>

#define INAME "kext-memleak"
#define LOG(fmt, ...) printf(INAME ": " fmt "\n", ##__VA_ARGS__)

static unsigned char *p;

kern_return_t kext_memleak_start(kmod_info_t *ki, void *d)
{
    p = _MALLOC(1, M_TEMP, M_WAITOK);
    LOG("byte allocated %#llx %#x", (uint64_t) p, *p);
    return KERN_SUCCESS;
}

kern_return_t kext_memleak_stop(kmod_info_t *ki, void *d)
{
    /* Say, you forget to free the byte */
    LOG("byte leaked %#llx", (uint64_t) p);
    return KERN_SUCCESS;
}

When you load and unload the kext, the output like:

kext-memleak: byte allocated 0xffffff8027da0a10 0xef
kext-memleak: byte leaked 0xffffff8027da0a10

The same frustration apply to OSMalloc() family functions:

/**
 * A OSMalloc leakage demo kext
 */
#include <mach/mach_types.h>
#include <sys/systm.h>
#include <libkern/OSMalloc.h>
#include <kern/queue.h>         /* queue_chain_t */

#define INAME "kext-memleak"
#define LOG(fmt, ...) printf(INAME ": " fmt "\n", ##__VA_ARGS__)

static const char *tagname = "f2fc2611-07b1-4a30-af82-3459596b64cb";
static OSMallocTag tag;
static unsigned char *p;

kern_return_t kext_memleak_start(kmod_info_t *ki, void *d)
{
    tag = OSMalloc_Tagalloc(tagname, OSMT_DEFAULT);
    if (tag == NULL) goto out_oom;
    p = OSMalloc(1, tag);

    /*
     * After OSMalloc_Tagalloc and OSMalloc
     * tag->OSMT_refcnt equals to two
     *
     * XXX: following statements extremely dangerous  may panic!
     * see: xnu/libkern/libkern/OSMalloc.h#_OSMallocTag_
     */
    uint32_t *tag_refcnt = (void *) tag + sizeof(queue_chain_t);
    if (*tag_refcnt != 2) {
        LOG("tag->OSMT_refcnt != 2 due to struct alignment");
    } else {
        LOG("tag->OSMT_refcnt = 2");
    }

    LOG("byte allocated %#llx %#x", (uint64_t) p, *p);
    return KERN_SUCCESS;

out_oom:
    LOG("OSMalloc_Tagalloc failure");
    return KERN_FAILURE;
}

kern_return_t kext_memleak_stop(kmod_info_t *ki, void *d)
{
    /*
     * Say, you forget OSFree the memory allocated by OSMalloc
     * Tag won't be freed :. its OSMT_refcnt not down to zero
     */
    OSMalloc_Tagfree(tag);

    LOG("#1 byte leaked %#llx %#x", (uint64_t) p, *p);

    /* OSMalloc will succeed :. tag still alive */
    unsigned char *q = OSMalloc(1, tag);
    LOG("#2 byte leaked %#llx %#x", (uint64_t) q, *q);

    return KERN_SUCCESS;
}

System message output:

kext-memleak: tag->OSMT_refcnt = 2
kext-memleak: byte allocated 0xffffff8018c7b290 0xef
kext-memleak: #1 byte leaked 0xffffff8018c7b290 0xef
kext-memleak: #2 byte leaked 0xffffff8018c7b670 0xef

If you dive into OSMalloc, you’ll find

xnu/osfmk/kern/kalloc.c#OSMalloc_Tagalloc:

OSMallocTag
OSMalloc_Tagalloc(
	const char			*str,
	uint32_t			flags)
{
	OSMallocTag       OSMTag;

	OSMTag = (OSMallocTag)kalloc(sizeof(*OSMTag));

	bzero((void *)OSMTag, sizeof(*OSMTag));

	if (flags & OSMT_PAGEABLE)
		OSMTag->OSMT_attr = OSMT_ATTR_PAGEABLE;

	OSMTag->OSMT_refcnt = 1; /* XXX: Initial refcot one */

	strlcpy(OSMTag->OSMT_name, str, OSMT_MAX_NAME);

	OSMalloc_tag_spin_lock();
	enqueue_tail(&OSMalloc_tag_list, (queue_entry_t)OSMTag);
	OSMalloc_tag_unlock();
	OSMTag->OSMT_state = OSMT_VALID;
	return(OSMTag);
}

xnu/osfmk/kern/kalloc.c#OSMalloc:

void *
OSMalloc(
	uint32_t			size,
	OSMallocTag			tag)
{
	void			*addr=NULL;
	kern_return_t	kr;

	/* XXX: equivalent to tag->OSMT_refcnt++ */
	OSMalloc_Tagref(tag);

	if ((tag->OSMT_attr & OSMT_PAGEABLE)
	    && (size & ~PAGE_MASK)) {
		if ((kr = kmem_alloc_pageable_external(kernel_map, (vm_offset_t *)&addr, size)) != KERN_SUCCESS)
			addr = NULL;
	} else 
		addr = kalloc_tag_bt((vm_size_t)size, VM_KERN_MEMORY_KALLOC);

	if (!addr)
		OSMalloc_Tagrele(tag);

	return(addr);
}

xnu-3789.70.16/osfmk/kern/kalloc.c#OSMalloc_Tagfree:

void
OSMalloc_Tagfree(
	 OSMallocTag		tag)
{
	if (!hw_compare_and_store(OSMT_VALID, OSMT_VALID|OSMT_RELEASED, &tag->OSMT_state))
		panic("OSMalloc_Tagfree():'%s' has bad state 0x%08X \n", tag->OSMT_name, tag->OSMT_state);

	/* XXX: Free the tag iff. OSMT_refcnt down to zero */
	if (hw_atomic_sub(&tag->OSMT_refcnt, 1) == 0) {
		OSMalloc_tag_spin_lock();
		(void)remque((queue_entry_t)tag);
		OSMalloc_tag_unlock();
		kfree((void*)tag, sizeof(*tag));
	}
}

Thus even when you use the OSMalloc family functions won’t prevent you from memory leakage in some ways.


In stead of using untracked _MALLOC family functions, or, refcnt-tracked OSMalloc family functions, you may alternatively implement your own memory-allocation facilities, here is a sane example taken from libkext

static void libkext_mref(int opt)
{
    static volatile SInt64 cnt = 0;
    switch (opt) {
    case 0:
        if (OSDecrementAtomic64(&cnt) > 0) return;
        break;
    case 1:
        if (OSIncrementAtomic64(&cnt) >= 0) return;
        break;
    case 2:
        if (cnt == 0) return;
        break;
    }
    /* You may use DEBUG macro for production */
    panic("\n%s#L%d (potential memleak)  opt: %d cnt: %llu\n",
            __func__, __LINE__, opt, cnt);
}

void *libkext_malloc(size_t size, int flags)
{
    /* _MALLOC `type' parameter is a joke */
    void *addr = _MALLOC(size, M_TEMP, flags);
    if (addr != NULL) libkext_mref(1);
    return addr;
}

void *libkext_realloc(void *addr, size_t size, int flags)
{
    /* XXX: _REALLOC not exported  implement you own */
    void *newaddr = _REALLOC(addr, size, M_TEMP, flags);
    if (!!addr ^ !!newaddr) libkext_mref(!!newaddr);
    return newaddr;
}

void libkext_mfree(void *addr)
{
    if (addr != NULL) libkext_mref(0);
    _FREE(addr, M_TEMP);
}

/* XXX: call when all memory freed */
void libkext_memck(void)
{
    libkext_mref(2);
}

Note that the _REALLOC is not exported by Apple, so you may have no chance to use this function, anyway, you can implement _REALLOC via _MALLOC, that’s how the XNU kernel implement it.

Check

/System/Library/Frameworks/Kernel.framework/Resources/SupportedKPIs-all-archs.txt

/System/Library/Frameworks/Kernel.framework/Resources/SupportedKPIs-x86_64.txt

for a list of universal/x86_64 exported kernel functions.

resolvkpi.sh is a script used to resolve KPI functions.


Here is a bare-bone example show the power of above implementation:

#include <mach/mach_types.h>
#include <sys/systm.h>
#include <libkern/OSAtomic.h>

#define INAME "kext-memleak"
#define LOG(fmt, ...) printf(INAME ": " fmt "\n", ##__VA_ARGS__)

/* Above code snippet omitted */

static unsigned char *nil;
static unsigned char *p;

kern_return_t kext_memleak_start(kmod_info_t *ki, void *d)
{
    nil = libkext_malloc(INT_MAX, M_NOWAIT); /* Should fail */
    p = libkext_malloc(1, M_WAITOK);
    LOG("nil address %#llx", (uint64_t) nil);
    LOG("byte allocated %#llx %#x", (uint64_t) p, *p);
    return KERN_SUCCESS;
}

kern_return_t kext_memleak_stop(kmod_info_t *ki, void *d)
{
    libkext_mfree(nil);
    /* Say, you forget to free the p */

    libkext_memck(); /* Panic if memleaked */
    LOG("byte leaked!!! %#llx %#x", (uint64_t) p, *p);
    return KERN_SUCCESS;
}

When you kextload and kextunload the demo kext, system buffer output:

kext-memleak: nil address 0
kext-memleak: byte allocated 0xffffff8027d67b80 0xef

--- XXX: kernel panic when kextunloading ---

panic(cpu 1 caller 0xffffff7f9f73ed55): 
libkext_mref#L26 (potential memleak)  opt: 2 cnt: 1

Backtrace (CPU 1), Frame : Return Address
0xffffff90b627b6c0 : 0xffffff801d830c64 
0xffffff90b627bb60 : 0xffffff7f9f73ed55 
0xffffff90b627bb80 : 0xffffff7f9f73edae 
0xffffff90b627bb90 : 0xffffff7f9f73ee61 
0xffffff90b627bbc0 : 0xffffff801ddffedf 
0xffffff90b627bbf0 : 0xffffff801ddfd7da 
0xffffff90b627bc30 : 0xffffff801de06bfe 
0xffffff90b627bc70 : 0xffffff801de0c7e4 
0xffffff90b627bcf0 : 0xffffff801de1bcfc 
0xffffff90b627bd70 : 0xffffff801d88bd15 
0xffffff90b627bdc0 : 0xffffff801d8361cc 
0xffffff90b627be20 : 0xffffff801d80d19c 
0xffffff90b627be70 : 0xffffff801d826057 
0xffffff90b627bf00 : 0xffffff801d96db7d 
0xffffff90b627bfb0 : 0xffffff801d7d9db6 
      Kernel Extensions in backtrace:
         cn.junkman.kext-memleak(1.0)[62D705E1-D080-30F7-9AB1-1CA1F37A07D6]@0xffffff7f9f73e000->0xffffff7f9f740fff

BSD process name corresponding to current thread: kextunload
Boot args: debug=0x144 kext-dev-mode=1 pmuflags=1 -v

Mac OS version:
16G29

Kernel version:
Darwin Kernel Version 16.7.0: Thu Jun 15 17:36:27 PDT 2017; root:xnu-3789.70.16~2/DEVELOPMENT_X86_64
Kernel UUID: B8B972B8-220D-3E56-92F8-ADB8CF777CE2
Kernel slide:     0x000000001d400000
Kernel text base: 0xffffff801d600000
__HIB  text base: 0xffffff801d500000
System model name: VMware7,1 (Mac-E43C1C25D4880AD6)

System uptime in nanoseconds: 354232721359
ethernet MAC address: 00:0c:29:af:fa:a0
ip address: 172.16.41.129

Waiting for remote debugger connection.

Which the panic report shows that there is one memory leakage in kern_malloc, panic backtrace:

(lldb) up 0
frame #0: 0xffffff801d98957e kernel.development`Debugger [inlined] hw_atomic_sub(delt=1) at locks.c:1514 [opt]
(lldb) up
frame #1: 0xffffff801d98957e kernel.development`Debugger(message=<unavailable>) at model_dep.c:1016 [opt]
(lldb) 
frame #2: 0xffffff801d830c64 kernel.development`panic(str="\n%s#L%d (potential memleak)  opt: %d cnt: %llu\n") at debug.c:459 [opt]
(lldb) 
frame #3: 0xffffff7f9f73ed55 kext-memleak`libkext_mref(opt=2) at kext_memleak.c:25
   22  	        if (cnt == 0) return;
   23  	        break;
   24  	    }
-> 25  	    panic("\n%s#L%d (potential memleak)  opt: %d cnt: %llu\n",
   26  	            __func__, __LINE__, opt, cnt);
   27  	}
   28  	
(lldb) 
frame #4: 0xffffff7f9f73edae kext-memleak`libkext_memck at kext_memleak.c:54
   51  	/* XXX: call when all memory freed */
   52  	void libkext_memck(void)
   53  	{
-> 54  	    libkext_mref(2);
   55  	}
   56  	
   57  	static unsigned char *nil;
(lldb) 
frame #5: 0xffffff7f9f73ee61 kext-memleak`kext_memleak_stop(ki=0xffffff7f9f73f000, d=0x0000000000000000) at kext_memleak.c:74
   71  	    libkext_mfree(nil);
   72  	    /* Say, you forget to free the p */
   73  	
-> 74  	    libkext_memck(); /* Panic if memleaked */
   75  	    LOG("byte leaked!!! %#llx %#x", (uint64_t) p, *p);
   76  	    return KERN_SUCCESS;
   77  	}

We use the similar technique(e.g. refcnt) as in OSMalloc, yet we balanced simplicity and safety. Put kern_memck as last statement when unloading kext can prevent you from memory leakage.

You certainly mustn’t mix libkext_malloc family with any other mallocs, it’ll debalance libkext_malloc refcnt, and eventually panic when you call libkext_memck.