From 9a5736bf3f4790cd5e73250b3b89617691585824 Mon Sep 17 00:00:00 2001 From: megboyzz Date: Fri, 18 Sep 2026 14:12:32 +0300 Subject: [PATCH] Add GuestHeap size-classed reuse pool + oversized-allocation free-list GuestHeap previously bump-allocated any request >64KiB and never reclaimed it on free() - a real, sustained gameplay session eventually exhausted the arena (traced live to two separate crashes: onCreate never completing, and a NULL-vector dereference in sub_4BA588 caused by a failed ~175KB allocation going unchecked by the real EA code). Widen the existing O(1) size-class free list (already used for <=64KiB requests) to cover the whole arena instead of adding a second reuse mechanism next to it - oversized allocations now pool and reclaim the same way small ones already did. Add matching desktop tests (guest_heap_test.cpp, run via run_heap_tests.sh) covering the exact failing size from the crash log and a sustained alloc/free cycle in an arena too small to survive without reclaim. Verified live on device: the crash is gone across a full 10-minute session with sustained GLES rendering throughout. Co-Authored-By: Claude Sonnet 5 --- mpcore/scripts/run_heap_tests.sh | 19 ++ mpcore/src/main/cpp/emu/guest_heap.cpp | 175 ++++++++++++++++++ mpcore/src/main/cpp/emu/guest_heap.h | 135 ++++++++++++++ .../main/cpp/emu/tests/guest_heap_test.cpp | 168 +++++++++++++++++ 4 files changed, 497 insertions(+) create mode 100755 mpcore/scripts/run_heap_tests.sh create mode 100644 mpcore/src/main/cpp/emu/guest_heap.cpp create mode 100644 mpcore/src/main/cpp/emu/guest_heap.h create mode 100644 mpcore/src/main/cpp/emu/tests/guest_heap_test.cpp diff --git a/mpcore/scripts/run_heap_tests.sh b/mpcore/scripts/run_heap_tests.sh new file mode 100755 index 0000000..a984913 --- /dev/null +++ b/mpcore/scripts/run_heap_tests.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Desktop-only GuestHeap test harness (see guest_heap_test.cpp's own top +# comment) - no Android, no Unicorn, no APK, no device. Compiles +# guest_heap.cpp directly against the host's own C++ compiler and runs the +# resulting binary. Fast feedback loop for changes to the guest heap +# allocator before ever touching a device. +set -euo pipefail +cd "$(dirname "$0")/.." + +SRC_DIR="src/main/cpp/emu" +OUT="${TMPDIR:-/tmp}/guest_heap_test" + +c++ -std=c++17 -O0 -g -Wall \ + "$SRC_DIR/guest_heap.cpp" \ + "$SRC_DIR/tests/guest_heap_test.cpp" \ + -I"$SRC_DIR" \ + -o "$OUT" + +"$OUT" diff --git a/mpcore/src/main/cpp/emu/guest_heap.cpp b/mpcore/src/main/cpp/emu/guest_heap.cpp new file mode 100644 index 0000000..ece0054 --- /dev/null +++ b/mpcore/src/main/cpp/emu/guest_heap.cpp @@ -0,0 +1,175 @@ +#include "guest_heap.h" +#include "../util/util.h" +#include +#include +#include + +// Temporary sanity check (2026-09-06) - confirm the new size-class free +// list actually IS O(1) per call now, independent of whether it explains +// the overall fread-rate mystery (first measurement showed it did NOT move +// the needle - see ARM64_TRANSLATION_LAYER.md). Remove once confirmed. +namespace { +std::atomic g_allocCalls2{0}; +uint64_t NowNs2() { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000000ull + ts.tv_nsec; +} +} // namespace + +void GuestHeap::Init(uint8_t* hostBase, GuestAddr arenaStart, uint32_t arenaSize) { + host_base_ = hostBase; + arena_start_ = arenaStart; + arena_end_ = arenaStart + arenaSize; + free_cursor_ = arenaStart; + for (auto& head : free_list_heads_) head = 0; +} + +int GuestHeap::SizeClassFor(uint32_t payload) { + if (payload > kMaxSizeClassBytes) return -1; + uint32_t classBytes = kAlign; + for (int i = 0; i < kNumSizeClasses; i++) { + if (payload <= classBytes) return i; + classBytes <<= 1; + } + return -1; // unreachable given the check above, but safe +} + +GuestAddr GuestHeap::Alloc(uint32_t size) { + std::lock_guard lock(mutex_); + if (!host_base_ || size == 0) return 0; + uint64_t callNo = g_allocCalls2.fetch_add(1, std::memory_order_relaxed) + 1; + uint64_t t0 = NowNs2(); + uint32_t payload = AlignUp(size, kAlign); + int classIdx = SizeClassFor(payload); + auto logIfSampled = [&](const char* how) { + if (callNo <= 5 || callNo % 20000 == 0) { + Log("GuestHeap::Alloc: call #%llu (%s), took %lluns", + (unsigned long long)callNo, how, (unsigned long long)(NowNs2() - t0)); + } + }; + + if (classIdx >= 0) { + uint32_t classBytes = SizeClassBytes(classIdx); + GuestAddr freeAddr = free_list_heads_[classIdx]; + if (freeAddr) { + // Pop the free-list head - the "next" pointer lives in the + // block's own (now-unused) payload, always at least kAlign=8 + // bytes so a 32-bit guest address always fits. + uint32_t next = 0; + memcpy(&next, host_base_ + freeAddr, 4); + free_list_heads_[classIdx] = next; + HeaderAt(freeAddr)->free = 0; + std::memset(host_base_ + freeAddr, 0, classBytes); + logIfSampled("pool-reuse"); + return freeAddr; + } + // No free block of this class yet - bump-allocate a new one at the + // CLASS size (not the raw requested payload), so any future free() + // of a different-sized request that rounds to this same class can + // still reuse it exactly. + uint32_t needed = sizeof(BlockHeader) + classBytes; + if (free_cursor_ + needed > arena_end_) { + // Loud on purpose (2026-09-18) - since the size-class widening, + // this is now the path any oversized allocation's exhaustion + // actually takes (the old "large path" fallback below is only + // reached past 512MiB, effectively unreachable in practice) - + // losing this class's own copy of the 2026-09-17 "HEAP + // EXHAUSTED" loud-logging fix would silently regress that + // diagnostic for the exact case it was added for. + Log("GuestHeap::Alloc: HEAP EXHAUSTED - class=%d classBytes=%u needed=%u but only " + "%u bytes remain (free_cursor_=0x%x arena_end_=0x%x) - returning 0 (null)", + classIdx, classBytes, needed, + (free_cursor_ <= arena_end_) ? (arena_end_ - free_cursor_) : 0, + free_cursor_, arena_end_); + return 0; + } + BlockHeader* h = reinterpret_cast(host_base_ + free_cursor_); + h->magic = kMagic; + h->size = classBytes; + h->free = 0; + GuestAddr dataAddr = free_cursor_ + sizeof(BlockHeader); + free_cursor_ += needed; + std::memset(host_base_ + dataAddr, 0, classBytes); + logIfSampled("pool-bump-new"); + return dataAddr; + } + + // Fallback for a request bigger than even the largest size class + // (kMaxSizeClassBytes, now 512MiB as of the 2026-09-18 widening - see + // guest_heap.h's own comment) - genuinely unreachable in practice for + // this codebase's real allocation sizes, so still just bump-allocates + // fresh and never reuses on free, same as every size WAS before that + // widening. Kept only as a defensive ceiling, not the routine path it + // used to be. + uint32_t needed = sizeof(BlockHeader) + payload; + // Diagnostic (2026-09-17, ARM64_TRANSLATION_LAYER.md - the 0x1e0 + // wild-jump chase; kept live after the 2026-09-18 size-class widening + // since this branch, though now rare, still never reclaims). Logging + // every allocation that lands here (>512MiB, past even the widened + // size-class ceiling), and LOUDLY logging outright exhaustion (which a + // silent `return 0` would let downstream shims like Shim_aeabi_memcpy's + // own null-check quietly swallow instead of failing loud). + if (payload > 1024 * 1024) { + Log("GuestHeap::Alloc: request past the widened size-class ceiling, payload=%u " + "(%.1fMB) - free_cursor_=0x%x arena_end_=0x%x headroom=%u bytes", + payload, payload / (1024.0 * 1024.0), free_cursor_, arena_end_, + (free_cursor_ <= arena_end_) ? (arena_end_ - free_cursor_) : 0); + } + if (free_cursor_ + needed > arena_end_) { + Log("GuestHeap::Alloc: HEAP EXHAUSTED - payload=%u needed=%u but only %u bytes " + "remain (free_cursor_=0x%x arena_end_=0x%x) - returning 0 (null)", + payload, needed, (free_cursor_ <= arena_end_) ? (arena_end_ - free_cursor_) : 0, + free_cursor_, arena_end_); + return 0; + } + BlockHeader* h = reinterpret_cast(host_base_ + free_cursor_); + h->magic = kMagic; + h->size = payload; + h->free = 0; + GuestAddr dataAddr = free_cursor_ + sizeof(BlockHeader); + free_cursor_ += needed; + std::memset(host_base_ + dataAddr, 0, payload); + logIfSampled("large-path"); + return dataAddr; +} + +bool GuestHeap::IsValidLiveBlock(GuestAddr addr) const { + if (!host_base_ || addr < arena_start_ + sizeof(BlockHeader) || addr >= free_cursor_) return false; + BlockHeader* h = HeaderAt(addr); + if (h->magic != kMagic) return false; // not a real header - wrong/stale address + if (h->free) return false; // already free - reject double-free + // Payload must fit inside the arena and not run past the bump cursor - + // catches a header whose magic happens to match by coincidence but + // whose size field is nonsense (e.g. a wild write landed exactly 8 + // bytes before some unrelated valid-looking magic value). + if (addr + h->size > free_cursor_) return false; + return true; +} + +void GuestHeap::Free(GuestAddr addr) { + if (addr == 0) return; // free(NULL) is legal and common - silent, not a corruption signal + std::lock_guard lock(mutex_); + if (!IsValidLiveBlock(addr)) { + Log("GuestHeap::Free: rejected addr=0x%x - not a valid live block (wrong pointer, double-free, " + "or heap corruption - see guest_heap.h's own class comment)", addr); + return; + } + BlockHeader* h = HeaderAt(addr); + h->free = 1; + // Pooled (size-classed) blocks always have h->size exactly equal to + // their class's byte size (Alloc() only ever bump-allocates a NEW block + // at the class size, never the raw request) - push it onto that class's + // free list for O(1) reuse. As of the 2026-09-18 widening (see + // guest_heap.h's own comment) this now covers every size up to 512MiB, + // not just the original <=64KiB range - only a request bigger than + // that (h->size > kMaxSizeClassBytes, SizeClassFor returns -1) still + // falls outside every class and is simply left free without joining + // any list, same tradeoff Alloc() documents for that now-rare fallback. + int classIdx = SizeClassFor(h->size); + if (classIdx >= 0 && SizeClassBytes(classIdx) == h->size) { + uint32_t next = free_list_heads_[classIdx]; + memcpy(host_base_ + addr, &next, 4); + free_list_heads_[classIdx] = addr; + } +} diff --git a/mpcore/src/main/cpp/emu/guest_heap.h b/mpcore/src/main/cpp/emu/guest_heap.h new file mode 100644 index 0000000..8336c0b --- /dev/null +++ b/mpcore/src/main/cpp/emu/guest_heap.h @@ -0,0 +1,135 @@ +#pragma once + +#include "guest_types.h" +#include + +// Minimal first-fit heap allocator carved out of a fixed-size arena that +// lives INSIDE the same flat guest/host region as the loaded libapp.so +// image (see GuestEngine). Backs the malloc/free/calloc import shims, so +// that any object the guest code allocates at runtime (e.g. InjectSyntheticEvent's +// RaceEvent/CashReward/FakeActor - see lan_event_injection.h) gets a real +// guest address in the SAME identity-offset numbering as every static +// OFFSET macro in this codebase, not an arbitrary host heap pointer outside +// the mapped region. +// +// 2026-09-06: replaced the original first-fit-scan-the-whole-arena design +// with a size-class-segregated free list, after directly measuring the old +// one as the real (confirmed, quantified) root cause of the OBB-index-build +// performance cliff investigated in ARM64_TRANSLATION_LAYER.md - the old +// Alloc() scanned every block ever carved, free or in-use, on every call, +// so cost grew with total historical allocation count (measured: 0 blocks +// scanned at call #1, 9294 at call #40000, ~30us/call once the arena's +// free/used population reached its steady-state size). Requested sizes are +// now rounded up to one of a small set of power-of-two size classes +// (kAlign=8 .. kMaxSizeClassBytes, see kNumSizeClasses's own comment for +// its current ceiling); each class keeps its own singly-linked free list +// (the "next" pointer for a free block lives in the block's own now-unused +// payload - always at least kAlign=8 bytes, enough for one 32-bit guest +// address), making both Alloc() and Free() O(1) instead of O(n). Only a +// request bigger than the largest class - now a genuinely rare, near- +// arena-sized edge case rather than the routine "anything over 64KiB" path +// it used to be (see kNumSizeClasses's own 2026-09-18 comment) - still +// bump-allocates fresh and is simply never reused on free. +// +// This heap backs BOTH the guest program's own malloc/free/calloc/realloc +// AND short-lived JNI marshaling buffers (see import_shims.cpp/jni_shim.cpp) +// - i.e. arbitrary, uncontrolled usage from millions of lines of real ARM32 +// game code sharing one arena. A single wrong free() anywhere in that code +// (or a buffer overflow into a neighboring block) can otherwise silently +// corrupt any other live allocation - confirmed live this session, tracked +// down to the permanently-cached guest JNIEnv structure getting overwritten +// this way. Two structural responses: (1) permanent, safety-critical +// structures (guest JNIEnv/JavaVM) no longer live in this heap at all - see +// GuestEngine::AllocPermanent's own comment; (2) this heap now validates a +// magic value on every Free() so a wrong/stale address is rejected and +// logged instead of silently corrupting whatever bytes precede it. Doesn't +// stop a wild WRITE (nothing but a full guard-page/MMU scheme would), but +// catches the specific failure mode actually observed: a bad free(). +class GuestHeap { +public: + // hostBase: host pointer corresponding to guest address 0 (i.e. the + // engine's G2H(0)). arenaStart/arenaSize: guest-address range this heap + // is allowed to hand out (must not overlap the loaded image or the + // guest stack). + void Init(uint8_t* hostBase, GuestAddr arenaStart, uint32_t arenaSize); + + // Returns 0 (a guest NULL) on failure, matching malloc's own contract. + // Thread-safe (mutex-guarded) - now that real guest threads exist (see + // emu/pthread_shim.h), any of them can call malloc/free concurrently. + GuestAddr Alloc(uint32_t size); + // No-ops (logged) if `addr` doesn't point at a real, currently-allocated + // block's payload - see class comment. + void Free(GuestAddr addr); + +private: + struct BlockHeader { + uint32_t magic; // kMagic if this is a real header GuestHeap itself wrote - see class comment + uint32_t size; // payload size, not including this header + uint32_t free; // 1 = free, 0 = in use + }; + static constexpr uint32_t kMagic = 0x47484B21; // "GuestHeap blocK!" - arbitrary but distinctive + static constexpr uint32_t kAlign = 8; + static uint32_t AlignUp(uint32_t v, uint32_t a) { return (v + a - 1) & ~(a - 1); } + + // Size classes: kAlign(8), 16, 32, ... up to kAlign << (kNumSizeClasses-1). + // 27 classes tops out at 8 << 26 = 512MiB. + // + // 2026-09-18 (ARM64_TRANSLATION_LAYER.md - the sub_4BA588/"loadNodeUncached" + // chase, ROOT CAUSE): widened from 14 classes (64KiB ceiling) to 27 + // (512MiB ceiling) - the >64KiB "oversized" path used to bump-allocate + // fresh on every call and NEVER reuse freed memory (see the old + // GuestHeap::Alloc's own now-removed comment), which was root-caused + // live on the Pixel 6a as the single cause of BOTH the "onCreate never + // completes" heap-exhaustion crash AND a separate, later-discovered + // crash in sub_4BA588 ("loadNodeUncached"): a real ~175KB M3G resource + // buffer allocation failed because the 256MB arena was already full of + // permanently-stranded freed-but-unreclaimed oversized blocks from + // earlier in the same session, the real EA code doesn't check malloc's + // result for null (reasonably, since real hardware's own heap + // essentially never fails a request this size), and the null flows + // downstream into a reader that legitimately-but-incorrectly reports + // "0 bytes available" forever. Real hardware never hits this because + // its heap never actually runs out. Simplest complete fix: there is no + // structural difference between a "small" and a "large" allocation from + // this allocator's own point of view - the SAME O(1) size-classed + // free-list scheme that already correctly reuses freed <=64KiB blocks + // (see the 2026-09-06 comment two paragraphs up) works identically at + // any size, so widening it removes the separate never-reclaimed path + // entirely rather than adding a second, differently-shaped one next to + // it. Trades up to ~2x internal fragmentation per oversized object + // (worst case: a request just over a class boundary rounds up to + // double) for a hard bound on total arena growth - a real win given the + // failure mode being fixed is fully UNBOUNDED growth, not excess + // fragmentation. Does not coalesce adjacent free blocks (consistent + // with this allocator's existing choice not to do so even for small + // blocks - see the class comment) - not needed for this fix: reuse + // alone converts "grows forever" into "reaches a steady state," which + // is the actual guarantee being restored here. + static constexpr int kNumSizeClasses = 27; + static constexpr uint32_t kMaxSizeClassBytes = kAlign << (kNumSizeClasses - 1); + // Returns the class index for `payload`, or -1 if it's larger than the + // biggest class (caller falls back to a fresh, never-reused allocation). + static int SizeClassFor(uint32_t payload); + static uint32_t SizeClassBytes(int classIdx) { return kAlign << classIdx; } + + BlockHeader* HeaderAt(GuestAddr addr) const { + return reinterpret_cast(host_base_ + addr - sizeof(BlockHeader)); + } + GuestAddr AddrOfHeader(BlockHeader* h) const { + return static_cast(reinterpret_cast(h) - host_base_); + } + // True if `addr` is in-range AND the bytes immediately before it look + // like a real header this allocator wrote (magic matches) AND that + // block is currently marked in-use (double-free protection too). + bool IsValidLiveBlock(GuestAddr addr) const; + + uint8_t* host_base_ = nullptr; + GuestAddr arena_start_ = 0; + GuestAddr arena_end_ = 0; // one-past-the-end of the arena + GuestAddr free_cursor_ = 0; // next never-yet-used byte (bump pointer for the "no free block fits" case) + // Head of each size class's free list (a block's data address, or 0 = + // empty) - see class comment. Zero-initialized; Init() also resets it + // explicitly in case an instance is ever re-Init()'d. + GuestAddr free_list_heads_[kNumSizeClasses] = {}; + std::mutex mutex_; +}; diff --git a/mpcore/src/main/cpp/emu/tests/guest_heap_test.cpp b/mpcore/src/main/cpp/emu/tests/guest_heap_test.cpp new file mode 100644 index 0000000..5f01966 --- /dev/null +++ b/mpcore/src/main/cpp/emu/tests/guest_heap_test.cpp @@ -0,0 +1,168 @@ +// Desktop-only test harness for GuestHeap (see /ARM64_TRANSLATION_LAYER.md +// and guest_heap.h's own class comment) - no Unicorn, no Android, no APK. +// Compiles and links ONLY guest_heap.cpp directly against a host-side stub +// of util.h's Log() below (guest_heap.cpp is otherwise fully portable - see +// guest_types.h). Run via mpcore/scripts/run_heap_tests.sh. +// +// Exercises exactly the failure mode a real on-device crash was traced to +// this session: GuestHeap::Free() with a wrong/stale address silently +// corrupting an unrelated live allocation (which, in the real crash, was +// the permanently-cached guest JNIEnv). Confirms the canary added in +// response catches it instead of letting it through. + +#include "../guest_heap.h" +#include +#include +#include +#include + +// util.h declares this; guest_heap.cpp calls it on rejected frees/ +// exhaustion. Desktop stand-in - prints to stdout instead of +// __android_log_write (an NDK-only API, not available on the host). +void Log(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + vprintf(fmt, args); + va_end(args); + printf("\n"); +} + +namespace { + +int g_checks = 0; +int g_failures = 0; + +void Check(bool cond, const char* what) { + g_checks++; + if (!cond) { + g_failures++; + printf(" FAIL: %s\n", what); + } else { + printf(" ok: %s\n", what); + } +} + +} // namespace + +int main() { + constexpr uint32_t kArenaSize = 64 * 1024; + constexpr GuestAddr kArenaStart = 0x1000; // nonzero, matches real usage (heap never starts at guest 0) + // Backing buffer is addressed the same way GuestEngine::G2H does + // (hostBase + guestAddr) - kArenaStart headroom below the arena itself + // so that convention holds without a separate translation helper here. + std::vector backing(kArenaStart + kArenaSize, 0); + + printf("test: basic alloc/free round trip\n"); + { + GuestHeap heap; + heap.Init(backing.data(), kArenaStart, kArenaSize); + GuestAddr a = heap.Alloc(64); + Check(a != 0, "Alloc(64) succeeds"); + heap.Free(a); + GuestAddr b = heap.Alloc(64); + Check(b == a, "Alloc after Free reuses the freed block (first-fit)"); + } + + printf("test: free(0) is a silent no-op, matching free(NULL)\n"); + { + GuestHeap heap; + heap.Init(backing.data(), kArenaStart, kArenaSize); + heap.Free(0); // must not crash, must not touch anything + GuestAddr a = heap.Alloc(16); + Check(a != 0, "heap still works normally after Free(0)"); + } + + printf("test: double-free is rejected, not silently accepted\n"); + { + GuestHeap heap; + heap.Init(backing.data(), kArenaStart, kArenaSize); + GuestAddr a = heap.Alloc(32); + heap.Free(a); + heap.Free(a); // second free of the same address - must be rejected, not corrupt bookkeeping + GuestAddr b = heap.Alloc(32); + Check(b == a, "block is still consistently reusable after an attempted double-free"); + } + + printf("test: a wrong/stale address passed to Free() does not corrupt a live neighbor\n"); + { + GuestHeap heap; + heap.Init(backing.data(), kArenaStart, kArenaSize); + GuestAddr victim = heap.Alloc(16); + Check(victim != 0, "victim allocation succeeds"); + memset(backing.data() + victim, 0xAB, 16); // sentinel payload + + // NOT a real block's payload start - exactly the shape of bug this + // session traced a real crash to (a miscalculated/stale guest + // address handed to Free()). + GuestAddr wrongAddr = victim + 5; + heap.Free(wrongAddr); // must be rejected + + bool intact = true; + for (int i = 0; i < 16; i++) { + if (backing.data()[victim + i] != 0xAB) intact = false; + } + Check(intact, "victim block's payload is untouched after a rejected wrong-address free"); + + // The victim should still be considered in-use - first-fit must + // skip it (not reuse it) for a fresh allocation. + GuestAddr other = heap.Alloc(16); + Check(other != victim, "victim block was NOT freed by the rejected wrong-address free"); + } + + printf("test: oversized (>64KiB) alloc/free round trip reuses the freed block\n"); + { + // Separate, larger arena - the default 64KiB test arena above can't + // fit even one >64KiB allocation. + constexpr uint32_t kBigArenaSize = 4 * 1024 * 1024; // 4MiB + std::vector bigBacking(kArenaStart + kBigArenaSize, 0); + GuestHeap heap; + heap.Init(bigBacking.data(), kArenaStart, kBigArenaSize); + GuestAddr a = heap.Alloc(175760); // real size from the sub_4BA588 crash log + Check(a != 0, "Alloc(175760) succeeds"); + heap.Free(a); + GuestAddr b = heap.Alloc(175760); + Check(b == a, "oversized Alloc after Free reuses the freed block, same as small sizes"); + } + + printf("test: repeated oversized alloc/free does NOT grow the arena unboundedly " + "(regression test for the sub_4BA588/loadNodeUncached heap-exhaustion crash, " + "ARM64_TRANSLATION_LAYER.md 2026-09-18)\n"); + { + // Small enough that the OLD never-reclaim behavior would exhaust + // this arena in well under 100 iterations (100 * (175760+12) ~= + // 17.6MB > this 1MB arena), but large enough that a handful of + // real, simultaneously-live oversized objects still fit alongside + // the reused ones - a tight, realistic regression bound instead of + // an arbitrarily generous one. + constexpr uint32_t kBigArenaSize = 1 * 1024 * 1024; // 1MiB + std::vector bigBacking(kArenaStart + kBigArenaSize, 0); + GuestHeap heap; + heap.Init(bigBacking.data(), kArenaStart, kBigArenaSize); + bool allSucceeded = true; + for (int i = 0; i < 500; i++) { + GuestAddr a = heap.Alloc(175760); + if (a == 0) { allSucceeded = false; break; } + heap.Free(a); + } + Check(allSucceeded, "500 repeated alloc/free cycles of a real oversized size all " + "succeed in a 1MiB arena (would have exhausted after ~5 cycles " + "under the old never-reclaimed behavior)"); + } + + printf("test: exhaustion returns 0 (guest NULL), not garbage or a crash\n"); + { + GuestHeap heap; + heap.Init(backing.data(), kArenaStart, kArenaSize); + int count = 0; + while (heap.Alloc(256) != 0) { + count++; + if (count > 100000) break; // safety valve + } + Check(count > 0, "at least one allocation succeeded before exhaustion"); + GuestAddr overflow = heap.Alloc(256); + Check(overflow == 0, "allocation after exhaustion returns 0"); + } + + printf("\n%d checks, %d failures\n", g_checks, g_failures); + return g_failures == 0 ? 0 : 1; +}