From 9c954669035a12ebe2b0ebcc3293c3e1e104ba45 Mon Sep 17 00:00:00 2001 From: k4lizen <124312252+k4lizen@users.noreply.github.com> Date: Sat, 13 Dec 2025 03:59:41 +0100 Subject: [PATCH] Make mallocng tests pass on fedora (#3491) * cleanup mallocng tests * lint --- pwndbg/aglib/heap/mallocng.py | 31 ++++++++++---- tests/binaries/host/heap_musl.native.c | 18 ++++++--- tests/library/dbg/tests/test_mallocng.py | 51 ++++++++++++------------ 3 files changed, 62 insertions(+), 38 deletions(-) diff --git a/pwndbg/aglib/heap/mallocng.py b/pwndbg/aglib/heap/mallocng.py index 006f63c7b..6e2845321 100644 --- a/pwndbg/aglib/heap/mallocng.py +++ b/pwndbg/aglib/heap/mallocng.py @@ -19,7 +19,9 @@ import pwndbg.aglib.heap.heap import pwndbg.aglib.memory as memory import pwndbg.aglib.stack import pwndbg.aglib.typeinfo +import pwndbg.auxv import pwndbg.color.message as message +import pwndbg.search # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L14 # Slot granularity. @@ -1055,19 +1057,19 @@ class MallocContext: assert len(size_classes) == 48 - for i in range(len(size_classes)): + for _ in range(len(size_classes)): cur_active = next_int(ptrsize) self.active.append(cur_active) - for i in range(len(size_classes)): + for _ in range(len(size_classes)): cur_usage = next_int(size_tsize) self.usage_by_class.append(cur_usage) - for i in range(32): + for _ in range(32): cur_seq = next_int(uint8size) self.unmap_seq.append(cur_seq) - for i in range(32): + for _ in range(32): cur_bounce = next_int(uint8size) self.bounces.append(cur_bounce) @@ -1213,7 +1215,11 @@ class Mallocng(pwndbg.aglib.heap.heap.MemoryAllocator): if pwndbg.dbg.selected_inferior().is_dynamically_linked(): for addr, mapname in possible: - if mapname.endswith("libc.so"): + # Unlike glibc, for musl the libc and ld are loaded as one object file. + # On some distro's the object file shows up as libc + # (e.g. /usr/lib/musl/lib/libc.so on archlinux) and on some as ld + # (e.g. /usr/lib/ld-musl-x86_64.so.1 on fedora). + if mapname.endswith("libc.so") or "/ld-musl-" in mapname: maybe_ctx = MallocContext(addr) if maybe_ctx.looks_valid(): self.ctx_addr = addr @@ -1223,7 +1229,18 @@ class Mallocng(pwndbg.aglib.heap.heap.MemoryAllocator): known_invalid.add(addr) for addr, mapname in possible: - if "libc" in mapname and addr not in known_invalid: + if addr in known_invalid: + continue + + # It is probably better for this matching to be overly eager than overly restrictive, + # we can fix it later if we ever actually encounter false positives. + # The .startswith() logic is there for CTF-type setups when the libc/ld is in the same folder as the binary. + if ( + "/libc" in mapname + or "/ld-" in mapname + or mapname.startswith("libc") + or mapname.startswith("ld-") + ): maybe_ctx = MallocContext(addr) if maybe_ctx.looks_valid(): self.ctx_addr = addr @@ -1234,7 +1251,7 @@ class Mallocng(pwndbg.aglib.heap.heap.MemoryAllocator): print( message.warn( - "Couldn't find __malloc_context in a 'libc' mapping, trying elsewhere." + "Couldn't find __malloc_context in a 'libc' or 'ld-' mapping, trying elsewhere." ) ) else: diff --git a/tests/binaries/host/heap_musl.native.c b/tests/binaries/host/heap_musl.native.c index e490a4eb7..5ca592275 100644 --- a/tests/binaries/host/heap_musl.native.c +++ b/tests/binaries/host/heap_musl.native.c @@ -4,17 +4,23 @@ void break_here() {}; int main () { - char* buffer1 = malloc(0x20); - char* buffer2 = malloc(0x20); - char* buffer3 = malloc(0x20); + // In some environments (e.g. on fedora 41), musl will allocate a slot with a string like + // "/usr/x86_64-linux-musl/lib64" while on some environments (like archlinux) this string + // won't be allocated at all (it seems the `sys_path` symbol points to this string). We make + // sure to allocate 0x50 so we don't end up in the same group as that string to have consistent + // tests. We also need to be careful about slots with slack because they may bring indeterminism + // (e.g. between the static and dynamic run). + char* buffer1 = malloc(0x50); + char* buffer2 = malloc(0x50); + char* buffer3 = malloc(0x50); char* buffer4 = malloc(0x211); char* buffer5 = malloc(0x211); break_here(); - memset(buffer1, 0xA, 0x20); - memset(buffer2, 0xB, 0x20); - memset(buffer3, 0xC, 0x20); + memset(buffer1, 0xA, 0x50); + memset(buffer2, 0xB, 0x50); + memset(buffer3, 0xC, 0x50); memset(buffer4, 0xD, 0x211); memset(buffer5, 0xE, 0x211); diff --git a/tests/library/dbg/tests/test_mallocng.py b/tests/library/dbg/tests/test_mallocng.py index ca477f05f..e0360c15e 100644 --- a/tests/library/dbg/tests/test_mallocng.py +++ b/tests/library/dbg/tests/test_mallocng.py @@ -44,8 +44,8 @@ async def test_mallocng_slot_user(ctrl: Controller, binary: str): f" start: {re_addr} ", f" user start: {re_addr} aka `p`", rf" end: {re_addr} start \+ stride - 4", - " stride: 0x30 distance between adjacent slots", - """ user size: 0x20 aka "nominal size", `n`""", + " stride: 0x60 distance between adjacent slots", + """ user size: 0x50 aka "nominal size", `n`""", r" slack: 0x0 \(0x0\) slot's unused memory \/ 0x10", " state: allocated ", "in-band", @@ -77,15 +77,15 @@ async def test_mallocng_slot_user(ctrl: Controller, binary: str): cyclic_idx = 17 # Check stride - assert "stride" in buffer2_out[stride_idx] and " 0x30 " in buffer2_out[stride_idx] + assert "stride" in buffer2_out[stride_idx] and " 0x60 " in buffer2_out[stride_idx] assert "stride" in buffer4_out[stride_idx] and " 0x2a0 " in buffer4_out[stride_idx] # Check user size - assert "user size" in buffer2_out[user_size_idx] and " 0x20 " in buffer2_out[user_size_idx] + assert "user size" in buffer2_out[user_size_idx] and " 0x50 " in buffer2_out[user_size_idx] assert "user size" in buffer4_out[user_size_idx] and " 0x211 " in buffer4_out[user_size_idx] # Check slack - assert "slack" in buffer2_out[slack_idx] and " 0x0 " in buffer2_out[slack_idx] + assert "slack" in buffer2_out[slack_idx] and " 0x0 (0x0)" in buffer2_out[slack_idx] assert "slack" in buffer4_out[slack_idx] and " 0x8 (0x80) " in buffer4_out[slack_idx] # Check allocation status @@ -93,7 +93,7 @@ async def test_mallocng_slot_user(ctrl: Controller, binary: str): assert "state" in buffer4_out[state_idx] and " allocated " in buffer4_out[state_idx] # Check offset - assert "offset" in buffer2_out[offset_idx] and " 0x3 (0x30) " in buffer2_out[offset_idx] + assert "offset" in buffer2_out[offset_idx] and " 0x6 (0x60) " in buffer2_out[offset_idx] if binary == HEAP_MALLOCNG_STATIC: # Because it's cyclic assert "offset" in buffer4_out[offset_idx] and " 0x1 (0x10) " in buffer4_out[offset_idx] @@ -218,7 +218,7 @@ async def test_mallocng_group(ctrl: Controller, binary: str): "group", f" @ {re_addr} - {re_addr}", f" meta: {re_addr} ", - " active_idx: 0x9 ", + " active_idx: 0x4 ", f" storage: {re_addr} start of slots", "---", " group size: 0x1f0 ", @@ -227,16 +227,16 @@ async def test_mallocng_group(ctrl: Controller, binary: str): f" prev: {re_addr} ", f" next: {re_addr} ", f" mem: {re_addr} the group", - " avail_mask: 0x3f8 0b00000000000000000000001111111000", + " avail_mask: 0x18 0b00000000000000000000000000011000", " freed_mask: 0x0 0b00000000000000000000000000000000", - r" last_idx: 0x9 \(cnt: 0xa\) index of last slot", + r" last_idx: 0x4 \(cnt: 0x5\) index of last slot", " freeable: True ", - r" sizeclass: 0x2 \(stride: 0x30\) ", + r" sizeclass: 0x5 \(stride: 0x60\) ", " maplen: 0x0 ", "", rf"Group nested in slot of another group \({re_addr}\).", "", - "Slot statuses: UUUAAAAAAA", + "Slot statuses: UUUAA", r" \(U: Inuse \(allocated\) / F: Freed / A: Available\)", ] @@ -440,13 +440,23 @@ async def test_mallocng_vis(ctrl: Controller, binary: str): "LEGEND: .*", "LEGEND: .*", "", - rf"{re_addr}0\t0x[0-9a-fA-F]{{16}}\t0x0000ff0000000009\t................", + # the dots match anything but w/e + rf"{re_addr}0\t0x[0-9a-fA-F]{{16}}\t0x0000ff0000000004\t................", + rf"{re_addr}0\t0x0a0a0a0a0a0a0a0a\t0x0a0a0a0a0a0a0a0a\t................", + rf"{re_addr}0\t0x0a0a0a0a0a0a0a0a\t0x0a0a0a0a0a0a0a0a\t................", + rf"{re_addr}0\t0x0a0a0a0a0a0a0a0a\t0x0a0a0a0a0a0a0a0a\t................", rf"{re_addr}0\t0x0a0a0a0a0a0a0a0a\t0x0a0a0a0a0a0a0a0a\t................", rf"{re_addr}0\t0x0a0a0a0a0a0a0a0a\t0x0a0a0a0a0a0a0a0a\t................", rf"{re_addr}0\t0x0000000000000000\t0x0000ff000000000c\t................", rf"{re_addr}0\t0x0b0b0b0b0b0b0b0b\t0x0b0b0b0b0b0b0b0b\t................", rf"{re_addr}0\t0x0b0b0b0b0b0b0b0b\t0x0b0b0b0b0b0b0b0b\t................", - rf"{re_addr}0\t0x0000000000000000\t0x0006a2000000000c\t................ 2 \+ \(5 << 5\)", + rf"{re_addr}0\t0x0b0b0b0b0b0b0b0b\t0x0b0b0b0b0b0b0b0b\t................", + rf"{re_addr}0\t0x0b0b0b0b0b0b0b0b\t0x0b0b0b0b0b0b0b0b\t................", + rf"{re_addr}0\t0x0b0b0b0b0b0b0b0b\t0x0b0b0b0b0b0b0b0b\t................", + rf"{re_addr}0\t0x0000000000000000\t0x000ca2000000000c\t................ 2 \+ \(5 << 5\)", + rf"{re_addr}0\t0x0c0c0c0c0c0c0c0c\t0x0c0c0c0c0c0c0c0c\t................", + rf"{re_addr}0\t0x0c0c0c0c0c0c0c0c\t0x0c0c0c0c0c0c0c0c\t................", + rf"{re_addr}0\t0x0c0c0c0c0c0c0c0c\t0x0c0c0c0c0c0c0c0c\t................", rf"{re_addr}0\t0x0c0c0c0c0c0c0c0c\t0x0c0c0c0c0c0c0c0c\t................", rf"{re_addr}0\t0x0c0c0c0c0c0c0c0c\t0x0c0c0c0c0c0c0c0c\t................", rf"{re_addr}0\t0x0000000000000000\t0x000000000000000c\t................", @@ -462,15 +472,6 @@ async def test_mallocng_vis(ctrl: Controller, binary: str): rf"{re_addr}0\t0x0000000000000000\t0x0000000000000000\t................", rf"{re_addr}0\t0x0000000000000000\t0x0000000000000000\t................", rf"{re_addr}0\t0x0000000000000000\t0x0000000000000000\t................", - rf"{re_addr}0\t0x0000000000000000\t0x0000000000000000\t................", - rf"{re_addr}0\t0x0000000000000000\t0x0000000000000000\t................", - rf"{re_addr}0\t0x0000000000000000\t0x0000000000000000\t................", - rf"{re_addr}0\t0x0000000000000000\t0x0000000000000000\t................", - rf"{re_addr}0\t0x0000000000000000\t0x0000000000000000\t................", - rf"{re_addr}0\t0x0000000000000000\t0x0000000000000000\t................", - rf"{re_addr}0\t0x0000000000000000\t0x0000000000000000\t................", - rf"{re_addr}0\t0x0000000000000000\t0x0000000000000000\t................", - rf"{re_addr}0\t0x0000000000000000\t0x0000000000000000\t................", ] assert len(expected_out) == len(vis_out) @@ -502,8 +503,8 @@ async def test_mallocng_dump(ctrl: Controller, binary: str): dump_out = await ctrl.execute_and_capture("ng-dump") assert "meta_area" in dump_out assert "group @" in dump_out - assert "(slot size: 0x30)" in dump_out # buffer{1,2,3} + assert "(slot size: 0x60)" in dump_out # buffer{1,2,3} assert "(slot size: 0x2a0)" in dump_out # buffer{4,5} - # 10 slots in the buffer{1,2,3} group. - for idx in range(10): + # 5 slots in the buffer{1,2,3} group. + for idx in range(5): assert f"[{idx}]" in dump_out