heap: support glibc >= 2.43 (fastbins were removed) (#1231)

Fixes #1225

## Problem

glibc 2.43 removed the fastbin infrastructure (upstream commit
`bb5a4f5`),
so `struct malloc_state` no longer contains `fastbinsY[NFASTBINS]` nor
`have_fastchunks`. GEF's `GlibcArena.malloc_state_t()` was still parsing
the old layout, so on glibc >= 2.43 every heap command reading the arena
showed garbage (`heap arenas` with wrong `top`/`last_remainder`, broken
`heap bins`, ...).

## Fix

Made the arena layout version-aware:

- `GlibcArena.has_fastbins()`: fastbins exist only on glibc < 2.43
- `GlibcArena.malloc_state_t()`: no longer defines `fastbinsY` /
  `have_fastchunks` on glibc >= 2.43
- `GlibcArena.fastbinsY` returns an empty array, `fastbin()` returns
  `None`, so existing callers behave safely
- `heap bins fast` exits with a clear message instead of parsing
  corrupted data

## Tests

Added `HeapCommandNoFastbins` (tests/commands/heap.py): it forces
`gef.libc._version = (2, 43)` regardless of the host glibc and verifies
the 2.43 arena layout has no fastbins and `heap bins fast` reports them
as unsupported.

The initial CI run failed on `fedora-rawhide` (glibc 2.44) for two
reasons, both fixed in the follow-up commit:

- `HeapCommandFastBins::test_cmd_heap_bins_fast` was missing the
  `skipif(is_glibc_ge(2, 43))` marker that all the other fastbin tests
  use, and failed on a real no-fastbins glibc
- `CanaryCommand::test_cmd_canary` asserted that the stack canary equals
  the `AT_RANDOM`-derived value; on glibc >= 2.44 the canary is no
  longer derived from `AT_RANDOM`, so the test is skipped there (the
  `canary` command itself still reads the right value from the TLS slot)

Verified locally against glibc 2.41 (Debian), 2.43 (fedora:44) and
2.44 (fedora:rawhide) containers, plus `pre-commit run --all-files`.

---------

Co-authored-by: NK <[email protected]>
This commit is contained in:
Naveenkumar
2026-08-20 14:33:25 -07:00
committed by GitHub
co-authored by NK
parent 87f359ba27
commit 49c07dc606
3 changed files with 62 additions and 6 deletions
+22 -6
View File
@@ -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 @@ class GlibcArena:
("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 @@ class GlibcArena:
@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,12 @@ class GlibcHeapFastbinsYCommand(GenericCommand):
@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
+6
View File
@@ -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 @@ class CanaryCommand(RemoteGefUnitTestGeneric):
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
+34
View File
@@ -253,6 +253,7 @@ class HeapCommandFastBins(RemoteGefUnitTestGeneric):
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"
@@ -267,6 +268,39 @@ class HeapCommandFastBins(RemoteGefUnitTestGeneric):
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")