zone, hunk, cache: harden allocator size handling

Audit pass over the three allocators (zone for small Z_Malloc
allocations, hunk for the long-lived BSP / texture data, cache
for sound and skin LRU caches).  Eight findings, mostly defence
in depth against caller-passed bad sizes; one real
use-after-free in Z_Realloc.

Z_Realloc

  Pre-existing use-after-free.  Original order:
      Z_Free(ptr)
      ret = Z_TagMalloc(size, 1)
      memmove(ret, ptr, ...)
  Between Z_Free and Z_TagMalloc the freed block is coalesced
  with its neighbours, which overwrites the freed block's
  prev/next/size fields with the merged-block layout.
  Z_TagMalloc can then split that same region and stamp a new
  memblock_t header inside it.  Either path corrupts the source
  bytes before memmove reads them.  Swap the order to
  alloc -> memmove -> free.

  Also: handle size <= 0 by bumping to 1.  Quake's allocator
  was never asked for size 0 historically, but POSIX
  realloc(p, 0) is implementation-defined and callers do not
  null-check Z_Realloc's return value.

Z_TagMalloc

  size += sizeof(memblock_t) + 4; size = (size + 7) & ~7
  overflows int near INT_MAX.  The wrapped-negative size
  makes the free-list scan match block 0 unconditionally and
  hands back a 0-usable-byte region.  Reject size <= 0 and
  size > INT_MAX - 31 at entry.

Hunk_Alloc, Hunk_HighAlloc, Hunk_TempAlloc, Hunk_TempAllocExtend

  Same overflow shape: size = sizeof(hunk_t) + ((size + 15) &
  ~15).  Wrapped-negative size passes the
  (hunk_size - low - high < size) check and hunk_low_used
  walks backward.  Subsequent allocations overlap the
  previous one and memset wraps with negative count.  All
  four bounded against INT_MAX - sizeof(hunk_t) - 15.

Cache_AllocPadded

  size + pad + sizeof(cache_system_t) + 15 overflow.  pad is
  signed and unbounded; reject pad < 0 and the sum > INT_MAX.

Hunk_Check

  for (h = base; h != base + low_used;) walks block headers.
  A stomped h->size == 0 produces an infinite loop.  The
  guard h->size < sizeof(hunk_t) was intended to catch this
  but compared signed int against size_t (unsigned), so a
  negative size promoted to huge unsigned passed.  Cast to
  (int) so the comparison is signed-vs-signed.

