Files
NX2navi/tests/heap_lazy_copy_check.c
chrg6 b146ee716c Copy untouched lazy-heap pages as zeros in mem_copy_readable instead of faulting on them under frz_lock.
With NX2NAVI_HEAP_LAZY=1, a heap page nothing has touched yet is still reserved. mem_copy_readable holds frz_lock
and, for a range with no armed or probed region, copied it with a plain memcpy. A reserved heap page in that range
faulted into fault_veh, then access_fault, then heap_lazy_fault, which takes frz_lock on the same thread. The SRW lock
is not recursive, so the copy never returned, and every later fault, thaw and freeze waited behind it. Callers are
frame_rate.c's floor probe and vk_memory.c mirror_capture (MIRROR_CAPTURE_KB or MIRROR_STAGE).

Now, when the lazy heap is on and a range meets the heap, the range is copied run by run as plat_mem_state reports its
pages. The heap's reserved runs read zero, as they would once committed, and are not committed, so a diagnostic read
or a mirror's first copy does not undo what the setting saves. Readable runs are copied, and anything else leaves the
range out (ok = 0). Ranges outside the heap, and every range while the lazy heap is off, keep the copy that asks the
system nothing.

tests/heap_lazy_copy_check builds memory.c with the Windows platform layer and its fault handler. It copies an
untouched heap page, a range from written pages into untouched ones, a range past the heap's end, and a batch of
three, each on a worker the main thread gives 3 s. Before the change the first copy hung (exit 4). Checks that passed:
heap_lazy_copy_check, pass_cc_check on memory.c, no new -Wall -Wextra warnings, wb_lazy_check, mem_growth_check,
mem_budget_check and gpa_check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:41:40 -05:00

116 lines
5.7 KiB
C

