diff --git a/Makefile b/Makefile index 3eb5788e..c4711110 100644 --- a/Makefile +++ b/Makefile @@ -641,6 +641,12 @@ $(BUILD_DIR)/test-futex-wake-nowaiter: tests/test-futex-wake-nowaiter.c \ @echo " CROSS $< (with -lpthread)" $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread +# test-noreserve-materialize races a writer against a neighbor's first touch. +$(BUILD_DIR)/test-noreserve-materialize: tests/test-noreserve-materialize.c \ + | $(BUILD_DIR) + @echo " CROSS $< (with -lpthread)" + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread + # test-shim-futex-fast spawns a waker thread for the matching-word case, which # is the one branch of the futex fast path that must decline and block. $(BUILD_DIR)/test-shim-futex-fast: tests/test-shim-futex-fast.c | $(BUILD_DIR) diff --git a/src/core/guest.c b/src/core/guest.c index 6d84f0f3..a15c7f73 100644 --- a/src/core/guest.c +++ b/src/core/guest.c @@ -3063,6 +3063,57 @@ uint64_t guest_build_page_tables(guest_t *g, const mem_region_t *regions, int n) return ttbr0; } +/* Return the L2 slot covering ipa, allocating the L1 and L2 tables on the way + * if they do not exist yet. + * + * Returns NULL when ipa is out of range or the page table pool is exhausted. + */ +static uint64_t *l2_slot_alloc(guest_t *g, uint64_t ipa) +{ + uint64_t base = g->ipa_base; + uint64_t *l0 = pt_at(g, g->ttbr0 - base); + + /* L0 index: which 512GiB slot (>512GiB addresses need L0[1]+) */ + unsigned l0_idx = (unsigned) (ipa / (512ULL * BLOCK_1GIB)); + if (l0_idx >= 512) { + log_error("guest: IPA 0x%llx out of L0 range in extend", + (unsigned long long) ipa); + return NULL; + } + + /* Allocate L1 table on first access to each L0 slot */ + if (!(l0[l0_idx] & PT_VALID)) { + uint64_t l1_gpa = pt_alloc_page(g); + if (!l1_gpa) + return NULL; + pte_store_release(&l0[l0_idx], (base + l1_gpa) | PT_VALID | PT_TABLE); + } + + uint64_t l1_ipa = l0[l0_idx] & 0xFFFFFFFFF000ULL; + uint64_t *l1 = pt_at(g, l1_ipa - base); + + unsigned l1_idx = (unsigned) ((ipa % (512ULL * BLOCK_1GIB)) / BLOCK_1GIB); + if (l1_idx >= 512) { + log_error("guest: IPA 0x%llx out of L1 range in extend", + (unsigned long long) ipa); + return NULL; + } + + /* Ensure L1 entry points to an L2 table */ + if (!(l1[l1_idx] & PT_VALID)) { + uint64_t l2_gpa = pt_alloc_page(g); + if (!l2_gpa) + return NULL; + pte_store_release(&l1[l1_idx], (base + l2_gpa) | PT_VALID | PT_TABLE); + } + + uint64_t l2_ipa = l1[l1_idx] & 0xFFFFFFFFF000ULL; + uint64_t *l2 = pt_at(g, l2_ipa - base); + + unsigned l2_idx = (unsigned) ((ipa % BLOCK_1GIB) / BLOCK_2MIB); + return &l2[l2_idx]; +} + /* Extend page tables to cover [start, end) with 2MiB block descriptors. Walks * the existing L0->L1 structure (from g->ttbr0) and allocates new L2 tables as * needed. This is safe to call while the vCPU is paused (during HVC #5 @@ -3103,10 +3154,6 @@ int guest_extend_page_tables(guest_t *g, uint64_t base = g->ipa_base; - /* Navigate to L0 table */ - uint64_t l0_gpa_off = g->ttbr0 - base; - uint64_t *l0 = pt_at(g, l0_gpa_off); - /* Walk 2MiB blocks in [start, end). Track the smallest sub-range whose L2 * entry actually transitioned from unmapped to mapped; blocks that were * already valid get no new descriptor and need no TLBI (false-positive @@ -3121,49 +3168,9 @@ int guest_extend_page_tables(guest_t *g, for (uint64_t addr = addr_start; addr < addr_end; addr += BLOCK_2MIB) { uint64_t ipa = base + addr; - - /* L0 index: which 512GiB slot (>512GiB addresses need L0[1]+) */ - unsigned l0_idx = (unsigned) (ipa / (512ULL * BLOCK_1GIB)); - if (l0_idx >= 512) { - log_error("guest: IPA 0x%llx out of L0 range in extend", - (unsigned long long) ipa); - return -1; - } - - /* Allocate L1 table on first access to each L0 slot */ - if (!(l0[l0_idx] & PT_VALID)) { - uint64_t l1_gpa = pt_alloc_page(g); - if (!l1_gpa) - return -1; - pte_store_release(&l0[l0_idx], - (base + l1_gpa) | PT_VALID | PT_TABLE); - } - - uint64_t l1_ipa = l0[l0_idx] & 0xFFFFFFFFF000ULL; - uint64_t *l1 = pt_at(g, l1_ipa - base); - - unsigned l1_idx = - (unsigned) ((ipa % (512ULL * BLOCK_1GIB)) / BLOCK_1GIB); - if (l1_idx >= 512) { - log_error("guest: IPA 0x%llx out of L1 range in extend", - (unsigned long long) ipa); + uint64_t *l2_entry = l2_slot_alloc(g, ipa); + if (!l2_entry) return -1; - } - - /* Ensure L1 entry points to an L2 table */ - if (!(l1[l1_idx] & PT_VALID)) { - uint64_t l2_gpa = pt_alloc_page(g); - if (!l2_gpa) - return -1; - pte_store_release(&l1[l1_idx], - (base + l2_gpa) | PT_VALID | PT_TABLE); - } - - /* Navigate to L2 table */ - uint64_t l2_ipa = l1[l1_idx] & 0xFFFFFFFFF000ULL; - uint64_t *l2 = pt_at(g, l2_ipa - base); - - unsigned l2_idx = (unsigned) ((ipa % BLOCK_1GIB) / BLOCK_2MIB); /* Only map if not already mapped. A negative TLB entry from a prior * translation fault is possible only for VAs that were unmapped at the @@ -3177,9 +3184,9 @@ int guest_extend_page_tables(guest_t *g, * an explicit PT_VALID test so the intent survives a future * descriptor-bit renumbering. */ - if (l2[l2_idx] & PT_VALID) + if (*l2_entry & PT_VALID) continue; - pte_store_release(&l2[l2_idx], make_block_desc(ipa, perms)); + pte_store_release(l2_entry, make_block_desc(ipa, perms)); if (!bcast) { if (addr < changed_lo) changed_lo = addr; @@ -3708,6 +3715,38 @@ int guest_install_va_pages(guest_t *g, /* Lazy page materialization for MAP_NORESERVE. */ +/* Map [start, end) inside the empty 2MiB slot at block_start with 4KiB pages + * and leave the rest of the block unmapped. The L3 table is complete before the + * release store publishes it, so no vCPU ever sees a page outside [start, end). + */ +static int materialize_partial_block(guest_t *g, + uint64_t block_start, + uint64_t start, + uint64_t end, + int perms) +{ + uint64_t base = g->ipa_base; + uint64_t *l2_entry = l2_slot_alloc(g, base + block_start); + if (!l2_entry) + return -1; + + uint64_t l3_gpa = pt_alloc_page(g); + if (!l3_gpa) + return -1; + uint64_t *l3 = pt_at(g, l3_gpa); + for (uint64_t pa = start; pa < end; pa += PAGE_SIZE) { + unsigned l3_idx = (unsigned) (((base + pa) % BLOCK_2MIB) / PAGE_SIZE); + l3[l3_idx] = make_page_desc(base + pa, perms); + } + + if (perms & MEM_PERM_X) + tlbi_request_mark_icache(); + pte_store_release(l2_entry, (base + l3_gpa) | PT_VALID | PT_TABLE); + tlbi_request_range(base + start, base + end); + guest_pt_gen_bump(g); + return 0; +} + int guest_materialize_lazy(guest_t *g, uint64_t fault_offset) { /* Find the noreserve region containing this offset */ @@ -3723,6 +3762,23 @@ int guest_materialize_lazy(guest_t *g, uint64_t fault_offset) if (!region) return -1; /* Not a noreserve region */ + /* PROT_NONE reserves the range; an access to it is a real fault, not a + * request for memory. + */ + if (!(region->prot & + (LINUX_PROT_READ | LINUX_PROT_WRITE | LINUX_PROT_EXEC))) + return -1; + + /* Another vCPU materialized this page while this one waited for mmap_lock, + * and the guest may have written to it since. All that is left is the stale + * translation the fault came from; decline it so the caller's bounded + * stale-TLB retry issues the TLBI and still delivers SIGSEGV if the walk + * and the hardware keep disagreeing. + */ + uint64_t fault_page = fault_offset & ~(PAGE_SIZE - 1); + if (gva_resolve_perm(g, g->ipa_base + fault_page, NULL, 0, UINT64_MAX)) + return -1; + /* Materialize one 2MiB block containing the fault address. This is the * smallest granule that guest_extend_page_tables works with. For the common * case (sparse heap touch), materializing one block at a time is the right @@ -3752,47 +3808,52 @@ int guest_materialize_lazy(guest_t *g, uint64_t fault_offset) perms |= MEM_PERM_W; if (region->prot & LINUX_PROT_EXEC) perms |= MEM_PERM_X; - if (perms == 0) - perms = MEM_PERM_R; /* At minimum readable */ + + /* Zero before any descriptor is published: once one is, a sibling vCPU can + * write through it without faulting, and zeroing afterwards would wipe that + * write. Nothing writes an unmapped page in between, since every path that + * maps one holds mmap_lock. A page that is already mapped belongs to an + * adjacent mapping merged into this region and holds the guest's data, so + * it is left alone. + */ + for (uint64_t pg = materialize_start; pg < materialize_end;) { + if (gva_resolve_perm(g, g->ipa_base + pg, NULL, 0, UINT64_MAX)) { + pg += PAGE_SIZE; + continue; + } + uint64_t run = pg; + do { + pg += PAGE_SIZE; + } while (pg < materialize_end && + !gva_resolve_perm(g, g->ipa_base + pg, NULL, 0, UINT64_MAX)); + memset((uint8_t *) g->host_base + run, 0, pg - run); + } + + /* A block with no descriptor yet that the region only partly covers gets an + * L3 table holding just the region's pages, filled before the L2 slot + * points at it. Publishing a whole 2MiB block first and trimming it after + * would let a sibling vCPU reach a guard or hole in the same block during + * the trim. + */ + if (partial_block && !had_mapping) + return materialize_partial_block(g, block_start, materialize_start, + materialize_end, perms); /* Create page table entries. guest_extend_page_tables creates L2 block * descriptors but skips existing table descriptors (L2->L3 splits). * guest_update_perms handles the L3 case: if guest_invalidate_ptes * previously split the block and invalidated the L3 entries, update_perms - * recreates them with correct perms. + * recreates them with correct perms. An existing split block already + * encodes the neighboring mappings, so it is left as is. */ if (guest_extend_page_tables(g, block_start, block_end, perms) < 0) return -1; - if (partial_block) { - if (guest_split_block(g, block_start) < 0) - return -1; - - /* If this block had no page-table entry before the lazy fault, - * guest_extend_page_tables() necessarily created a full 2MiB block. - * Split it and remove pages outside this noreserve region so holes and - * guards in the same 2MiB block remain faults. Existing split blocks - * already encode neighboring mappings, so leave them intact. - */ - if (!had_mapping) { - if (block_start < materialize_start && - guest_invalidate_ptes(g, block_start, materialize_start) < 0) - return -1; - if (materialize_end < block_end && - guest_invalidate_ptes(g, materialize_end, block_end) < 0) - return -1; - } - } + if (partial_block && guest_split_block(g, block_start) < 0) + return -1; guest_update_perms(g, materialize_start, materialize_end, perms); - /* Zero the materialized memory. Only zero within the region boundaries to - * avoid clobbering adjacent data. - */ - if (materialize_end > materialize_start) - memset((uint8_t *) g->host_base + materialize_start, 0, - materialize_end - materialize_start); - /* The page-table helpers above already requested the matching TLBI; no * additional flush is needed here. */ diff --git a/src/core/guest.h b/src/core/guest.h index 9054e5fe..b50563d2 100644 --- a/src/core/guest.h +++ b/src/core/guest.h @@ -1420,10 +1420,11 @@ bool guest_region_range_has_ro_shared_backing(const guest_t *g, /* Try to materialize a lazy (MAP_NORESERVE) page at the given offset. Called * from the data/instruction abort handler when the faulting address falls - * within a noreserve region. Creates page table entries for one 2MiB block - * containing the fault address, zeros the memory, and clears the noreserve flag - * for the materialized sub-range. + * within a noreserve region. Creates page table entries for the part of the + * region inside the 2MiB block containing the fault address, zeroing the pages + * that had none. Pages already mapped keep their contents. * Returns 0 on success (caller should TLBI and retry), -1 if the offset is not - * in a noreserve region. + * in a noreserve region, the region is PROT_NONE, or the page already has a + * descriptor (a stale translation, left to the caller's stale-TLB retry). */ int guest_materialize_lazy(guest_t *g, uint64_t fault_offset); diff --git a/src/syscall/proc.c b/src/syscall/proc.c index c127a543..2a8c198c 100644 --- a/src/syscall/proc.c +++ b/src/syscall/proc.c @@ -4036,9 +4036,19 @@ static bool vcpu_handle_el0_fault(guest_t *g, /* Real SIGSEGV. Permission faults (xFSC[5:2] == 0x3) map to SEGV_ACCERR; * address size, translation, and access-flag faults map to SEGV_MAPERR for - * Linux. + * Linux. A translation fault on a page the live PT now maps is also + * SEGV_ACCERR: another vCPU mapped it while this one waited for mmap_lock, + * without the access this one needs, and Linux reports a mapped page that + * denies the access as ACCERR. */ - int si_code = (fsc_type == 0x03) ? LINUX_SEGV_ACCERR : LINUX_SEGV_MAPERR; + bool mapped_now = false; + if (fsc_type == 0x01) { + mmap_lock_acquire(); + mapped_now = guest_ptr_avail(g, far_addr, NULL, 0) != NULL; + mmap_lock_release(); + } + int si_code = (fsc_type == 0x03 || mapped_now) ? LINUX_SEGV_ACCERR + : LINUX_SEGV_MAPERR; signal_set_fault_info(si_code, far_addr, esr); int sig_ret = signal_deliver_fault(vcpu, g, LINUX_SIGSEGV, exit_code); diff --git a/tests/test-matrix.sh b/tests/test-matrix.sh index b06f0d9d..5708b878 100755 --- a/tests/test-matrix.sh +++ b/tests/test-matrix.sh @@ -905,6 +905,10 @@ run_unit_tests() printf "\nmadvise MADV_DONTNEED\n" test_rc "$runner" "test-madvise" 0 "$bindir/test-madvise" + printf "\nMAP_NORESERVE first touch\n" + test_rc "$runner" "test-noreserve-materialize" 0 \ + "$bindir/test-noreserve-materialize" + printf "\nScatter-gather I/O\n" test_check "$runner" "test-readv-writev" "PASS" "$bindir/test-readv-writev" diff --git a/tests/test-noreserve-materialize.c b/tests/test-noreserve-materialize.c new file mode 100644 index 00000000..507345b3 --- /dev/null +++ b/tests/test-noreserve-materialize.c @@ -0,0 +1,325 @@ +/* + * test-noreserve-materialize.c -- first touch of MAP_NORESERVE memory + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + * + * An anonymous MAP_NORESERVE mapping is filled in on first touch. Two things + * that first touch must not do: + * + * Wipe a neighbor. A mapping placed next to one the guest already wrote, with + * the same protection, can share its 2 MiB block, and the first touch of the + * new one must leave what the guest wrote into the old one in place. + * + * Grant access PROT_NONE refuses. Touching a PROT_NONE reservation is SIGSEGV, + * on the first touch as on every later one. + * + * Map past its own end. A PROT_NONE guard next to the mapping, inside the same + * 2 MiB block, faults while the mapping is being filled in and after. + * + * The threaded case races two first touches of one fresh mapping, so a fill + * that ran after a page became writable would lose the other thread's store. + * + * A write racing the first read of a PROT_READ mapping faults as SEGV_ACCERR + * even when the fault waited for that read's fill. + * + * Every assertion is plain Linux mmap behavior. + */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test-harness.h" + +int passes = 0, fails = 0; + +#define CHUNK ((size_t) 64 * 1024) +#define BLOCK ((size_t) 2 * 1024 * 1024) +#define ROUNDS 64 + +static void *lazy_map(size_t len, int prot) +{ + void *p = mmap(NULL, len, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, + -1, 0); + return p == MAP_FAILED ? NULL : p; +} + +/* The wipe needs both mappings in one 2 MiB block. The kernel picks where b + * goes, so a round that lands it in another block proves nothing and is not + * counted; if no round shares a block the test fails rather than passing + * without having looked. + */ +static void test_neighbor_survives(void) +{ + TEST("touching a neighbor keeps data"); + int ok = 1, shared = 0; + for (int i = 0; i < ROUNDS && ok; i++) { + volatile uint8_t *a = lazy_map(CHUNK, PROT_READ | PROT_WRITE); + if (!a) { + ok = 0; + break; + } + for (size_t off = 0; off < CHUNK; off += 4096) + a[off] = 0x5a; + /* Mapped only now, so it joins a range that is already filled in. */ + volatile uint8_t *b = lazy_map(CHUNK, PROT_READ | PROT_WRITE); + if (!b) { + ok = 0; + break; + } + if (((uintptr_t) a ^ (uintptr_t) b) / BLOCK != 0) + continue; + shared++; + b[CHUNK / 2] = 1; + for (size_t off = 0; off < CHUNK; off += 4096) + if (a[off] != 0x5a) + ok = 0; + if (b[0] != 0 || b[CHUNK / 2] != 1) + ok = 0; + } + EXPECT_TRUE(ok && shared > 0, + "expected the first mapping's bytes to survive"); +} + +/* Per thread: a SIGSEGV is delivered to the thread that faulted. */ +static __thread sigjmp_buf segv_jmp; + +static void on_segv(int sig) +{ + (void) sig; + siglongjmp(segv_jmp, 1); +} + +static void test_prot_none(void) +{ + TEST("touching PROT_NONE is SIGSEGV"); + volatile uint8_t *p = lazy_map(CHUNK, PROT_NONE); + struct sigaction sa = {0}, old; + sa.sa_handler = on_segv; + sigaction(SIGSEGV, &sa, &old); + int faulted = 0; + if (p && sigsetjmp(segv_jmp, 1) == 0) + (void) p[0]; + else + faulted = 1; + sigaction(SIGSEGV, &old, NULL); + EXPECT_TRUE(p && faulted, "expected SIGSEGV on the first read"); +} + +/* One 2 MiB block holding a CHUNK of memory followed by a PROT_NONE guard, so + * the first touch fills in only part of the block. mmap guarantees only page + * alignment, so over-reserve and trim. The guard stays reserved rather than + * unmapped, so nothing else, such as a thread stack, can be placed there. + */ +static volatile uint8_t *guarded_chunk(void) +{ + uint8_t *win = lazy_map(2 * BLOCK, PROT_READ | PROT_WRITE); + if (!win) + return NULL; + uint8_t *blk = + (uint8_t *) (((uintptr_t) win + BLOCK - 1) & ~(uintptr_t) (BLOCK - 1)); + if (blk > win) + munmap(win, (size_t) (blk - win)); + if (win + 2 * BLOCK > blk + BLOCK) + munmap(blk + BLOCK, (size_t) (win + 2 * BLOCK - (blk + BLOCK))); + if (mprotect(blk + CHUNK, BLOCK - CHUNK, PROT_NONE) != 0) { + munmap(blk, BLOCK); + return NULL; + } + return blk; +} + +static volatile uint8_t *guard_p; +static volatile int guard_go, guard_done, guard_escaped; + +/* Reads the guard until the main thread's first touch is done. Every read must + * fault; one that returns saw the guard mapped while the block was filled in. + * Best effort: a read that faults then waits for the lock the fill holds, so + * few reads overlap the fill. The read of the guard after the fill is the check + * that runs every round. + */ +static void *guard_prober(void *arg) +{ + (void) arg; + while (!__atomic_load_n(&guard_go, __ATOMIC_ACQUIRE)) + ; + while (!__atomic_load_n(&guard_done, __ATOMIC_ACQUIRE)) { + if (sigsetjmp(segv_jmp, 1) == 0) { + (void) guard_p[CHUNK]; + guard_escaped = 1; + break; + } + } + return NULL; +} + +static void test_guard_after_fill(void) +{ + TEST("guard past a filled mapping is SIGSEGV"); + struct sigaction sa = {0}, old; + sa.sa_handler = on_segv; + sigaction(SIGSEGV, &sa, &old); + volatile int ok = 1; + for (int i = 0; i < ROUNDS && ok; i++) { + volatile uint8_t *p = guarded_chunk(); + if (!p) { + ok = 0; + break; + } + guard_p = p; + guard_go = guard_done = guard_escaped = 0; + pthread_t t; + if (pthread_create(&t, NULL, guard_prober, NULL) != 0) { + munmap((void *) p, BLOCK); + ok = 0; + break; + } + __atomic_store_n(&guard_go, 1, __ATOMIC_RELEASE); + if (sigsetjmp(segv_jmp, 1) == 0) + p[0] = 1; + else + ok = 0; /* The mapping itself faulted. */ + __atomic_store_n(&guard_done, 1, __ATOMIC_RELEASE); + pthread_join(t, NULL); + if (guard_escaped) + ok = 0; + if (sigsetjmp(segv_jmp, 1) == 0) { + (void) p[CHUNK]; + ok = 0; + } + munmap((void *) p, BLOCK); + } + sigaction(SIGSEGV, &old, NULL); + EXPECT_TRUE(ok, "expected SIGSEGV on the guard during and after the fill"); +} + +static volatile uint8_t *race_b; +static volatile int race_go; + +/* Stores once into every page of the fresh mapping while the main thread's + * first touch fills it in. A page published before it was zeroed takes this + * store and then loses it to the zeroing. + */ +static void *toucher(void *arg) +{ + (void) arg; + while (!__atomic_load_n(&race_go, __ATOMIC_ACQUIRE)) + ; + for (size_t off = 0; off < CHUNK; off += 4096) + race_b[off + 8] = 0xee; + return NULL; +} + +static void test_fill_race(void) +{ + TEST("first touch races a store"); + int ok = 1; + for (int i = 0; i < ROUNDS && ok; i++) { + race_b = lazy_map(CHUNK, PROT_READ | PROT_WRITE); + if (!race_b) { + ok = 0; + break; + } + race_go = 0; + pthread_t t; + if (pthread_create(&t, NULL, toucher, NULL) != 0) { + ok = 0; + break; + } + __atomic_store_n(&race_go, 1, __ATOMIC_RELEASE); + race_b[0] = 1; + pthread_join(t, NULL); + if (race_b[0] != 1) + ok = 0; + for (size_t off = 0; off < CHUNK; off += 4096) + if (race_b[off + 8] != 0xee) + ok = 0; + } + EXPECT_TRUE(ok, "expected every store to survive the fill"); +} + +static __thread int segv_code; + +static void on_segv_info(int sig, siginfo_t *si, void *uc) +{ + (void) sig; + (void) uc; + segv_code = si->si_code; + siglongjmp(segv_jmp, 1); +} + +static volatile uint8_t *ro_p; +static volatile int ro_go, ro_wrong_code; + +/* Writes every page of a fresh PROT_READ mapping while the main thread's first + * read fills it in. Every write faults, and the page is mapped by the time the + * fault is reported, so each one is SEGV_ACCERR, including a write whose fault + * waited for the other thread's fill to finish. + */ +static void *ro_writer(void *arg) +{ + (void) arg; + while (!__atomic_load_n(&ro_go, __ATOMIC_ACQUIRE)) + ; + for (size_t off = 0; off < CHUNK; off += 4096) { + if (sigsetjmp(segv_jmp, 1) == 0) { + ro_p[off] = 1; + ro_wrong_code = -1; /* The write went through. */ + break; + } + if (segv_code != SEGV_ACCERR) { + ro_wrong_code = segv_code; + break; + } + } + return NULL; +} + +static void test_read_only_race(void) +{ + TEST("write racing a read-only fill"); + struct sigaction sa = {0}, old; + sa.sa_sigaction = on_segv_info; + sa.sa_flags = SA_SIGINFO; + sigaction(SIGSEGV, &sa, &old); + int ok = 1; + for (int i = 0; i < ROUNDS && ok; i++) { + ro_p = lazy_map(CHUNK, PROT_READ); + if (!ro_p) { + ok = 0; + break; + } + ro_go = ro_wrong_code = 0; + pthread_t t; + if (pthread_create(&t, NULL, ro_writer, NULL) != 0) { + munmap((void *) ro_p, CHUNK); + ok = 0; + break; + } + __atomic_store_n(&ro_go, 1, __ATOMIC_RELEASE); + (void) ro_p[0]; + pthread_join(t, NULL); + if (ro_wrong_code) + ok = 0; + munmap((void *) ro_p, CHUNK); + } + sigaction(SIGSEGV, &old, NULL); + EXPECT_TRUE(ok, "expected SEGV_ACCERR on every write"); +} + +int main(void) +{ + printf("test-noreserve-materialize: first touch of MAP_NORESERVE\n"); + test_neighbor_survives(); + test_prot_none(); + test_guard_after_fill(); + test_fill_race(); + test_read_only_race(); + SUMMARY("test-noreserve-materialize"); + return fails ? 1 : 0; +}