From cfca490d29a26356609ce318de332b36b7136808 Mon Sep 17 00:00:00 2001 From: Max042004 Date: Sun, 4 Oct 2026 23:24:54 +0800 Subject: [PATCH] Keep live pages when filling in lazy memory guest_materialize_lazy fills in the part of a MAP_NORESERVE region that lies in the faulting 2 MiB block. Four things it did wrong: It zeroed every page of that part, including pages already mapped. A mapping placed next to one the guest has written joins the same region, so the first touch of the new mapping wiped the old one. It zeroed after publishing the descriptors, so a sibling vCPU could store through a new descriptor before the zeroing landed and lose the store. It mapped a PROT_NONE region readable, so touching a PROT_NONE reservation read zeros instead of raising SIGSEGV. For a block the region only partly covers, it published a whole 2 MiB block and trimmed it afterwards, so a sibling vCPU could reach a guard or hole in that block during the trim. Zero only the pages that had no mapping, before any descriptor is published; fill the L3 table of a partly covered block before the L2 slot points at it; refuse a region with no access bits; and leave a fault on a page another vCPU has already filled in to the bounded stale-TLB retry, which reissues the TLBI. A translation fault on a page that is mapped by the time it is reported, but without the access it needs, is SEGV_ACCERR, as Linux reports it. --- Makefile | 6 + src/core/guest.c | 215 ++++++++++++------- src/core/guest.h | 9 +- src/syscall/proc.c | 14 +- tests/test-matrix.sh | 4 + tests/test-noreserve-materialize.c | 325 +++++++++++++++++++++++++++++ 6 files changed, 490 insertions(+), 83 deletions(-) create mode 100644 tests/test-noreserve-materialize.c 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; +}