// runtime/memory.c mem_copy_readable with the lazy heap (NX2NAVI_HEAP_LAZY=1): a heap page nothing touched yet is
// still reserved, and the copy holds frz_lock, which the fault on such a page (heap_lazy_fault) waits for on the same
// thread. Built and run by tests/heap_lazy_copy_check.py with the Windows platform layer and the real fault handler;
// the rest of the runtime is stubbed. Prints each failed check and exits 1 on any, 4 when a copy never returns.
// Each copy runs on a worker the main thread gives 3 s: before the fix the first one hung there.
#include "memory.c"
#include <stdarg.h>
static int fails;
#define CHECK(c, ...) do { if (!(c)) { fails++; printf("FAIL line %d: ", __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0)
// ---- the runtime memory.c and the platform layer reach
unsigned rt_trace;
uint64_t rt_guest_start_ns;
const unsigned long long rt_app_pool_bytes = 0xCD500000ull;
const RtLayoutEntry rt_layout[] = {{"main", MOD_MAIN_BASE, MOD_MAIN_SIZE}};
const int rt_layout_count = 1;
const char *cfg_get(const char *name) { (void)name; return NULL; }
const char *cfg_value(const char *name) { (void)name; return "0"; }
int cfg_bool(const char *name) { return !strcmp(name, "NX2NAVI_HEAP_LAZY"); }
void cfg_init(void) {}
int cfg_setenv(const char *name, const char *value) { (void)name, (void)value; return 0; }
int rt_knob(const char *name, const char *def) { (void)name; return strcmp(def, "0") != 0; }
int ldr_count(void) { return 0; }
const RtLoaded *ldr_module(int i) { (void)i; return NULL; }
uint64_t perf_mark_begin(void) { return 0; }
void perf_mark_end(int kind, uint64_t t0, uint64_t n) { (void)kind, (void)t0, (void)n; }
void perf_size(PerfSizes *s, const char *name, uint64_t count, uint64_t limit, uint64_t bytes) {
(void)s, (void)name, (void)count, (void)limit, (void)bytes;
}
uint64_t res_heap_extra_bytes(void) { return 0; }
void rt_log(const char *fmt, ...) { (void)fmt; }
void rt_logf(const char *fmt, ...) { (void)fmt; }
void rt_fatal(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
printf("FATAL: ");
vprintf(fmt, ap);
printf("\n");
va_end(ap);
fflush(stdout);
_Exit(3);
}
// ---- one copy on a worker, as the replay (vk_memory.c mirror_capture) or a present (frame_rate.c) makes it
static struct { uint64_t a, len; uint8_t *dst, ok; int got; volatile int done; } J;
static void copier(void *arg) {
(void)arg;
uint64_t src[1][2] = {{J.a, J.len}};
J.got = mem_copy_readable(&J.dst, (const uint64_t (*)[2])src, 1, &J.ok);
__atomic_store_n(&J.done, 1, __ATOMIC_SEQ_CST);
}
static void copy(uint64_t a, uint64_t len, uint8_t *dst, const char *what) {
J.a = a, J.len = len, J.dst = dst, J.ok = 0xEE, J.got = -1, J.done = 0;
void *t = plat_thread_new(copier, NULL, "copier");
for (int i = 0; i < 300 && !__atomic_load_n(&J.done, __ATOMIC_SEQ_CST); i++) plat_wait_ns(10000000);
if (!J.done) {
printf("FAIL: %s: the copy has not returned after 3 s (frz_lock held by the copying thread)\n", what);
fflush(stdout);
_Exit(4);
}
plat_thread_join(t);
}
static int all(const uint8_t *p, uint64_t n, uint8_t v) {
for (uint64_t i = 0; i < n; i++)
if (p[i] != v) return 0;
return 1;
}
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
mem_init();
uint64_t heap = 0, size = 64ull << 20, y;
CHECK(mem_set_heap_size(size, &heap) == R_OK && heap, "SetHeapSize failed");
CHECK(heap_lazy, "the lazy heap is off");
static uint8_t buf[0x30000];
// ---- a page nothing touched: copied as zeros, and left reserved
uint64_t a = heap + 0x30000;
CHECK(plat_mem_state(a, a + PAGE, &y) == PLAT_MEM_RESERVED, "the untouched page is not reserved");
memset(buf, 0xAB, 64);
copy(a, 64, buf, "an untouched heap page");
CHECK(J.got == 1 && J.ok == 1 && all(buf, 64, 0), "an untouched heap page: returned %d, ok %d, bytes %#x",
J.got, J.ok, buf[0]);
CHECK(plat_mem_state(a, a + PAGE, &y) == PLAT_MEM_RESERVED, "the copy committed the untouched page");
// ---- a range from touched pages into untouched ones: the written bytes, then zeros
uint64_t t = heap + 0x100000; // a 64 KiB run committed by a host access, then written
CHECK(mem_host_readable(t, 0x10000), "the heap page could not be made readable");
memset((void *)(uintptr_t)t, 0x5A, 0x10000);
CHECK(plat_mem_state(t + 0x10000, t + 0x10000 + PAGE, &y) == PLAT_MEM_RESERVED, "the next run is not reserved");
memset(buf, 0xAB, sizeof buf);
copy(t + 0x8000, 0x20000, buf, "touched then untouched heap pages");
CHECK(J.got == 1 && J.ok == 1 && all(buf, 0x8000, 0x5A) && all(buf + 0x8000, 0x18000, 0),
"touched then untouched heap pages: returned %d, ok %d, bytes %#x %#x", J.got, J.ok, buf[0], buf[0x8000]);
// ---- past the heap's end the pages are not the heap's: left out, without a fault
memset(buf, 0xAB, 64);
copy(heap + size - 32, 64, buf, "a range past the heap's end");
CHECK(J.got == 0 && J.ok == 0 && all(buf, 64, 0xAB), "a range past the heap's end: returned %d, ok %d", J.got, J.ok);
// ---- several ranges in one call, as frame_rate.c batches them
uint8_t d0[16], d1[16], d2[16], oks[3];
uint8_t *dst[3] = {d0, d1, d2};
uint64_t src[3][2] = {{t, 16}, {a + 0x100, 16}, {heap + size - 8, 16}};
memset(d0, 0, 16), memset(d1, 0xAB, 16), memset(d2, 0xAB, 16);
int got = mem_copy_readable(dst, (const uint64_t (*)[2])src, 3, oks);
CHECK(got == 2 && oks[0] && oks[1] && !oks[2] && all(d0, 16, 0x5A) && all(d1, 16, 0),
"three ranges: returned %d, ok %d %d %d", got, oks[0], oks[1], oks[2]);
if (fails) return printf("heap_lazy_copy_check: %d checks failed\n", fails), 1;
printf("heap_lazy_copy_check: all checks passed\n");
return 0;
}