Ask an armed region's page state under the freeze lock when a fault finds its range gone, so that a late write is not taken for a frozen page's and a read is not passed on past an arm.

The review found two windows in memory.c lazy_fault, where a fault holding frz_lock and lz_lock finds its address
neither armed nor in flight. It returned 0 there, and access_fault then asked for the page's state without a lock.
A write whose range a flight gave back while the write waited for frz_lock went on to thaw_fault, whose thaw released
every armed range of the 2 MiB region (lazy_drop_region_locked), each fetched under the lock. And after a release
that sets no hold (mem_lazy_skip's give-back of another buffer's range), the completion thread could arm the page
again between that 0 and access_fault's query, so a read saw an inaccessible page and went to the crash handler; that
one predates the ranges in flight.

Now a fault that finds a flight there waits for it as before. Otherwise it releases lz_lock, asks for the page's state
while it still holds frz_lock, under which no arm runs, and runs the access again when the page allows it: a read on
a readable page, a write on a read-write one. A write to a frozen page still goes on to thaw_fault, and an address
that is not ours to the crash handler.

The new late_write scenario of tests/lazy_fetch_check.py has a thread write a range whose release starts and ends
while the write waits for frz_lock: the write runs again and the region's other range stays armed. With the old
return of 0 it fails, the other range released. The read's window has no check: the arm would have to land between
two instructions. Not reproduced in the game before the change; lazy_fetch_check passed in all its scenarios.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 20:04:59 -05:00
parent da60e0d327
commit a8d18ef474
4 changed files with 57 additions and 16 deletions

View File

@@ -268,6 +268,30 @@ int main(int argc, char **argv) {
CHECK(armed(s2, K64), "the region's other range was released");
CHECK(lz_other == 0, "%llu other releases", (unsigned long long)lz_other);
CHECK(b.took < 250 && b.got == 0x77, "the write waited %.1f ms and read back %#x", b.took, b.got);
} else if (!strcmp(what, "late_write")) {
// B writes a range whose flight starts and ends while B waits for frz_lock in lazy_fault (this thread holds the
// lock and runs a release's flight under it, as a thaw does): B finds the page given back and writes again, and
// the region's other range stays armed (B went on to thaw_fault before, whose thaw released it)
uint64_t R = heap + 128 * M, s1 = R, s2 = R + M;
CHECK(arm_buf(s1, K64, 1) == K64 && arm_buf(s2, K64, 1) == K64, "arms");
stale(s1, K64, 1), stale(s2, K64, 1);
fetch_ns = 20000000ull;
plat_mutex_lock(&frz_lock);
Touch b;
void *tb = touch(&b, s1 + 0x20, 1);
plat_wait_ns(50000000); // B waits for frz_lock in lazy_fault, past its look for a range in flight
int k[LZ_BATCH];
LzSeg sg[LZ_BATCH];
uint64_t rg[1][3] = {{s1, s1 + K64, 0}};
plat_mutex_lock(&lz_lock);
int nt = lazy_take_batch_locked((const uint64_t (*)[3])rg, 1, k, sg);
plat_mutex_unlock(&lz_lock);
if (nt > 0) lazy_fly_run(k, sg, nt);
plat_mutex_unlock(&frz_lock);
plat_thread_join(tb);
CHECK(nt == 1 && b.got == 0x77, "%d taken out, the write read back %#x", nt, b.got);
CHECK(armed(s2, K64) && lz_other == 0, "the region's other range was released (%llu other releases)",
(unsigned long long)lz_other);
} else if (!strcmp(what, "concurrent")) {
// two threads touch two ranges at once: their fetches run at the same time
uint64_t s1 = heap + 40 * M, s2 = heap + 48 * M;

View File

@@ -27,6 +27,7 @@ SCENARIOS = {
'no_stall': ('no_stall', {}),
'join': ('join', {}),
'loser': ('loser', {}),
'late_write': ('late_write', {}),
'concurrent': ('concurrent', {}),
'thaw': ('thaw', {}),
'thaw_unlocked': ('thaw_unlocked', {}),