Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,12 @@ $(BUILD_DIR)/test-stdio-nonblock-host: tests/test-stdio-nonblock-host.c | $(BUIL
@echo " CC $<"
$(Q)$(CC) $(CFLAGS) -Itests -o $@ $<

# test-rlimit-unchanged-host launches elfuse with ELFUSE_HOST_RLIMIT_CHECK set
# and reads what the check reports, so it is a host binary too.
$(BUILD_DIR)/test-rlimit-unchanged-host: tests/test-rlimit-unchanged-host.c | $(BUILD_DIR)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This target does not rebuild when tests/test-harness.h changes, so make check can run a stale host-test binary. Add the header as a prerequisite.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Makefile, line 350:

<comment>This target does not rebuild when `tests/test-harness.h` changes, so `make check` can run a stale host-test binary. Add the header as a prerequisite.</comment>

<file context>
@@ -345,6 +345,12 @@ $(BUILD_DIR)/test-stdio-nonblock-host: tests/test-stdio-nonblock-host.c | $(BUIL
 
+# test-rlimit-unchanged-host launches elfuse with ELFUSE_HOST_RLIMIT_CHECK set
+# and reads what the check reports, so it is a host binary too.
+$(BUILD_DIR)/test-rlimit-unchanged-host: tests/test-rlimit-unchanged-host.c | $(BUILD_DIR)
+	@echo "  CC      $<"
+	$(Q)$(CC) $(CFLAGS) -Itests -o $@ $<
</file context>
Suggested change
$(BUILD_DIR)/test-rlimit-unchanged-host: tests/test-rlimit-unchanged-host.c | $(BUILD_DIR)
$(BUILD_DIR)/test-rlimit-unchanged-host: tests/test-rlimit-unchanged-host.c \
tests/test-harness.h | $(BUILD_DIR)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed in this PR. The rule has the same shape as the host-test rules around it: test-stdio-nonblock-host, two rules up, also includes test-harness.h and lists only its .c file. Header prerequisites for the host tests belong in one change over all of those rules.

@echo " CC $<"
$(Q)$(CC) $(CFLAGS) -Itests -o $@ $<

# Guest test binaries (cross-compiled, aarch64-linux)
# Only used when GUEST_TEST_BINARIES is not set.

Expand Down Expand Up @@ -402,6 +408,12 @@ $(BUILD_DIR)/test-usbdev-ioctl-departed: tests/test-usbdev-ioctl-departed.c \
@echo " CROSS $<"
$(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -I$(BUILD_DIR) -o $@ $<

# test-rlimit-enforce starts a second thread to show SIGXFSZ is sent to the
# thread that wrote.
$(BUILD_DIR)/test-rlimit-enforce: tests/test-rlimit-enforce.c | $(BUILD_DIR)
@echo " CROSS $< (with -lpthread)"
$(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread

# test-usbdev-ioctl churns open/read/close on one usbdevfs node from four
# threads, so a close and a sibling's open contend for the same fd number.
$(BUILD_DIR)/test-usbdev-ioctl: tests/test-usbdev-ioctl.c | $(BUILD_DIR)
Expand Down
9 changes: 7 additions & 2 deletions docs/internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,7 @@ Low, fixed addresses:
```
0x00400000 - varies: ELF LOAD segments (PIE_LOAD_BASE for ET_DYN)
0x01000000: brk base (16 MiB)
0x07800000 - 0x07800FFF: Stack guard page (PROT_NONE, dynamic position)
0x07801000 - 0x07FFFFFF: Stack (8 MiB, 4 x 2 MiB blocks, RW, grows down)
0x07800000 - 0x07FFFFFF: Default stack window (8 MiB, dynamic position)
0x10000000 - 0x101FFFFF: mmap RX region (initial 2 MiB, pre-mapped RX)
0x10200000 - mmap_limit: mmap RX growth area
0x000200000000 - 0x0002001FFFFF: mmap RW region (initial 2 MiB at 8 GiB, RW)
Expand All @@ -200,6 +199,8 @@ Within-32-bit values are rendered with 8-digit padding; values that
extend above 32 bits use 12-digit padding so a reader can tell the
range class at a glance.

The stack grows on demand, as it does on Linux. `execve` maps the argument block plus up to 128 KiB at the top of the window as the `[stack]` region and leaves everything below it without page-table entries; the extra 128 KiB is cut to what `RLIMIT_STACK` and the 8 MiB window leave, and never below what the argument block already took. The first access to a page under the region, by the guest or by a syscall acting on a guest pointer, grows the region down to that page when the grown size fits `RLIMIT_STACK` and `RLIMIT_AS` and stays 1 MiB clear of an accessible mapping below; otherwise it is a `SIGSEGV`, or `EFAULT` for the syscall. The break never enters the 8 MiB window, so a default stack always has its room, and a mapping whose address `mmap` chooses, an address hint included, stays 1 MiB below the region, as the kernel's `vm_start_gap` keeps it. A larger limit lets the stack grow below the window toward the heap, which bounds it at roughly 110 MiB with the default layout; Linux has no such bound.

High addresses, anchored to `interp_base` (computed from `guest_size`, see
below). The runtime infrastructure reserve is a 16 MiB region placed at
`[interp_base - INFRA_RESERVE, interp_base)` -- in the dead zone above
Expand Down Expand Up @@ -356,6 +357,10 @@ from the one the frame was entered with.
Vectors enter EL1 from EL0 traps and forward them to the host through HVC.
DC ZVA is emulated inside the shim (it zeroes 64 bytes at the cache-line-
aligned address from the `Rt` register); HVF traps DC ZVA via `HCR_EL2.TDZ=1`.
The shim first probes the block for EL0 write access. A block that fails the
probe is forwarded on HVC #11 as the data abort the instruction would have
taken, so the host grows the stack or materializes the page and the instruction
runs again, or the guest gets `SIGSEGV`.

| HVC # | Purpose | Registers |
|-------|---------|-----------|
Expand Down
6 changes: 6 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ What they do:
expressible `proved/gva.h` preconditions are checked on every call the suite
makes. Separate from `make check` because those checks sit on the
`guest_read` / `guest_write` hot path
- `make check-locks`: rebuild with `-DELFUSE_LOCK_ASSERT`, which counts the locks each thread holds, and run `make check` on that build. It aborts when a thread calls `mmap_lock_acquire` while it holds a counted lock or is inside a `mmap_lock_nowait_begin` scope, and when a thread reads or writes guest memory while it holds a counted lock, unless it owns `mmap_lock` or is inside such a scope. Run it after adding a lock or a guest access under one
- `make lint`: static analysis through `clang-tidy`

## Quick Iteration
Expand Down Expand Up @@ -504,6 +505,11 @@ interface declares `bInterfaceNumber` 200: every byte is well formed and the
range is the field's own, but nothing anyone can plug in emits it, so it is the
only way to reach the paths that index by that number.

`ELFUSE_HOST_RLIMIT_CHECK` points at observation: no other process can read
the rlimits of an elfuse process, so with the variable set each one records its
host limits at start and, when one has moved by exit, writes a line to stderr
and exits 86. `test-rlimit-unchanged-host` runs `test-proc-limits` under it.

The lanes these drive:

| Lane | Property |
Expand Down
1 change: 1 addition & 0 deletions mk/config.mk
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ NATIVE_TESTS := tests/test-multi-vcpu.c tests/test-rwx.c \
tests/test-string-builder-host.c \
tests/test-wakeup-pipe-host.c \
tests/test-stdio-nonblock-host.c \
tests/test-rlimit-unchanged-host.c \
tests/test-guest-env-host.c \
tests/test-usb-desc-host.c \
tests/test-usbdev-urb-host.c \
Expand Down
4 changes: 3 additions & 1 deletion mk/tests.mk
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,8 @@ endef
CHECK_HOST_UNIT_BINS := $(addprefix $(BUILD_DIR)/, \
test-tlbi-encoder-host test-fork-ipc-protocol-host \
test-vcpu-run-hooks-host test-identity-override-host \
test-teardown-live-vcpu-host test-stdio-nonblock-host test-casefold-host \
test-teardown-live-vcpu-host test-stdio-nonblock-host \
test-rlimit-unchanged-host test-casefold-host \
test-casefold-walk-host test-absock-names-host \
test-dynamic-array-host test-string-builder-host \
test-wakeup-pipe-host test-guest-env-host \
Expand All @@ -273,6 +274,7 @@ $(call run-host-unit,test-dynamic-array-host,dynamic array unit test)
$(call run-host-unit,test-string-builder-host,string builder unit test)
$(call run-host-unit,test-wakeup-pipe-host,wakeup pipe concurrency unit test)
$(call run-host-unit,test-stdio-nonblock-host,launcher stdio flags across a guest)
$(call run-host-unit,test-rlimit-unchanged-host,host rlimits across a guest)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This lane silently skips the rlimit check when GUEST_TEST_BINARIES supplies the guest tests, because the host test looks only for build/test-proc-limits. Make the guest path configurable and use the configured test directory so this lane still exercises the regression.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At mk/tests.mk, line 277:

<comment>This lane silently skips the rlimit check when `GUEST_TEST_BINARIES` supplies the guest tests, because the host test looks only for `build/test-proc-limits`. Make the guest path configurable and use the configured test directory so this lane still exercises the regression.</comment>

<file context>
@@ -273,6 +274,7 @@ $(call run-host-unit,test-dynamic-array-host,dynamic array unit test)
 $(call run-host-unit,test-string-builder-host,string builder unit test)
 $(call run-host-unit,test-wakeup-pipe-host,wakeup pipe concurrency unit test)
 $(call run-host-unit,test-stdio-nonblock-host,launcher stdio flags across a guest)
+$(call run-host-unit,test-rlimit-unchanged-host,host rlimits across a guest)
 $(call run-host-unit,test-guest-env-host,guest environment merge cross product)
 $(call run-host-unit,test-usb-desc-host,USB descriptor blob walk unit test)
</file context>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed in this PR. test-rlimit-unchanged-host finds its guest the way the host test beside it does: test-stdio-nonblock-host also hard-codes build/elfuse and build/test-pipe-steal and prints SKIP when either is missing. Pointing the host tests at TEST_DIR under GUEST_TEST_BINARIES is a change to all of them, not to this one.

$(call run-host-unit,test-guest-env-host,guest environment merge cross product)
$(call run-host-unit,test-usb-desc-host,USB descriptor blob walk unit test)
$(call run-host-unit,test-usbdev-urb-host,usbdevfs URB bookkeeping unit test)
Expand Down
14 changes: 13 additions & 1 deletion mk/verify.mk
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Frama-C WP proofs

.PHONY: verify check-contracts verify-mutants check-char-signedness \
.PHONY: verify check-contracts check-locks verify-mutants check-char-signedness \
check-stub-constants check-stub-shadow print-verify-targets \
print-verify-profiles

Expand Down Expand Up @@ -751,6 +751,18 @@ verify:
# elfuse" would silently relink them: a default-looking binary carrying the
# checks on its hot path. A separate tree also means no -B is needed, since it
# starts empty.
## Rebuild with the held-lock count live, then run the suite
#
# src/debug/lock-assert.h is force-included, so every pthread mutex and rwlock
# site in the tree is counted with no change to it. Separate from "make check"
# and built into its own directory for the reasons check-contracts gives below:
# the count sits on every lock and every guest access.
check-locks:
@echo " LOCKS nothing below mmap_lock held where a thread waits on it"
$(Q)$(MAKE) BUILD_DIR=$(BUILD_DIR)/locks \
EXTRA_CFLAGS="-DELFUSE_LOCK_ASSERT -include $(CURDIR)/src/debug/lock-assert.h $(EXTRA_CFLAGS)" \
check

check-contracts:
@echo " CONTRACT proved/gva.h call-site preconditions (5 of 9 clauses)"
$(Q)$(MAKE) BUILD_DIR=$(BUILD_DIR)/contracts \
Expand Down
6 changes: 6 additions & 0 deletions scripts/check-eintr-contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@
"restartable",
"Polled retry helper; reports before the operation it guards runs.",
),
"syscall/io.c::copy_past_fsize": (
"restartable",
"Waits for a byte in an empty pipe before the write path can refuse "
"it, and only while the copy has moved nothing. It reads nothing "
"itself, so a restart re-runs the call over the same input.",
),
"syscall/io.c::io_wait_fd_or_interrupted": (
"restartable",
"Reports readiness or EINTR without transferring anything itself. It "
Expand Down
39 changes: 24 additions & 15 deletions src/core/bootstrap.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@
#include "syscall/linux-wire.h"
#include "syscall/fuse.h"
#include "syscall/internal.h"
#include "syscall/mem.h"
#include "syscall/path.h"
#include "syscall/proc.h"
#include "syscall/signal.h"
#include "syscall/sys.h"

#include "debug/log.h"

Expand Down Expand Up @@ -94,18 +96,16 @@ static void register_elf_segment_regions(guest_t *g,
uint64_t seg_start = info->segments[i].gpa + va_load_base;
uint64_t seg_end = seg_start + info->segments[i].memsz;
uint64_t seg_gpa = info->segments[i].gpa + gpa_load_base;
guest_region_add_ex_gpa(g, seg_start, seg_end, seg_gpa,
elf_pf_to_prot(info->segments[i].flags),
LINUX_MAP_PRIVATE, info->segments[i].offset,
path, -1);
guest_region_add_elf_segment(g, seg_start, seg_end, seg_gpa,
elf_pf_to_prot(info->segments[i].flags),
info->segments[i].offset, path);
}
}

/* Publish shim, shim-data, heap, stack-guard, and stack regions to
* /proc/self/maps view, and invalidate the null page and stack-guard PTEs.
* Shared by guest_bootstrap_prepare and guest_bootstrap_rosetta_post_reset; the
* caller registers ELF or rosetta segments separately because those differ
* between aarch64 and rosetta guests.
/* Publish shim, shim-data, heap, and stack regions to /proc/self/maps view, and
* invalidate the null page PTEs. Shared by guest_bootstrap_prepare and
* guest_bootstrap_rosetta_post_reset; the caller registers ELF or rosetta
* segments separately because those differ between aarch64 and rosetta guests.
*/
static void register_runtime_regions(guest_t *g, size_t shim_bin_len)
{
Expand All @@ -122,17 +122,23 @@ static void register_runtime_regions(guest_t *g, size_t shim_bin_len)
guest_region_add(g, g->shim_data_base, g->shim_data_base + BLOCK_2MIB,
LINUX_PROT_NONE, LINUX_MAP_PRIVATE, 0, "[shim-data]");

/* The vDSO page, which Linux lists as [vdso]. Tracking it also keeps a
* hinted mmap from being placed over it.
*/
guest_region_add(g, VDSO_BASE, VDSO_BASE + VDSO_SIZE,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Bootstrap adds [vdso] after PT_LOAD regions without resolving overlap, so a low-linked image covering VDSO_BASE leaves overlapping region records; exec handles the same case in the opposite order. Reserve or reject the fixed vDSO range before registering ELF segments, and keep both paths consistent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/core/bootstrap.c, line 128:

<comment>Bootstrap adds `[vdso]` after PT_LOAD regions without resolving overlap, so a low-linked image covering `VDSO_BASE` leaves overlapping region records; exec handles the same case in the opposite order. Reserve or reject the fixed vDSO range before registering ELF segments, and keep both paths consistent.</comment>

<file context>
@@ -123,6 +122,13 @@ static void register_runtime_regions(guest_t *g, size_t shim_bin_len)
+    /* The vDSO page, which Linux lists as [vdso]. Tracking it also keeps a
+     * hinted mmap from being placed over it.
+     */
+    guest_region_add(g, VDSO_BASE, VDSO_BASE + VDSO_SIZE,
+                     LINUX_PROT_READ | LINUX_PROT_EXEC, LINUX_MAP_PRIVATE, 0,
+                     "[vdso]");
</file context>

@xalestar xalestar Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overlap needs a PT_LOAD that covers VDSO_BASE (0xF000), and such an image does not run under elfuse before or after this change: vdso_build writes the vDSO over the image’s text at VDSO_BASE, and if the image starts at 0x0, the unmapped null page also removes its ELF header. I checked with a static binary linked at 0x0. It faults at 0x40 on this branch and on ba4cc0f, while Linux runs it when run as root. Rejecting or reserving the range here would only change how that image fails. The underlying gap is the fixed VDSO_BASE; running low-linked images is tracked in #440.

LINUX_PROT_READ | LINUX_PROT_EXEC, LINUX_MAP_PRIVATE, 0,
"[vdso]");

if (g->brk_base < g->brk_current) {
guest_region_add(g, g->brk_base, g->brk_current,
LINUX_PROT_READ | LINUX_PROT_WRITE,
LINUX_MAP_PRIVATE | LINUX_MAP_ANONYMOUS, 0, "[heap]");
}

guest_invalidate_ptes(g, g->stack_base, g->stack_base + STACK_GUARD_SIZE);
guest_region_add(g, g->stack_base, g->stack_base + STACK_GUARD_SIZE,
LINUX_PROT_NONE, LINUX_MAP_PRIVATE | LINUX_MAP_ANONYMOUS,
0, "[stack-guard]");
guest_region_add(g, g->stack_base + STACK_GUARD_SIZE, g->stack_top,
/* The whole window the image is built in; mem_stack_init_extent cuts it
* down to what Linux maps at execve once the stack pointer is known.
*/
guest_region_add(g, g->stack_base, g->stack_top,
LINUX_PROT_READ | LINUX_PROT_WRITE,
LINUX_MAP_PRIVATE | LINUX_MAP_ANONYMOUS, 0, "[stack]");
guest_invalidate_ptes(g, 0, 0x1000);
Expand Down Expand Up @@ -583,6 +589,7 @@ int guest_bootstrap_prepare(guest_t *g,
t0 = startup_trace_now_ns();
syscall_init();
proc_init();
sys_rlimit_init();

{
char self_path[LINUX_PATH_MAX];
Expand Down Expand Up @@ -644,6 +651,7 @@ int guest_bootstrap_prepare(guest_t *g,
return -1;
}
g->start_stack = boot->stack_pointer;
mem_stack_init_extent(g, boot->stack_pointer);
startup_trace_step("build_linux_stack", t0);

/* rosetta_argv was copied into the guest stack; the host allocation is no
Expand Down Expand Up @@ -872,7 +880,7 @@ int guest_bootstrap_rosetta_post_reset(guest_t *g,

/* Re-publish /proc/self/maps style metadata. Mirrors the bootstrap path so
* the post-exec view reports rosetta-as-anonymous-mapping plus the heap,
* stack, stack-guard, shim, and shim-data.
* stack, shim, and shim-data.
*/
register_elf_segment_regions(g, &rr.rosetta_info, 0,
g->rosetta_guest_base - g->rosetta_va_base,
Expand Down Expand Up @@ -905,6 +913,7 @@ int guest_bootstrap_rosetta_post_reset(guest_t *g,
return -1;
}
g->start_stack = sp;
mem_stack_init_extent(g, sp);
proc_set_auxv(auxv.words, auxv.nwords * sizeof(auxv.words[0]));

*out_entry_point = rr.entry_point;
Expand Down
101 changes: 95 additions & 6 deletions src/core/guest.c
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
#include "utils.h"
#include "runtime/futex.h" /* futex_interrupt_request */
#include "runtime/thread.h" /* thread_destroy_all_vcpus */
#include "syscall/mem.h" /* mem_stack_grow_for_host */
#include "syscall/proc.h" /* proc_request_exit_group */
#include "syscall/signal.h"
#include "syscall/wakeup-pipe.h"
Expand Down Expand Up @@ -1619,6 +1620,15 @@ static uint64_t gva_contiguous_avail(const guest_t *g,
return total;
}

bool guest_range_any_mapped(const guest_t *g, uint64_t start, uint64_t end)
{
gva_translation_t t;
for (uint64_t off = start; off < end; off += GUEST_PAGE_SIZE)
if (gva_translate_perm(g, g->ipa_base + off, 0, &t) == 0)
return true;
return false;
}

/* Resolve a guest virtual address to a host pointer and available size.
* Returns NULL if the address is not in any mapping.
*
Expand All @@ -1637,10 +1647,20 @@ static void *gva_resolve_perm(const guest_t *g,
* identity-mapped (GVA == GPA == offset), but L2 block descriptors carry
* permission bits and L3 page tables have per-4KiB permissions after
* guest_split_block. Skipping the walk would bypass W^X enforcement for all
* normal guest addresses.
*/
* normal guest addresses. An address just below the stack is not a miss
* until the stack has refused to grow to it: Linux grows the stack for a
* kernel access the way it does for a guest one, so read(2) into a stack
* buffer the guest has not touched yet succeeds. Growing writes page tables
* and the region table, which is why the guest is not const there; every
* guest_t this is called with is a writable object.
*/
#ifdef ELFUSE_LOCK_ASSERT
mmap_lock_assert_guest_access();
#endif
gva_translation_t first;
if (gva_translate_perm(g, gva, required_perms, &first) < 0)
if (gva_translate_perm(g, gva, required_perms, &first) < 0 &&
(!mem_stack_grow_for_host((guest_t *) g, gva) ||
gva_translate_perm(g, gva, required_perms, &first) < 0))
return NULL;
Comment on lines +1661 to 1664

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns NULL in two cases where the access should succeed.

  • A sibling thread grows the stack over gva between the first gva_translate_perm and taking mmap_lock. Then mem_stack_grow sees off >= r->start, returns false, and the second translation is skipped.
  • mmap_lock_try_acquire gives up after 1 s while another thread holds mmap_lock (a long pread fallback in sys_mmap). Then read() into a valid stack buffer gets EFAULT. Worse, deliver_signal_locked writing a frame below the extent kills the guest instead of running the handler.

Always retry the translation after the grow attempt. Treat lock contention as "wait", not "refuse", at least on paths that hold no lock ranked below mmap_lock. This hook also fires for gdbstub and crash-report reads. Those grow the guest stack (Linux 6.5+ GUP does not expand stacks for remote access) and queue any TLBI from stack_unmap_approach into a non-vCPU thread's cpu_tlbi_req, where nothing drains it.

Suggested change
if (gva_translate_perm(g, gva, required_perms, &first) < 0 &&
(!mem_stack_grow_for_host((guest_t *) g, gva) ||
gva_translate_perm(g, gva, required_perms, &first) < 0))
return NULL;
if (gva_translate_perm(g, gva, required_perms, &first) < 0) {
(void) mem_stack_grow_for_host((guest_t *) g, gva);
if (gva_translate_perm(g, gva, required_perms, &first) < 0)
return NULL;
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sibling-growth case is fixed in 09932ef: mem_stack_grow_for_host tells the caller to translate again whenever the address is in stack range, whoever did the growing.

Contention is changed in 2ba2fb0. The wait is chosen by the caller instead of being a 1 s trylock everywhere. The lock order in internal.h ranks a host access to guest memory with mmap_lock, ahead of sig_lock and the rest. A path that touches guest memory with a lower lock held brackets the access with mmap_lock_nowait_begin / mmap_lock_nowait_end, in the way of pagefault_disable, and only there does stack growth fall back to trylock; every other access waits, so a held lock no longer turns into EFAULT. make check-locks builds with -DELFUSE_LOCK_ASSERT, which counts the locks each thread holds and aborts when a thread waits on mmap_lock, or reaches guest memory outside a scope, with one held. The count is used for checking only. make check passes under that build, and removing the scope in rt_sigprocmask makes test-signal abort there. It is a held-lock count, not a pairwise order validator.


/* Computed once and clamped once per branch below. Writing *avail twice
Expand Down Expand Up @@ -2005,10 +2025,19 @@ int guest_get_used_regions(const guest_t *g,
n++;
}

/* Stack (dynamic position, stored in guest_t) */
/* Stack (dynamic position, stored in guest_t): the default reserve, and
* whatever a larger RLIMIT_STACK let it grow to below that.
*/
if (n < max) {
out[n].offset = g->stack_base;
out[n].size = g->stack_top - g->stack_base;
uint64_t stack_lo = g->stack_base;
for (int i = 0; i < g->nregions; i++) {
const guest_region_t *r = &g->regions[i];
if (r->start < stack_lo && r->end <= g->stack_top &&
Comment thread
xalestar marked this conversation as resolved.
!strcmp(r->name, "[stack]"))
stack_lo = r->start;
}
out[n].offset = stack_lo;
out[n].size = g->stack_top - stack_lo;
n++;
}

Expand Down Expand Up @@ -2231,6 +2260,24 @@ int guest_region_add_ex_gpa(guest_t *g,
0);
}

int guest_region_add_elf_segment(guest_t *g,
uint64_t start,
uint64_t end,
uint64_t gpa_base,
int prot,
uint64_t offset,
const char *name)
{
uint64_t lead = start & (GUEST_PAGE_SIZE - 1);
start -= lead;
end = PAGE_ALIGN_UP(end);
if (guest_region_remove(g, start, end) < 0)
return -1;
return guest_region_add_ex_gpa(
g, start, end, gpa_base - lead, prot, LINUX_MAP_PRIVATE,
offset >= lead ? offset - lead : 0, name, -1);
}

static uint64_t allocate_vma_id(guest_t *g)
{
uint64_t candidate = g->next_vma_id;
Expand Down Expand Up @@ -3706,6 +3753,48 @@ int guest_install_va_pages(guest_t *g,
return 0;
}

int guest_map_pages(guest_t *g, uint64_t start, uint64_t end, int perms)
Comment thread
xalestar marked this conversation as resolved.
{
if (end <= start)
return 0;

/* Only the two end blocks can be partly covered. One that has no mapping
* yet is about to get a whole-block descriptor, so note it before the
* extend and cut it back to the range after.
*/
uint64_t head = ALIGN_2MIB_DOWN(start), tail = ALIGN_2MIB_DOWN(end - 1);
const uint64_t *head_l2 = find_l2_entry(g, head);
const uint64_t *tail_l2 = find_l2_entry(g, tail);
bool trim_head = start > head && !(head_l2 && (*head_l2 & PT_VALID));
bool trim_tail =
end < tail + BLOCK_2MIB && !(tail_l2 && (*tail_l2 & PT_VALID));

int rc = guest_extend_page_tables(g, start, end, perms);
if (rc == 0 && trim_head &&
(guest_split_block(g, head) < 0 ||
guest_invalidate_ptes(g, head, start) < 0))
rc = -1;
if (rc == 0 && trim_tail &&
(guest_split_block(g, tail) < 0 ||
guest_invalidate_ptes(g, end, tail + BLOCK_2MIB) < 0))
rc = -1;
if (rc == 0)
rc = guest_update_perms(g, start, end, perms);

/* A failure can leave a fresh end block mapped whole, which is the
* over-mapping this function exists to prevent. Such a block held nothing
* before the call, so it is unmapped whole; clearing a whole block takes no
* page-table page and so cannot fail the way a split does.
*/
if (rc < 0) {
if (trim_head)
(void) guest_invalidate_ptes(g, head, head + BLOCK_2MIB);
if (trim_tail)
(void) guest_invalidate_ptes(g, tail, tail + BLOCK_2MIB);
}
return rc;
}

/* Lazy page materialization for MAP_NORESERVE. */

int guest_materialize_lazy(guest_t *g, uint64_t fault_offset)
Expand Down
Loading
Loading