From b5c9a9b38e3bc5c78fb1d7159c8dc0a3c9b9b688 Mon Sep 17 00:00:00 2001 From: chrg6 Date: Wed, 11 Nov 2026 17:05:39 -0500 Subject: [PATCH] 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 --- runtime/nvn_api.h | 5 +++++ runtime/nvn_api_core.c | 48 +++++++++++++++++++++++++++++++++++++++--- runtime/nvn_api_obj.c | 2 ++ runtime/nvn_ext.c | 7 ++++-- runtime/nvn_probe.c | 15 +++++++++++++ 5 files changed, 72 insertions(+), 5 deletions(-) diff --git a/runtime/nvn_api.h b/runtime/nvn_api.h index fc0c45fd..578cec25 100644 --- a/runtime/nvn_api.h +++ b/runtime/nvn_api.h @@ -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) diff --git a/runtime/nvn_api_core.c b/runtime/nvn_api_core.c index 83000367..8fe7ae7a 100644 --- a/runtime/nvn_api_core.c +++ b/runtime/nvn_api_core.c @@ -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) diff --git a/runtime/nvn_api_obj.c b/runtime/nvn_api_obj.c index b5aa780c..8ba65eb5 100644 --- a/runtime/nvn_api_obj.c +++ b/runtime/nvn_api_obj.c @@ -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); diff --git a/runtime/nvn_ext.c b/runtime/nvn_ext.c index a0ce8d60..238f0789 100644 --- a/runtime/nvn_ext.c +++ b/runtime/nvn_ext.c @@ -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); diff --git a/runtime/nvn_probe.c b/runtime/nvn_probe.c index 98a32cc5..af6d7173 100644 --- a/runtime/nvn_probe.c +++ b/runtime/nvn_probe.c @@ -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");