No behaviour change on well-formed allocations.  All bounds
fail with Sys_Error giving the offending size, in the style
of the existing audit checks.
This commit is contained in:
U-DESKTOP-SPFP6AQ\twistedtechre committed 2026-05-03 08:25:40 +02:00
1 parent 35783da43c
commit d0f7e07c67
1 file changed
+70 -2
+70 -2
View File
@@ -213,6 +213,18 @@ static void *Z_TagMalloc(int size, int tag)
if (!tag)
Sys_Error("%s: tried to use a 0 tag", __func__);
/* Reject callers that pass a non-positive size or a size so
* large that the +sizeof(memblock_t)+4 alignment math below
* would overflow into a negative int. The free-list scan
* compares block->size (positive) against this size; with
* a wrapped-negative size every free block looks "big enough"
* and we hand back a pointer to whatever the rover lands on,
* with nominal size 0. Subsequent caller writes corrupt
* adjacent allocations. */
if (size <= 0
|| size > INT_MAX - (int)sizeof(memblock_t) - 4 - 7)
Sys_Error("%s: bad size %i", __func__, size);
/*
* Scan through the block list looking for the first free block of
* sufficient size
@@ -300,6 +312,14 @@ void *Z_Realloc(void *ptr, int size)
if (!ptr)
return Z_Malloc(size);
/* POSIX realloc(p, 0) is implementation-defined since C17.
* Quake's allocator never asked for size 0 historically;
* treat it as a request to shrink to a minimum block.
* Don't free + return NULL because callers do not check
* for NULL after Z_Realloc. */
if (size <= 0)
size = 1;
block = (memblock_t *)((byte *)ptr - sizeof(memblock_t));
if (block->id != ZONEID)
Sys_Error("%s: realloced a pointer without ZONEID", __func__);
@@ -310,12 +330,26 @@ void *Z_Realloc(void *ptr, int size)
orig_size -= sizeof(memblock_t);
orig_size -= 4;
Z_Free(ptr);
/* The original implementation freed ptr first, then alloc'd a
* new block, then memmove'd from the (already-freed) ptr.
* That's a use-after-free: between Z_Free and Z_TagMalloc the
* block headers are coalesced (Z_Free overwrites the freed
* block's prev/next/size links with the merged-block layout),
* and Z_TagMalloc itself can split the same region we just
* freed and write a new memblock_t header partway through it.
* Either path corrupts the source bytes before memmove reads
* them.
*
* Allocate first, copy, then free. In the size-shrink case
* this slightly increases peak footprint; in exchange we no
* longer read garbage when the shrink happens to be in a
* region the new alloc reuses. */
ret = Z_TagMalloc(size, 1);
if (!ret)
Sys_Error("%s: failed on allocation of %i bytes", __func__, size);
if (ret != ptr)
memmove(ret, ptr, qmin(orig_size, size));
Z_Free(ptr);
return ret;
}
@@ -364,7 +398,12 @@ void Hunk_Check(void)
{
if (h->sentinal != HUNK_SENTINAL)
Sys_Error("%s: trashed sentinal", __func__);
if (h->size < sizeof(hunk_t) ||
/* h->size includes sizeof(hunk_t) and is rounded up to a
* 16-byte multiple (see Hunk_Alloc). A zero or negative
* size, or a size large enough that h + size walks past
* hunk_low_used, indicates a stomped header -- diagnose
* loudly rather than infinite-looping. */
if (h->size < (int)sizeof(hunk_t) ||
h->size + (byte *)h - hunk_base > hunk_size)
Sys_Error("%s: bad size", __func__);
h = (hunk_t *)((byte *)h + h->size);
@@ -401,8 +440,17 @@ void *Hunk_Alloc(int size)
{
hunk_t *h;
/* Reject negative sizes, and sizes large enough that
* sizeof(hunk_t) + ((size + 15) & ~15) would overflow int.
* Without the upper-bound check, a wrapped-negative size
* passes the (hunk_size - ... < size) guard (because
* negative < any positive), then hunk_low_used += size
* walks backward and the next allocation overlaps the
* previous one. */
if (size < 0)
Sys_Error("%s: bad size: %i", __func__, size);
if (size > INT_MAX - (int)sizeof(hunk_t) - 15)
Sys_Error("%s: size %i too large", __func__, size);
size = sizeof(hunk_t) + ((size + 15) & ~15);
@@ -473,6 +521,8 @@ void *Hunk_HighAlloc(int size)
if (size < 0)
Sys_Error("%s: bad size: %i", __func__, size);
if (size > INT_MAX - (int)sizeof(hunk_t) - 15)
Sys_Error("%s: size %i too large", __func__, size);
if (hunk_tempactive)
{
@@ -512,6 +562,11 @@ void *Hunk_TempAlloc(int size)
{
void *buf;
if (size < 0)
Sys_Error("%s: bad size: %i", __func__, size);
if (size > INT_MAX - 15)
Sys_Error("%s: size %i too large", __func__, size);
size = (size + 15) & ~15;
if (hunk_tempactive)
@@ -543,6 +598,10 @@ void *Hunk_TempAllocExtend(int size)
if (!hunk_tempactive)
Sys_Error("%s: temp hunk not active", __func__);
if (size < 0)
Sys_Error("%s: bad size: %i", __func__, size);
if (size > INT_MAX - 15)
Sys_Error("%s: size %i too large", __func__, size);
old = (hunk_t *)(hunk_base + hunk_size - hunk_high_used);
@@ -880,6 +939,15 @@ void *Cache_AllocPadded(cache_user_t *c, int pad, int size)
if (size <= 0)
Sys_Error("%s: size %i", __func__, size);
/* Reject sizes that would overflow the alignment math below.
* pad is typically a small alignment fudge but isn't bounded
* from above; reject negative pad explicitly and reject any
* size + pad that would walk past INT_MAX after adding
* sizeof(cache_system_t) and the 15-byte rounding slack. */
if (pad < 0)
Sys_Error("%s: bad pad %i", __func__, pad);
if (size > INT_MAX - pad - (int)sizeof(cache_system_t) - 15)
Sys_Error("%s: size %i (pad %i) too large", __func__, size, pad);
size = (size + pad + sizeof(cache_system_t) + 15) & ~15;