diff --git a/pwndbg/commands/slab.py b/pwndbg/commands/slab.py index 824edb127..5fe64a6e7 100644 --- a/pwndbg/commands/slab.py +++ b/pwndbg/commands/slab.py @@ -5,9 +5,6 @@ Some of the code here was inspired from https://github.com/NeatMonster/slabdbg """ import argparse import sys -from typing import Iterator -from typing import List -from typing import Union import gdb from tabulate import tabulate @@ -18,7 +15,9 @@ import pwndbg.commands import pwndbg.gdblib.kernel.slab from pwndbg.commands import CommandCategory from pwndbg.gdblib.kernel import kconfig +from pwndbg.gdblib.kernel import krelease from pwndbg.gdblib.kernel import per_cpu +from pwndbg.gdblib.kernel.slab import get_slab_key from pwndbg.gdblib.kernel.slab import oo_objects from pwndbg.gdblib.kernel.slab import oo_order @@ -133,52 +132,75 @@ def _rx(val: int) -> str: def print_slab( - slab: gdb.Value, freelist: Union[Iterator[int], List[int]], indent, verbose, is_partial + slab: gdb.Value, + cpu_cache: gdb.Value, + slab_cache: gdb.Value, + indent, + verbose: bool, + is_partial: bool = False, ) -> None: - page_address = int(slab.address) - virt_address = pwndbg.gdblib.kernel.page_to_virt(page_address) - indent.print(f"- {C.green('Slab')} @ {_yx(virt_address)} [{_rx(page_address)}]:") + slab_address = int(slab.address) + offset = int(slab_cache["offset"]) + random = int(slab_cache["random"]) if "SLAB_FREELIST_HARDENED" in kconfig() else 0 + address = pwndbg.gdblib.kernel.page_to_virt(slab_address) + + indent.print(f"- {C.green('Slab')} @ {_yx(address)} [{_rx(slab_address)}]:") + with indent: if is_partial: + freelists = [list(walk_freelist(slab["freelist"], offset, random))] inuse = slab["inuse"] else: # `freelist` is a generator, we need to evaluate it now and save the # result in case we want to print it later - freelist = list(freelist) + freelists = [ + list(walk_freelist(cpu_cache["freelist"], offset, random)), + list(walk_freelist(slab["freelist"], offset, random)), + ] # `inuse` will always equal `objects` for the active slab, so we # need to subtract the length of the freelist - inuse = int(slab["inuse"]) - len(freelist) + inuse = int(slab["inuse"]) - len(freelists[0]) - indent.print(f"{C.blue('In-Use')}: {inuse}/{slab['objects']}") + objects = int(slab["objects"]) + indent.print(f"{C.blue('In-Use')}: {inuse}/{objects}") indent.print(f"{C.blue('Frozen')}:", slab["frozen"]) indent.print(f"{C.blue('Freelist')}:", _yx(int(slab["freelist"]))) if verbose: with indent: - # TODO: Should I print just free objects or all objects? - for entry in freelist: - indent.print("-", _yx(int(entry))) - - -def print_cpu_cache(cpu_cache, offset, random, cpu_partial, indent, verbose) -> None: - address = int(cpu_cache) - indent.print(f"{C.green('Per-CPU Data')} @ {_yx(address)}:") + size = int(slab_cache["size"]) + for address in range(address, address + objects * size, size): + cur_freelist = next( + (freelist for freelist in freelists if address in freelist), None + ) + if cur_freelist is None: + indent.print("-", hex(int(address)), "(in-use)") + continue + next_free_idx = cur_freelist.index(address) + 1 + next_free = ( + cur_freelist[next_free_idx] if len(cur_freelist) > next_free_idx else 0 + ) + indent.print("-", _yx(int(address)), f"(next: {next_free:#018x})") + + +def print_cpu_cache(cpu_cache: gdb.Value, slab_cache: gdb.Value, verbose: bool, indent) -> None: + indent.print(f"{C.green('Per-CPU Data')} @ {_yx(int(cpu_cache))}:") with indent: freelist = cpu_cache["freelist"] indent.print(f"{C.blue('Freelist')}:", _yx(int(freelist))) - # TODO: Is the `if page:` a null pointer check or something else? - page = cpu_cache["page"] - if page: + slab_key = get_slab_key() + active_slab = cpu_cache[slab_key] + + if active_slab: indent.print(f"{C.green('Active Slab')}:") with indent: - freelist = walk_freelist(freelist, offset, random) print_slab( - page.dereference(), - # Use the CPU cache freelist for the active slab - freelist, + active_slab.dereference(), + cpu_cache, + slab_cache, indent, verbose, is_partial=False, @@ -186,16 +208,35 @@ def print_cpu_cache(cpu_cache, offset, random, cpu_partial, indent, verbose) -> else: indent.print("Active Slab: (none)") - slab = cpu_cache["partial"] - if slab: + partial_slab = cpu_cache["partial"] + if partial_slab: + slabs_key = f"{get_slab_key()}s" + if krelease() >= (5, 16): + # calculate approx obj count in half-full slabs (as done in kernel) + # Note, this is a very bad approximation and could/should probably + # be replaced by a more accurate method os removed from pwndbg + oo = oo_objects(int(slab_cache["oo"]["x"])) + slabs = int(partial_slab[slabs_key]) + pobjects = (slabs * oo) // 2 + else: + pobjects = partial_slab["pobjects"] + + cpu_partial = int(slab_cache["cpu_partial"]) indent.print( - f"{C.green('Partial Slabs')} [{slab['pages']}] [PO: {slab['pobjects']}/{cpu_partial}]:" + f"{C.green('Partial Slabs')} [{partial_slab[slabs_key]}] [PO: ~{pobjects}/{cpu_partial}]:" ) - while slab: - page = slab.dereference() - freelist = walk_freelist(page["freelist"], offset, random) - print_slab(page, freelist, indent, verbose, is_partial=True) - slab = page["next"] + + while partial_slab: + cur_slab = partial_slab.dereference() + print_slab( + cur_slab, + cpu_cache, + slab_cache, + indent, + verbose, + is_partial=True, + ) + partial_slab = cur_slab["next"] else: indent.print("Partial Slabs: (none)") @@ -218,8 +259,7 @@ def slab_info(name: str, verbose: bool) -> None: else: indent.print(f"{C.blue('Flags')}: (none)") - offset = int(cache["offset"]) - indent.print(f"{C.blue('Offset')}:", offset) + indent.print(f"{C.blue('Offset')}:", int(cache["offset"])) indent.print(f"{C.blue('Size')}:", int(cache["size"])) indent.print(f"{C.blue('Align')}:", int(cache["align"])) indent.print(f"{C.blue('Object Size')}:", int(cache["object_size"])) @@ -227,11 +267,7 @@ def slab_info(name: str, verbose: bool) -> None: # TODO: Handle multiple CPUs cpu_cache = per_cpu(cache["cpu_slab"]) - random = 0 - if "SLAB_FREELIST_HARDENED" in kconfig(): - random = int(cache["random"]) - - print_cpu_cache(cpu_cache, offset, random, int(cache["cpu_partial"]), indent, verbose) + print_cpu_cache(cpu_cache, cache, verbose, indent) # TODO: print_node_cache diff --git a/pwndbg/gdblib/kernel/slab.py b/pwndbg/gdblib/kernel/slab.py index 7f87c76d1..6aebf58bf 100644 --- a/pwndbg/gdblib/kernel/slab.py +++ b/pwndbg/gdblib/kernel/slab.py @@ -16,6 +16,15 @@ def get_cache(target_name: str): return slab_cache +def get_slab_key() -> str: + # In Linux kernel version 5.17 a slab struct was introduced instead of the previous page struct + try: + gdb.lookup_type("struct slab") + return "slab" + except gdb.error: + return "page" + + OO_SHIFT = 16 OO_MASK = (1 << OO_SHIFT) - 1 diff --git a/tests/qemu-tests/tests/system/test_commands_kernel.py b/tests/qemu-tests/tests/system/test_commands_kernel.py index 1a0de634b..d02d6310b 100644 --- a/tests/qemu-tests/tests/system/test_commands_kernel.py +++ b/tests/qemu-tests/tests/system/test_commands_kernel.py @@ -51,4 +51,14 @@ def test_command_slab_list(): def test_command_slab_info(): - pass # TODO + if not pwndbg.gdblib.kernel.has_debug_syms(): + res = gdb.execute("slab info kmalloc-512", to_string=True) + assert "may only be run when debugging a Linux kernel with debug" in res + return + + res = gdb.execute("slab info -v kmalloc-512", to_string=True) + assert "kmalloc-512" in res + assert "Freelist" in res + + res = gdb.execute("slab info -v does_not_exit", to_string=True) + assert "not found" in res