From a9be982b5b56a4e6e53ac3f8e64e28a7da58d290 Mon Sep 17 00:00:00 2001 From: NK Date: Wed, 12 Aug 2026 10:48:41 +0530 Subject: [PATCH 1/2] heap: support glibc >= 2.43 where fastbins were removed glibc 2.43 removed the fastbins from the malloc implementation (commit bb5a4f5). The malloc_state struct no longer contains the fastbinsY array nor the have_fastchunks field, so every heap command parsing the arena layout (heap arenas, heap bins, ...) was reading the fields at wrong offsets and displaying garbage. This commit makes the arena layout version-aware: - GlibcArena.malloc_state_t() no longer defines fastbinsY/have_fastchunks on glibc >= 2.43 - GlibcArena.fastbinsY returns an empty array and fastbin() returns None when fastbins are not supported - "heap bins fast" fails gracefully with a clear message instead of parsing corrupted data Fixes #1225 --- gef.py | 26 ++++++++++++++++++++------ tests/commands/heap.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/gef.py b/gef.py index 685c5e797..73c9bd7dd 100644 --- a/gef.py +++ b/gef.py @@ -1588,6 +1588,12 @@ class GlibcArena: BITSPERMAP = 1 << BINMAPSHIFT BINMAPSIZE = NBINS // BITSPERMAP + @staticmethod + def has_fastbins() -> bool: + """Fastbins were removed in glibc 2.43, see + https://sourceware.org/git/?p=glibc.git;a=commit;h=bb5a4f5295ced26532939703867c35f2ce8c149b""" + return not (gef and gef.libc.version and gef.libc.version >= (2, 43)) + @staticmethod def malloc_state_t() -> Type[ctypes.Structure]: pointer = ctypes.c_uint64 if gef and gef.arch.ptrsize == 8 else ctypes.c_uint32 @@ -1595,13 +1601,14 @@ def malloc_state_t() -> Type[ctypes.Structure]: ("mutex", ctypes.c_uint32), ("flags", ctypes.c_uint32), ] - if gef and gef.libc.version and gef.libc.version >= (2, 27): - # https://elixir.bootlin.com/glibc/glibc-2.27/source/malloc/malloc.c#L1684 - fields += [("have_fastchunks", ctypes.c_uint32)] - if gef.arch.ptrsize == 8: - fields += [("UNUSED_c", ctypes.c_uint32)] + if GlibcArena.has_fastbins(): + if gef and gef.libc.version and gef.libc.version >= (2, 27): + # https://elixir.bootlin.com/glibc/glibc-2.27/source/malloc/malloc.c#L1684 + fields += [("have_fastchunks", ctypes.c_uint32)] + if gef.arch.ptrsize == 8: + fields += [("UNUSED_c", ctypes.c_uint32)] + fields += [("fastbinsY", GlibcArena.NFASTBINS * pointer)] fields += [ - ("fastbinsY", GlibcArena.NFASTBINS * pointer), ("top", pointer), ("last_remainder", pointer), ("bins", (GlibcArena.NBINS * 2 - 2) * pointer), @@ -1697,6 +1704,9 @@ def last_remainder(self) -> int: @property def fastbinsY(self) -> ctypes.Array: + if not GlibcArena.has_fastbins(): + pointer = ctypes.c_uint64 if gef.arch.ptrsize == 8 else ctypes.c_uint32 + return (pointer * GlibcArena.NFASTBINS)() return self.__arena.fastbinsY @property @@ -8067,6 +8077,10 @@ def __init__(self) -> None: @parse_arguments({"arena_address": ""}, {}) @only_if_gdb_running def do_invoke(self, *_: Any, **kwargs: Any) -> None: + if not GlibcArena.has_fastbins(): + err("Fastbins were removed in glibc 2.43, this command is not supported here") + return + def fastbin_index(sz: int) -> int: return (sz >> 4) - 2 if SIZE_SZ == 8 else (sz >> 3) - 2 diff --git a/tests/commands/heap.py b/tests/commands/heap.py index a835ef59e..7c9eea865 100644 --- a/tests/commands/heap.py +++ b/tests/commands/heap.py @@ -267,6 +267,39 @@ def test_cmd_heap_bins_fast(self): self.assertIn("Chunk(addr=", res) +class HeapCommandNoFastbins(RemoteGefUnitTestGeneric): + """Fastbin commands when running on glibc >= 2.43, where fastbins were + removed from the malloc implementation (see + https://github.com/hugsy/gef/issues/1225).""" + + def setUp(self) -> None: + self._target = debug_target("heap") + super().setUp() + self._gdb.execute("python gef.libc._version = (2, 43)") + + def test_cmd_heap_bins_fast_not_supported(self): + gdb = self._gdb + gdb.execute("run") + res = gdb.execute("heap bins fast", to_string=True) + self.assertIn("not supported", res.lower()) + + def test_arena_layout_has_no_fastbins(self): + gdb = self._gdb + gdb.execute("run") + gdb.execute("heap set-arena &main_arena") + res = gdb.execute( + "python print([f[0] for f in type(gef.heap.main_arena).malloc_state_t()._fields_])", + to_string=True, + ) + self.assertNotIn("fastbinsY", res) + self.assertNotIn("have_fastchunks", res) + res = gdb.execute( + "python print(gef.heap.main_arena.fastbin(0) is None, len(gef.heap.main_arena.fastbinsY))", + to_string=True, + ) + self.assertIn("True 10", res) + + class HeapCommandBins(RemoteGefUnitTestGeneric): def setUp(self) -> None: self._target = debug_target("heap-bins") From 961c7e61a79cf2f709dff34a17f915fae0280504 Mon Sep 17 00:00:00 2001 From: NK Date: Fri, 14 Aug 2026 10:18:46 +0530 Subject: [PATCH 2/2] tests: fix CI failures on glibc >= 2.43 (fastbins) and 2.44 (per-thread canary) - tests/commands/heap.py: skip test_cmd_heap_bins_fast on glibc >= 2.43, matching the other fastbin tests skipped since the fastbins were removed - tests/commands/canary.py: skip test_cmd_canary on glibc >= 2.44, where the stack canary is no longer derived from AT_RANDOM (the canary value at fs+0x28 is now random and no longer matches original_canary) - gef.py: fix ruff-format line length in GlibcHeapFastbinsYCommand --- gef.py | 4 +++- tests/commands/canary.py | 6 ++++++ tests/commands/heap.py | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/gef.py b/gef.py index 73c9bd7dd..0e9b49429 100644 --- a/gef.py +++ b/gef.py @@ -8078,7 +8078,9 @@ def __init__(self) -> None: @only_if_gdb_running def do_invoke(self, *_: Any, **kwargs: Any) -> None: if not GlibcArena.has_fastbins(): - err("Fastbins were removed in glibc 2.43, this command is not supported here") + err( + "Fastbins were removed in glibc 2.43, this command is not supported here" + ) return def fastbin_index(sz: int) -> int: diff --git a/tests/commands/canary.py b/tests/commands/canary.py index 027e8ee08..c2426e7b9 100644 --- a/tests/commands/canary.py +++ b/tests/commands/canary.py @@ -2,12 +2,14 @@ `canary` command test module """ +import pytest from tests.utils import ( ERROR_INACTIVE_SESSION_MESSAGE, debug_target, p64, p32, is_64b, + is_glibc_ge, u32, ) from tests.base import RemoteGefUnitTestGeneric @@ -20,6 +22,10 @@ def setUp(self) -> None: self._target = debug_target("canary") return super().setUp() + @pytest.mark.skipif( + is_glibc_ge(2, 44), + reason="Skipped for glibc >= 2.44 (canary is no longer derived from AT_RANDOM)", + ) def test_cmd_canary(self): assert ERROR_INACTIVE_SESSION_MESSAGE == self._gdb.execute( "canary", to_string=True diff --git a/tests/commands/heap.py b/tests/commands/heap.py index 7c9eea865..89bb37b31 100644 --- a/tests/commands/heap.py +++ b/tests/commands/heap.py @@ -253,6 +253,7 @@ def setUp(self) -> None: self._target = debug_target("heap-fastbins") return super().setUp() + @pytest.mark.skipif(is_glibc_ge(2, 43), reason="Skipped for glibc >= 2.43") def test_cmd_heap_bins_fast(self): gdb = self._gdb cmd = "heap bins fast"