X-Git-Url: https://git.saurik.com/apple/xnu.git/blobdiff_plain/4d15aeb193b2c68f1d38666c317f8d3734f5f083..5ba3f43ea354af8ad55bea84372a2bc834d8757c:/tools/lldbmacros/kasan.py diff --git a/tools/lldbmacros/kasan.py b/tools/lldbmacros/kasan.py new file mode 100755 index 000000000..2142fa7ac --- /dev/null +++ b/tools/lldbmacros/kasan.py @@ -0,0 +1,345 @@ +from xnu import * +from utils import * +from core.configuration import * + +shift = None + +shadow_strings = { + 0x00: 'VALID', + 0x01: 'PARTIAL1', + 0x02: 'PARTIAL2', + 0x03: 'PARTIAL3', + 0x04: 'PARTIAL4', + 0x05: 'PARTIAL5', + 0x06: 'PARTIAL6', + 0x07: 'PARTIAL7', + 0xac: 'ARRAY_COOKIE', + 0xf0: 'STACK_RZ', + 0xf1: 'STACK_LEFT_RZ', + 0xf2: 'STACK_MID_RZ', + 0xf3: 'STACK_RIGHT_RZ', + 0xf5: 'STACK_FREED', + 0xf8: 'STACK_OOSCOPE', + 0xf9: 'GLOBAL_RZ', + 0xe9: 'HEAP_RZ', + 0xfa: 'HEAP_LEFT_RZ', + 0xfb: 'HEAP_RIGHT_RZ', + 0xfd: 'HEAP_FREED' +} + +def is_kasan_build(): + try: + enable = kern.globals.kasan_enabled + return True + except ValueError, e: + return False + +def shadow_for_address(addr, shift): + return ((addr >> 3) + shift) + +def address_for_shadow(addr, shift): + return ((addr - shift) << 3) + +def get_shadow_byte(shadow_addr): + return unsigned(kern.GetValueFromAddress(shadow_addr, 'uint8_t *')[0]) + +def print_legend(): + for (k,v) in shadow_strings.iteritems(): + print " {:02x}: {}".format(k,v) + +def print_shadow_context(addr, context): + addr = shadow_for_address(addr, shift) + base = (addr & ~0xf) - 16 * context + shadow = kern.GetValueFromAddress(unsigned(base), "uint8_t *") + + print " "*17 + " 0 1 2 3 4 5 6 7 8 9 a b c d e f" + for x in range(0, 2*context+1): + vals = "" + l = " " + for y in xrange(x*16, (x+1)*16): + r = " " + if base+y == addr: + l = "[" + r = "]" + elif base+y+1 == addr: + r = "" + sh = shadow[y] + vals += "{}{:02x}{}".format(l, sh, r) + l = "" + print("{:x}:{}".format(base + 16*x, vals)) + +kasan_guard_size = 16 +def print_alloc_free_entry(addr, orig_ptr): + h = kern.GetValueFromAddress(addr, 'struct freelist_entry *') + asz = unsigned(h.size) + usz = unsigned(h.user_size) + pgsz = unsigned(kern.globals.page_size) + + if h.zone: + zone = h.zone + if str(zone.zone_name).startswith("fakestack"): + alloc_type = "fakestack" + leftrz = 16 + else: + alloc_type = "zone" + leftrz = unsigned(zone.kasan_redzone) + else: + alloc_type = "kalloc" + if asz - usz >= 2*pgsz: + leftrz = pgsz + else: + leftrz = kasan_guard_size + + rightrz = asz - usz - leftrz + + print "Freed {} object".format(alloc_type) + print "Valid range: 0x{:x} -- 0x{:x} ({} bytes)".format(addr + leftrz, addr + leftrz + usz - 1, usz) + print "Total range: 0x{:x} -- 0x{:x} ({} bytes)".format(addr, addr + asz - 1, asz) + print "Offset: {} bytes".format(orig_ptr - addr - leftrz) + print "Redzone: {} / {} bytes".format(leftrz, rightrz) + if h.zone: + print "Zone: 0x{:x} <{:s}>".format(unsigned(zone), zone.zone_name) + + btframes = unsigned(h.frames) + if btframes > 0: + print "", + print "Free site backtrace ({} frames):".format(btframes) + for i in xrange(0, btframes): + fr = unsigned(kern.globals.vm_kernel_slid_base) + unsigned(h.backtrace[i]) + print " #{:}: {}".format(btframes-i-1, GetSourceInformationForAddress(fr)) + + print "", + print_hexdump(addr, asz, 0) + +alloc_header_sz = 16 + +def print_alloc_info(_addr): + addr = (_addr & ~0x7) + + _shp = shadow_for_address(_addr, shift) + _shbyte = get_shadow_byte(_shp) + _shstr = shadow_byte_to_string(_shbyte) + + # If we're in a left redzone, scan to the start of the real allocation, where + # the header should live + shbyte = _shbyte + while shbyte == 0xfa: + addr += 8 + shbyte = get_shadow_byte(shadow_for_address(addr, shift)) + + # Search backwards for an allocation + searchbytes = 0 + while searchbytes < 8*4096: + + shp = shadow_for_address(addr, shift) + shbyte = get_shadow_byte(shp) + shstr = shadow_byte_to_string(shbyte) + + headerp = addr - alloc_header_sz + liveh = kern.GetValueFromAddress(headerp, 'struct kasan_alloc_header *') + freeh = kern.GetValueFromAddress(addr, 'struct freelist_entry *') + + # heap allocations should only ever have these shadow values + if shbyte not in (0,1,2,3,4,5,6,7, 0xfa, 0xfb, 0xfd, 0xf5): + print "No allocation found at 0x{:x} (found shadow {:x})".format(_addr, shbyte) + return + + live_magic = (addr & 0xffffffff) ^ 0xA110C8ED + free_magic = (addr & 0xffffffff) ^ 0xF23333D + + if live_magic == unsigned(liveh.magic): + usz = unsigned(liveh.user_size) + asz = unsigned(liveh.alloc_size) + leftrz = unsigned(liveh.left_rz) + base = headerp + alloc_header_sz - leftrz + + if _addr >= base and _addr < base + asz: + footer = kern.GetValueFromAddress(addr + usz, 'struct kasan_alloc_footer *') + rightrz = asz - usz - leftrz + + print "Live heap object" + print "Valid range: 0x{:x} -- 0x{:x} ({} bytes)".format(addr, addr + usz - 1, usz) + print "Total range: 0x{:x} -- 0x{:x} ({} bytes)".format(base, base + asz - 1, asz) + print "Offset: {} bytes (shadow: 0x{:02x} {})".format(_addr - addr, _shbyte, _shstr) + print "Redzone: {} / {} bytes".format(leftrz, rightrz) + + btframes = unsigned(liveh.frames) + print "", + print "Alloc site backtrace ({} frames):".format(btframes) + for i in xrange(0, btframes): + fr = unsigned(kern.globals.vm_kernel_slid_base) + unsigned(footer.backtrace[i]) + print " #{:}: {}".format(btframes-i-1, GetSourceInformationForAddress(fr)) + + print "", + print_hexdump(base, asz, 0) + return + + elif free_magic == unsigned(freeh.magic): + asz = unsigned(freeh.size) + if _addr >= addr and _addr < addr + asz: + print_alloc_free_entry(addr, _addr) + return + + searchbytes += 8 + addr -= 8 + + print "No allocation found at 0x{:x}".format(_addr) + +def shadow_byte_to_string(sb): + return shadow_strings.get(sb, '??') + +def print_whatis(_addr, ctx): + addr = _addr & ~0x7 + total_size = 0 + base = None + leftrz = None + rightrz = None + extra = "Live" + + shbyte = get_shadow_byte(shadow_for_address(addr, shift)) + maxsearch = 4096 * 2 + + if shbyte in [0xfa, 0xfb, 0xfd, 0xf5]: + print_alloc_info(_addr) + return + + if shbyte not in [0,1,2,3,4,5,6,7,0xf8]: + print "Poisoned memory, shadow {:x} [{}]".format(shbyte, shadow_byte_to_string(shbyte)) + return + + if shbyte is 0xf8: + extra = "Out-of-scope" + + # look for the base of the object + while shbyte in [0,1,2,3,4,5,6,7,0xf8]: + sz = 8 - shbyte + if shbyte is 0xf8: + sz = 8 + total_size += sz + addr -= 8 + shbyte = get_shadow_byte(shadow_for_address(addr, shift)) + maxsearch -= 8 + if maxsearch <= 0: + print "No object found" + return + base = addr + 8 + leftrz = shbyte + + # If we did not find a left/mid redzone, we aren't in an object + if leftrz not in [0xf1, 0xf2, 0xfa, 0xf9]: + print "No object found" + return + + # now size the object + addr = (_addr & ~0x7) + 8 + shbyte = get_shadow_byte(shadow_for_address(addr, shift)) + while shbyte in [0,1,2,3,4,5,6,7,0xf8]: + sz = 8 - shbyte + if shbyte is 0xf8: + sz = 8 + total_size += sz + addr += 8 + shbyte = get_shadow_byte(shadow_for_address(addr, shift)) + maxsearch -= 8 + if maxsearch <= 0: + print "No object found" + return + rightrz = shbyte + + # work out the type of the object from its redzone + objtype = "Unknown" + if leftrz == 0xf1 or leftrz == 0xf2: + objtype = "stack" + elif leftrz == 0xf9 and rightrz == 0xf9: + objtype = "global" + elif leftrz == 0xfa and rightrz == 0xfb: + print_alloc_info(_addr) + return + + print "{} {} object".format(extra, objtype) + print "Valid range: 0x{:x} -- 0x{:x} ({} bytes)".format(base, base+total_size-1, total_size) + print "Offset: {} bytes".format(_addr - base) + print "", + print_hexdump(base, total_size, 0) + +def print_hexdump(base, size, ctx): + start = base - 16*ctx + size += size % 16 + size = min(size + 16*2*ctx, 256) + + try: + data_array = kern.GetValueFromAddress(start, "uint8_t *") + print_hex_data(data_array[0:size], start, "Hexdump") + except: + pass + +def kasan_subcommand(cmd, args, opts): + addr = None + if len(args) > 0: + addr = long(args[0], 0) + + if cmd in ['a2s', 'toshadow', 'fromaddr', 'fromaddress']: + print "0x{:016x}".format(shadow_for_address(addr, shift)) + elif cmd in ['s2a', 'toaddr', 'toaddress', 'fromshadow']: + print "0x{:016x}".format(address_for_shadow(addr, shift)) + elif cmd == 'shadow': + shadow = shadow_for_address(addr, shift) + sb = get_shadow_byte(shadow) + print("0x{:02x} @ 0x{:016x} [{}]\n\n".format(sb, shadow, shadow_byte_to_string(sb))) + ctx = long(opts.get("-C", 5)) + print_shadow_context(addr, ctx) + elif cmd == 'legend': + print_legend() + elif cmd == 'info': + pages_used = unsigned(kern.globals.shadow_pages_used) + pages_total = unsigned(kern.globals.shadow_pages_total) + nkexts = unsigned(kern.globals.kexts_loaded) + print "Offset: 0x{:016x}".format(shift) + print "Shadow used: {} / {} ({:.1f}%)".format(pages_used, pages_total, 100.0*pages_used/pages_total) + print "Kexts loaded: {}".format(nkexts) + elif cmd == 'whatis': + ctx = long(opts.get("-C", 1)) + print_whatis(addr, ctx) + elif cmd == 'alloc' or cmd == 'heap': + print_alloc_info(addr) + +@lldb_command('kasan', 'C:') +def Kasan(cmd_args=None, cmd_options={}): + """kasan [opts..] + + Commands: + + info basic KASan information + shadow print shadow around 'addr' + heap show info about heap object at 'addr' + whatis print whatever KASan knows about address + toshadow convert address to shadow pointer + toaddr convert shadow pointer to address + legend print a shadow byte table + + -C : num lines of context to show""" + + if not is_kasan_build(): + print "KASan not enabled in build" + return + + if len(cmd_args) == 0: + print Kasan.__doc__ + return + + global shift + shift = unsigned(kern.globals.__asan_shadow_memory_dynamic_address) + + # Since the VM is not aware of the KASan shadow mapping, accesses to it will + # fail. Setting kdp_read_io=1 avoids this check. + if GetConnectionProtocol() == "kdp" and unsigned(kern.globals.kdp_read_io) == 0: + print "Setting kdp_read_io=1 to allow KASan shadow reads" + if sizeof(kern.globals.kdp_read_io) == 4: + WriteInt32ToMemoryAddress(1, addressof(kern.globals.kdp_read_io)) + elif sizeof(kern.globals.kdp_read_io) == 8: + WriteInt64ToMemoryAddress(1, addressof(kern.globals.kdp_read_io)) + readio = unsigned(kern.globals.kdp_read_io) + assert readio == 1 + + return kasan_subcommand(cmd_args[0], cmd_args[1:], cmd_options) +