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 <noreply@anthropic.com>
This commit is contained in:
Executable
+19
@@ -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"
|
||||
@@ -0,0 +1,175 @@
|
||||
#include "guest_heap.h"
|
||||
#include "../util/util.h"
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <time.h>
|
||||
|
||||
// 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<uint64_t> 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<std::mutex> 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<BlockHeader*>(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<BlockHeader*>(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<std::mutex> 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
#pragma once
|
||||
|
||||
#include "guest_types.h"
|
||||
#include <mutex>
|
||||
|
||||
// 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<BlockHeader*>(host_base_ + addr - sizeof(BlockHeader));
|
||||
}
|
||||
GuestAddr AddrOfHeader(BlockHeader* h) const {
|
||||
return static_cast<GuestAddr>(reinterpret_cast<uint8_t*>(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_;
|
||||
};
|
||||
@@ -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 <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
// 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<uint8_t> 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<uint8_t> 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<uint8_t> 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;
|
||||
}
|
||||
Reference in New Issue
Block a user