Answer NVN getters asked of no object with 0, and map a buffer with no pool to memory of its own.

The game's scene-loading thread makes a few NVN calls with a null object while an area loads or Kakariko's time of day
changes: a memory pool's flags (and the other pool getters), a texture builder's storage size or alignment, and the
mapping of a buffer that has no pool, into which it then writes 0x3d0 bytes. Each read the object at address 0 plus a
field's offset, an access violation. The pool getters, nvnMemoryPoolInvalidateMappedRange and the two builder queries
now answer 0 for a null object (nvn_no_object), and nvnBufferMap of a buffer with no pool returns a block of scratch
memory (64 KiB, or the buffer's size when it records a larger one) instead of 0. The first such calls are logged with
their caller. These fit the crash reports in Castle Town and in Kakariko before Death Mountain (#605, #602), which have
not been confirmed to be this.

Checked: --probe-nvn gains three checks (pool getters of no pool, builder queries of no builder, a 0x3d0-byte write
through the mapping of a buffer with no pool) and they pass; its four other failures are the same on the baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-11 17:05:39 -05:00
parent e8b4d29f91
commit b5c9a9b38e
5 changed files with 72 additions and 5 deletions

View File

@@ -22,6 +22,11 @@ void nvn_called(uint8_t *seen, const char *name); // the counting prolo
// A wait for the GPU, the display or another thread gives the virtual core up until the end of the enclosing scope.
#define NVN_BLOCKING() __attribute__((cleanup(kern_core_back))) int nvn_away_ = kern_core_away()
static inline int nvn_arg_bool(const Cpu *c, int i) { return (int)(arg_x(c, i) & 0xff); } // NVNboolean: low 8 bits only
// 1 when argument 0, the object of a getter, is NULL: the call is answered with 0 instead of reading the object at
// address 0 plus a field's offset. The game's scene-loading thread makes a few such calls while an area or Kakariko's
// time of day changes (a memory pool's flags, a texture builder's storage size). The first calls are logged with
// their caller (nvn_api_core.c).
int nvn_no_object(Cpu *c, const char *what);
#define NVN_POOL_MAGIC MAGIC('P', 'O', 'O', 'L') // pool_t.magic of an initialized pool
#define NVN_VPOOL_PAGE 0x10000ull // NVN virtual mapping granularity (64 KiB)

View File

@@ -355,10 +355,23 @@ NVN_API(nvnMemoryPoolFlushMappedRange) {
if (rt_trace & RT_TRACE_PERF) flush_note(0, p, size);
if (p && off >= 0 && (uint64_t)off < p->size) nvn_pool_flushed(p, (uint64_t)off, size < p->size - (uint64_t)off ? size : p->size - (uint64_t)off);
}
int nvn_no_object(Cpu *c, const char *what) {
if (arg_x(c, 0)) return 0;
static int told;
if (__atomic_fetch_add(&told, 1, __ATOMIC_RELAXED) < 8)
rt_log("nvn: %s of no object (caller %#llx): 0", what, (unsigned long long)c->x[30]);
ret_x(c, 0);
return 1;
}
// NVNmemoryPoolFlags nvnMemoryPoolGetFlags(const NVNmemoryPool *p)
NVN_API(nvnMemoryPoolGetFlags) { ret_x(c, ((const pool_t *)arg_p(c, 0))->flags); }
NVN_API(nvnMemoryPoolGetFlags) {
if (nvn_no_object(c, "nvnMemoryPoolGetFlags")) return;
ret_x(c, ((const pool_t *)arg_p(c, 0))->flags);
}
// void *nvnMemoryPoolMap(const NVNmemoryPool *p): NULL for CPU_NO_ACCESS
NVN_API(nvnMemoryPoolMap) {
if (nvn_no_object(c, "nvnMemoryPoolMap")) return;
const pool_t *o = arg_p(c, 0);
if (o->magic != NVN_POOL_MAGIC || (o->flags & NVN_MEMORY_POOL_FLAGS_CPU_NO_ACCESS)) { ret_x(c, 0); return; }
ret_x(c, (uintptr_t)o->mem);
@@ -414,7 +427,10 @@ NVN_API(nvnMemoryPoolMapVirtual) {
ret_x(c, 1);
}
// NVNbufferAddress nvnMemoryPoolGetBufferAddress(const NVNmemoryPool *p)
NVN_API(nvnMemoryPoolGetBufferAddress) { ret_x(c, (uintptr_t)((const pool_t *)arg_p(c, 0))->mem); }
NVN_API(nvnMemoryPoolGetBufferAddress) {
if (nvn_no_object(c, "nvnMemoryPoolGetBufferAddress")) return;
ret_x(c, (uintptr_t)((const pool_t *)arg_p(c, 0))->mem);
}
// ---------- Buffers: they reference their pool and own no storage
// void nvnBufferBuilderSetDevice(NVNbufferBuilder *b, NVNdevice *d)
@@ -448,9 +464,35 @@ NVN_API(nvnBufferInitialize) {
// void nvnBufferFinalize(NVNbuffer *b)
NVN_API(nvnBufferFinalize) { memset(arg_p(c, 0), 0, sizeof(buf_t)); }
static uint64_t buf_address(const buf_t *o) { return o->pool && o->pool->mem ? (uintptr_t)o->pool->mem + (uint64_t)o->off : 0; }
// The mapping of a buffer with no pool behind it (no buffer, or one whose nvnBufferInitialize failed): memory of its
// own, zero at first, instead of address 0, since the game's scene-loading thread has been seen filling such a mapping
// (0x3d0 bytes) without looking at it. 64 KiB, or the buffer's size when it records a larger one; a larger block
// replaces the smaller for later calls, and none is freed (the game may still hold one). Guest addresses are host
// addresses.
static uint64_t no_pool_mapping(const buf_t *o, uint64_t lr) {
static uint8_t first[64 << 10] __attribute__((aligned(256)));
static void *block = first;
static uint64_t block_size = sizeof first;
static int told;
static PlatMutex mu = PLAT_MUTEX_INIT;
uint64_t want = o && o->magic == BUF_MAGIC && o->size <= (256ull << 20) ? o->size : 0;
plat_mutex_lock(&mu);
if (want > block_size) {
void *b = calloc(1, want);
if (b) block = b, block_size = want;
}
void *r = block;
if (told++ < 4)
rt_log("nvn: nvnBufferMap of a buffer with no memory pool (caller %#llx): %llu bytes of scratch memory",
(unsigned long long)lr, (unsigned long long)block_size);
plat_mutex_unlock(&mu);
return (uintptr_t)r;
}
// void *nvnBufferMap(const NVNbuffer *b): NULL in a GPU-window pool, which has no CPU view
NVN_API(nvnBufferMap) {
uint64_t a = buf_address(arg_p(c, 0));
const buf_t *o = arg_p(c, 0);
if (!o || !o->pool) { ret_x(c, no_pool_mapping(o, c->x[30])); return; }
uint64_t a = buf_address(o);
ret_x(c, nvn_gpu_only(a) ? 0 : a);
}
// NVNbufferAddress nvnBufferGetAddress(const NVNbuffer *b)

View File

@@ -135,6 +135,7 @@ NVN_API(nvnTextureBuilderGetStorageClass) { ret_x(c, storage_class(&TB(c)->d));
NVN_API(nvnTextureBuilderGetFlags) { ret_x(c, (uint32_t)TB(c)->d.flags); }
// size_t nvnTextureBuilderGetStorageSize(const NVNtextureBuilder *b)
NVN_API(nvnTextureBuilderGetStorageSize) {
if (nvn_no_object(c, "nvnTextureBuilderGetStorageSize")) return;
nvn_texlayout L;
nvn_tex_layout(&TB(c)->d, &L);
if (!L.bpb) rt_log("nvn: TextureBuilderGetStorageSize: unknown format %#x", TB(c)->d.format);
@@ -142,6 +143,7 @@ NVN_API(nvnTextureBuilderGetStorageSize) {
}
// size_t nvnTextureBuilderGetStorageAlignment(const NVNtextureBuilder *b)
NVN_API(nvnTextureBuilderGetStorageAlignment) {
if (nvn_no_object(c, "nvnTextureBuilderGetStorageAlignment")) return;
nvn_texlayout L;
nvn_tex_layout(&TB(c)->d, &L);
ret_x(c, L.align);

View File

@@ -14,7 +14,7 @@
// back end's images) back first. NX2navi invalidates its 260x260 R16 grass maps and 1x1 pick targets this way. A
// CPU-invisible pool has no storage to read.
void nvn_pool_invalidate(const pool_t *p, uint64_t off, uint64_t size) {
if (!p->mem || nvn_gpu_only((uintptr_t)p->mem) || off >= p->size || !size) return;
if (!p || !p->mem || nvn_gpu_only((uintptr_t)p->mem) || off >= p->size || !size) return;
if (size > p->size - off) size = p->size - off;
gfx_backend()->guest_read((uintptr_t)p->mem + off, size);
}
@@ -63,7 +63,10 @@ NVN_API(nvnDeviceGetTimestampInNanoseconds) { ret_x(c, ticks_to_ns(((const NVNco
// ---------- memory pools and buffers
// size_t nvnMemoryPoolGetSize(const NVNmemoryPool *p)
NVN_API(nvnMemoryPoolGetSize) { ret_x(c, ((const pool_t *)arg_p(c, 0))->size); }
NVN_API(nvnMemoryPoolGetSize) {
if (nvn_no_object(c, "nvnMemoryPoolGetSize")) return;
ret_x(c, ((const pool_t *)arg_p(c, 0))->size);
}
// void nvnMemoryPoolInvalidateMappedRange(const NVNmemoryPool *p, ptrdiff_t offset, size_t size)
NVN_API(nvnMemoryPoolInvalidateMappedRange) {
int64_t off = (int64_t)arg_x(c, 1);

View File

@@ -106,6 +106,21 @@ static void probe_device_and_pools(void) {
CHECK(nvn_pool_find(U(pmem) + 0x100, 0x200) == P() && !nvn_pool_find(U(pmem) + PROBE_POOL_SIZE - 8, 16), "pool index");
n_objects += 6;
// getters asked of no object answer 0 instead of reading address 0 (nvn_no_object); a buffer with no pool maps to
// writable memory of its own, not to 0
CHECK(CALL(nvnMemoryPoolGetFlags, 0) == 0 && CALL(nvnMemoryPoolGetSize, 0) == 0 && CALL(nvnMemoryPoolMap, 0) == 0 &&
CALL(nvnMemoryPoolGetBufferAddress, 0) == 0, "pool getters of no pool");
CALL(nvnMemoryPoolInvalidateMappedRange, 0, 0, 0x100);
CHECK(CALL(nvnTextureBuilderGetStorageSize, 0) == 0 && CALL(nvnTextureBuilderGetStorageAlignment, 0) == 0,
"storage size and alignment of no texture builder");
{
static NVNbuffer nopool; // zeroed, as a buffer whose initialization failed
uint8_t *m = (uint8_t *)(uintptr_t)CALL(nvnBufferMap, U(&nopool));
int ok = m != NULL;
if (ok) memset(m, 0xab, 0x3d0), ok = m[0x3cf] == 0xab;
CHECK(ok, "a buffer with no pool maps to writable memory");
}
// refused: no storage, PHYSICAL | VIRTUAL, a size that is not a 64 KiB multiple
static NVNmemoryPool bad;
CHECK(make_pool(&bad, NULL, 0x10000, NVN_MEMORY_POOL_FLAGS_CPU_CACHED) == 0, "a pool without storage is refused");