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.