diff --git a/src/core/guest.c b/src/core/guest.c index 0d2d4cebf..934d5727c 100644 --- a/src/core/guest.c +++ b/src/core/guest.c @@ -1957,12 +1957,12 @@ int guest_get_used_regions(const guest_t *g, * Check whether two adjacent regions have merge-compatible layouts. An actual * merge additionally requires a shared vma_id; a new same-generation mapping * may adopt its compatible neighbor's ID below. Regions must be contiguous in - * address space, have identical protection/flags/name, and have contiguous file - * offsets (so the merged region still represents valid mapping). For anonymous - * regions the offset is meaningless (always 0, but may become non-zero after - * split/trim), so the contiguity check is skipped. Without this, adjacent - * anonymous mmaps (common in megablock-style allocators) each create separate - * entries that exhaust the region table. + * address space, have identical protection/flags/name/shm_attach, and have + * contiguous file offsets (so the merged region still represents valid + * mapping). For anonymous regions the offset is meaningless (always 0, but may + * become non-zero after split/trim), so the contiguity check is skipped. + * Without this, adjacent anonymous mmaps (common in megablock-style allocators) + * each create separate entries that exhaust the region table. */ static bool regions_mergeable_layout(const guest_region_t *a, const guest_region_t *b) @@ -1993,6 +1993,15 @@ static bool regions_mergeable_layout(const guest_region_t *a, return false; if (strcmp(a->name, b->name) != 0) return false; + if (a->shm_attach != b->shm_attach) + return false; + + /* Pieces of one shm attach merge only where their offsets into the attach + * run on: a page mremap moved past another of the same attach is its own + * VMA on Linux, and a merged region would carry the wrong offset for it. + */ + if (a->shm_attach) + return a->offset + (a->end - a->start) == b->offset; /* Anonymous regions have no file offset to preserve. The flags field * reliably distinguishes anonymous from file-backed because every @@ -2163,12 +2172,15 @@ static uint64_t allocate_vma_id(guest_t *g) void guest_reseed_next_vma_id(guest_t *g) { - uint64_t max_id = g->next_vma_id; + uint64_t max_id = g->next_vma_id, max_attach = g->next_shm_attach; for (int i = 0; i < g->nregions; i++) { if (g->regions[i].vma_id > max_id) max_id = g->regions[i].vma_id; + if (g->regions[i].shm_attach > max_attach) + max_attach = g->regions[i].shm_attach; } g->next_vma_id = max_id; + g->next_shm_attach = max_attach; } int guest_region_add_ex_owned(guest_t *g, @@ -2198,6 +2210,24 @@ int guest_region_add_ex_owned_gpa(guest_t *g, int owned_backing_fd, bool inherited_at_fork, uint64_t vma_id) +{ + return guest_region_add_attach_gpa(g, start, end, gpa_base, prot, flags, + offset, name, owned_backing_fd, + inherited_at_fork, vma_id, 0); +} + +int guest_region_add_attach_gpa(guest_t *g, + uint64_t start, + uint64_t end, + uint64_t gpa_base, + int prot, + int flags, + uint64_t offset, + const char *name, + int owned_backing_fd, + bool inherited_at_fork, + uint64_t vma_id, + uint64_t shm_attach) { if (g->nregions >= GUEST_MAX_REGIONS) { log_error( @@ -2234,6 +2264,7 @@ int guest_region_add_ex_owned_gpa(guest_t *g, r->noreserve = (flags & LINUX_MAP_NORESERVE) != 0; r->backing_ro = false; r->inherited_at_fork = inherited_at_fork; + r->shm_attach = shm_attach; guest_region_clear_overlay(r); if (name) { str_copy_trunc(r->name, name, sizeof(r->name)); diff --git a/src/core/guest.h b/src/core/guest.h index 052c75abf..7a8db9f05 100644 --- a/src/core/guest.h +++ b/src/core/guest.h @@ -292,6 +292,10 @@ typedef struct { */ uint64_t overlay_end; /* Host-page-aligned overlay end (exclusive). */ char name[64]; /* Label: "[heap]", "[stack]", ELF path, etc. */ + uint64_t shm_attach; /* Nonzero for a SysV shm attach: a serial unique + * to that attach, so the region never merges with + * or lends its vma_id to any other mapping. + */ } guest_region_t; /* TLB invalidation request kinds. After every page-table modification, the shim @@ -540,8 +544,9 @@ typedef struct { /* Semantic region tracking for munmap/mprotect/proc-self-maps */ guest_region_t regions[GUEST_MAX_REGIONS]; - int nregions; /* Number of active regions */ - uint64_t next_vma_id; /* Last logical-VMA lineage ID allocated. */ + int nregions; /* Number of active regions */ + uint64_t next_vma_id; /* Last logical-VMA lineage ID allocated. */ + uint64_t next_shm_attach; /* Last SysV shm attach serial handed out. */ /* Set when a full region table stopped a mutation from being recorded: * guest_region_set_prot unable to split (leaving a region's old prot, or * applying the new one to a tail no PTE work covered), a munmap split @@ -844,7 +849,7 @@ static inline bool guest_range_hits_infra(const guest_t *g, /* Whether a tracked region is address space a Linux process would have. The * shim and its data sit in the infra reserve, which Linux has no mapping for, - * so they do not count toward VmSize. + * so they count toward neither VmSize nor RLIMIT_AS and RLIMIT_DATA. */ static inline bool guest_region_in_process_vm(const guest_t *g, const guest_region_t *r) @@ -1318,10 +1323,24 @@ int guest_region_add_ex_owned_gpa(guest_t *g, int owned_backing_fd, bool inherited_at_fork, uint64_t vma_id); - -/* Re-seed the logical-VMA allocator from a restored region snapshot. Fork IPC - * restores regions by value, so next_vma_id must be advanced past every - * serialized lineage before child-private mappings are added. +/* guest_region_add_ex_owned_gpa, for a region that carries @shm_attach. */ +int guest_region_add_attach_gpa(guest_t *g, + uint64_t start, + uint64_t end, + uint64_t gpa_base, + int prot, + int flags, + uint64_t offset, + const char *name, + int owned_backing_fd, + bool inherited_at_fork, + uint64_t vma_id, + uint64_t shm_attach); + +/* Re-seed the logical-VMA allocator and the shm attach serial from a restored + * region snapshot. Fork IPC restores regions by value, so next_vma_id and + * next_shm_attach must be advanced past every serialized lineage and attach + * before child-private mappings are added. */ void guest_reseed_next_vma_id(guest_t *g); diff --git a/src/runtime/fork-state.h b/src/runtime/fork-state.h index 1df816791..1afb7edad 100644 --- a/src/runtime/fork-state.h +++ b/src/runtime/fork-state.h @@ -20,7 +20,7 @@ /* Fork IPC protocol identity. Bump this whenever the header layout or ordered * fork payload changes incompatibly. */ -#define FORK_IPC_PROTOCOL_MAGIC 0x454C4653U /* "ELFS" */ +#define FORK_IPC_PROTOCOL_MAGIC 0x454C4654U /* "ELFT" */ #define IPC_MAGIC_HEADER FORK_IPC_PROTOCOL_MAGIC #define IPC_MAGIC_SENTINEL 0x454C4F4BU /* "ELOK" */ diff --git a/src/syscall/internal.h b/src/syscall/internal.h index 66a0c4eb5..5c25116d6 100644 --- a/src/syscall/internal.h +++ b/src/syscall/internal.h @@ -150,9 +150,8 @@ * log_mutex (debug/log.c) rosettad_path_lock (rosetta.c) * nl_lock (netlink.c) session_lock (proc-identity.c) * overlay_lock (chown-overlay.c) shm_dir_lock (procemu.c) - * proc_scratch_lock (procemu.c) shm_lock (sysvipc.c) - * removed_overlay_lock (fs.c) syscpu_dir_lock (procemu.c) - * sysinfo_lock (sys.c) + * proc_scratch_lock (procemu.c) syscpu_dir_lock (procemu.c) + * removed_overlay_lock (fs.c) sysinfo_lock (sys.c) * sysroot_lock (proc-state.c) * usb_lock (runtime/usb-sysfs.c) * usbdev_loop_lock (usbdev.c) @@ -964,12 +963,45 @@ int mac_to_linux_status_flags(int mac_flags); /* Translate Linux status flags (F_SETFL arg) to macOS equivalents. */ int linux_to_mac_status_flags(int linux_flags); -/* Anonymous mmap for other modules. */ - -/* Allocate anonymous guest memory. Wraps the static sys_mmap with - * MAP_PRIVATE|MAP_ANONYMOUS. Caller must hold mmap_lock. - */ -int64_t sys_mmap_anon(guest_t *g, uint64_t addr, uint64_t length, int prot); +/* Guest memory for a SysV shm attach of the segment with @key: shared + * anonymous, as a Linux attach is VM_SHARED. Its region carries a new serial, + * which is stored in *@attach. Caller must hold mmap_lock. + */ +int64_t sys_mmap_shm(guest_t *g, + uint64_t length, + int prot, + int32_t key, + uint64_t *attach); + +/* The first part starting in [@gva, @end) whose region carries the shm attach + * serial @attach at the offset into the attach that an attach at @base would + * put there, as [*@lo, *@hi) in guest addresses. *@hi is the end of that region + * and can lie past @end. This is the part Linux shmdt at @base finds: a VMA of + * the segment whose page offset matches its distance from @base. When @host is + * not NULL, *@host is set to the host memory behind *@lo, which is readable + * whatever the guest page protection. + * + * Returns false when no part of the range carries it. Caller must hold + * mmap_lock. + */ +bool mem_attach_next(guest_t *g, + uint64_t base, + uint64_t gva, + uint64_t end, + uint64_t attach, + uint64_t *lo, + uint64_t *hi, + void **host); + +/* Set @mapped[k] to whether any region carries the shm attach serial + * @attach[k], wherever the guest has moved it, for each of the @n serials. One + * walk over the regions answers all of them, and a region without a serial + * costs one test. Caller must hold mmap_lock. + */ +void mem_attach_mapped(const guest_t *g, + const uint64_t *attach, + bool *mapped, + int n); /* Borrowed-or-pinned host fd reference. * diff --git a/src/syscall/mem.c b/src/syscall/mem.c index 5f2300896..cd8302684 100644 --- a/src/syscall/mem.c +++ b/src/syscall/mem.c @@ -34,6 +34,7 @@ #include "syscall/fuse.h" #include "syscall/internal.h" #include "syscall/mem.h" +#include "syscall/sys.h" /* Protects mmap/brk bump allocators and page table extension. Multiple threads * may call mmap/brk concurrently; without this lock they could get overlapping @@ -114,6 +115,7 @@ typedef struct { bool backing_ro; bool inherited_at_fork; char name[sizeof(((guest_region_t *) 0)->name)]; + uint64_t shm_attach; } region_snapshot_t; /* Undo record for one boundary split. A split rewrites exactly one entry and @@ -462,6 +464,7 @@ typedef struct { int backing_fd; int tail_backing_fd; char name[sizeof(((guest_region_t *) 0)->name)]; + uint64_t shm_attach; } mremap_track_t; /* Release whatever descriptors the tracker still owns. Idempotent, so a failure @@ -510,6 +513,7 @@ static bool mremap_track_init(mremap_track_t *track, if (src_reg->backing_fd >= 0 && track->backing_fd < 0) return false; str_copy_trunc(track->name, src_reg->name, sizeof(track->name)); + track->shm_attach = src_reg->shm_attach; if (mremap_track_splits(track, new_size) && track->backing_fd >= 0) { track->tail_backing_fd = dup(track->backing_fd); @@ -551,17 +555,17 @@ static int add_mremap_region(guest_t *g, close(backing_fd); return -1; } - if (guest_region_add_ex_owned(g, start, start + inherited_size, prot, - flags, offset, name, backing_fd, true, - vma_id) < 0) { + if (guest_region_add_attach_gpa(g, start, start + inherited_size, start, + prot, flags, offset, name, backing_fd, + true, vma_id, track->shm_attach) < 0) { if (tail_backing_fd >= 0) close(tail_backing_fd); return -1; } - if (guest_region_add_ex_owned(g, start + inherited_size, - start + new_size, prot, flags, - offset + inherited_size, name, - tail_backing_fd, false, vma_id) < 0) { + if (guest_region_add_attach_gpa( + g, start + inherited_size, start + new_size, + start + inherited_size, prot, flags, offset + inherited_size, + name, tail_backing_fd, false, vma_id, track->shm_attach) < 0) { guest_region_remove(g, start, start + inherited_size); return -1; } @@ -570,9 +574,10 @@ static int add_mremap_region(guest_t *g, if (tail_backing_fd >= 0) close(tail_backing_fd); - return guest_region_add_ex_owned( - g, start, start + new_size, prot, flags, offset, name, backing_fd, - inherited_size == new_size && inherited_size > 0, vma_id); + return guest_region_add_attach_gpa( + g, start, start + new_size, start, prot, flags, offset, name, + backing_fd, inherited_size == new_size && inherited_size > 0, vma_id, + track->shm_attach); } static void region_clip_overlay(guest_region_t *r) @@ -1900,6 +1905,7 @@ static int capture_region_snapshots(guest_t *g, snap->backing_ro = r->backing_ro; snap->inherited_at_fork = r->inherited_at_fork; str_copy_trunc(snap->name, r->name, sizeof(snap->name)); + snap->shm_attach = r->shm_attach; } if (splits_out) @@ -2015,10 +2021,11 @@ static int restore_region_snapshots(guest_t *g, region_snapshot_t *snaps, int n) { for (int i = 0; i < n; i++) { region_snapshot_t *snap = &snaps[i]; - if (guest_region_add_ex_owned_gpa( + if (guest_region_add_attach_gpa( g, snap->start, snap->end, snap->gpa_base, snap->prot, snap->flags, snap->offset, snap->name[0] ? snap->name : NULL, - snap->backing_fd, snap->inherited_at_fork, snap->vma_id) < 0) { + snap->backing_fd, snap->inherited_at_fork, snap->vma_id, + snap->shm_attach) < 0) { snap->backing_fd = -1; close_region_snapshots(snaps, n); return -LINUX_ENOMEM; @@ -2445,6 +2452,17 @@ static bool overlay_fd_writable(int fd) return (fl & O_ACCMODE) != O_RDONLY; } +/* Whether @fd is open for reading, true when fcntl fails, as for + * overlay_fd_writable. + */ +static bool overlay_fd_readable(int fd) +{ + int fl = fcntl(fd, F_GETFL); + if (fl < 0) + return true; + return (fl & O_ACCMODE) != O_WRONLY; +} + /* Apply a real MAP_SHARED file overlay at [ipa, ipa+len) backed by [fd, * file_off). The IPA range may be sub-2 MiB; the containing 2 MiB segment is * split out first if it is not already isolated. Caller holds mmap_lock and has @@ -2654,6 +2672,204 @@ static bool heap_piece_is_plain(const guest_region_t *r) } +/* Whether a region counts toward RLIMIT_DATA once it is writable: private and + * not the stack (is_data_mapping, mm/internal.h). + */ +static bool region_may_be_data(const guest_region_t *r) +{ + return !r->shared && strcmp(r->name, "[stack]"); +} + +/* Whether a region is what Linux counts toward RLIMIT_DATA now. */ +static bool region_is_data(const guest_region_t *r) +{ + return (r->prot & LINUX_PROT_WRITE) && region_may_be_data(r); +} + +/* The bound may_expand_vm (Linux mm/mmap.c) holds data mappings to: the soft + * RLIMIT_DATA, or the hard one when the soft limit is 0, which is the kernel's + * allowance for Valgrind. + */ +static uint64_t data_rlimit(void) +{ + uint64_t cur = sys_rlimit_cur(LINUX_RLIMIT_DATA); + return cur ? cur : sys_rlimit_max(LINUX_RLIMIT_DATA); +} + +/* The part of @r that the limits count, below @win_end: from r->start, or from + * *@covered when an earlier record already counted past it, to the page-aligned + * end, as Linux maps the heap to PAGE_ALIGN(brk). A page two records share is + * counted once: a fork child that grows the break from inside a page starts a + * second [heap] record in that page, and the array is sorted, so *@covered is + * the highest end counted so far. Infra records count for nothing. + * + * Returns false when nothing of @r is left; otherwise sets [@lo, @hi) and + * advances *@covered to @hi. + */ +static bool region_counted_span(const guest_t *g, + const guest_region_t *r, + uint64_t win_end, + uint64_t *covered, + uint64_t *lo, + uint64_t *hi) +{ + if (!guest_region_in_process_vm(g, r)) + return false; + uint64_t start = r->start > *covered ? r->start : *covered; + uint64_t end = PAGE_ALIGN_UP(r->end); + if (end > win_end) + end = win_end; + if (start >= end) + return false; + *covered = end; + *lo = start; + *hi = end; + return true; +} + +/* The mapped total and the data total that may_expand_vm compares with + * RLIMIT_AS and RLIMIT_DATA, summed from the sorted region array, leaving out + * the infra reserve, each record as region_counted_span counts it. Caller holds + * mmap_lock. + * + * [stack] counts at the full extent elfuse maps for it up front (8188 KiB), + * where Linux counts the stack VMA only as far as it has grown (132 KiB right + * after exec of a static binary, measured on 6.18). The total is therefore + * about 8 MiB above Linux's, and a guest whose RLIMIT_AS was sized from a Linux + * run gets that much less room. + */ +static void mem_vm_totals(const guest_t *g, uint64_t *total, uint64_t *data) +{ + uint64_t t = 0, d = 0, covered = 0; + for (int i = 0; i < g->nregions; i++) { + const guest_region_t *r = &g->regions[i]; + uint64_t start, end; + if (!region_counted_span(g, r, UINT64_MAX, &covered, &start, &end)) + continue; + t += end - start; + if (region_is_data(r)) + d += end - start; + } + *total = t; + *data = d; +} + +/* The bytes already mapped in [@start, @end), in region coordinates, each + * record as region_counted_span counts it. Caller holds mmap_lock. + */ +static uint64_t mem_mapped_in(const guest_t *g, uint64_t start, uint64_t end) +{ + uint64_t mapped = 0, covered = start, lo, hi; + for (int i = guest_region_first_end_above(g, start); + i < g->nregions && g->regions[i].start < end; i++) { + if (region_counted_span(g, &g->regions[i], end, &covered, &lo, &hi)) + mapped += hi - lo; + } + return mapped; +} + +/* Whether a request with @prot and @flags maps data: private and writable. */ +static bool request_is_data(int prot, int flags) +{ + return (prot & LINUX_PROT_WRITE) && !(flags & LINUX_MAP_SHARED); +} + +/* RLIMIT_AS and RLIMIT_DATA at the point a request would extend the region + * table, after may_expand_vm. Both totals are summed from the sorted region + * array on each call, so they cannot drift from the region table. Only a + * request for a data mapping is held to RLIMIT_DATA. + * + * Once regions_tracker_stale is set the table has lost records and the totals + * are short by them. The limits are still judged on what is left: refusing + * instead would fail every later request of a guest that has a finite limit. + * + * @grow is the size the request adds, and @grow_is_data says whether it is a + * data mapping. What the request replaces is still counted in both totals: a + * MAP_FIXED request passes its net growth, since mmap_region checks + * may_expand_vm with the pages it replaces still counted, and a MREMAP_FIXED + * move is checked before its destination is unmapped. + * + * Returns true when the request fits both limits. Caller holds mmap_lock. + */ +static bool mem_rlimit_allows(const guest_t *g, + uint64_t grow, + bool grow_is_data) +{ + uint64_t as_limit = sys_rlimit_cur(LINUX_RLIMIT_AS); + uint64_t data_limit = data_rlimit(); + if (as_limit == LINUX_RLIM_INFINITY && + (!grow_is_data || data_limit == LINUX_RLIM_INFINITY)) + return true; + + uint64_t total, data; + mem_vm_totals(g, &total, &data); + + /* Each limit is compared with what it has left, so a request near + * UINT64_MAX cannot wrap a sum. + */ + if (total > as_limit || grow > as_limit - total) + return false; + return !grow_is_data || (data <= data_limit && grow <= data_limit - data); +} + +/* RLIMIT_DATA on an mprotect that adds PROT_WRITE, after mprotect_fixup (Linux + * mm/mprotect.c). The kernel takes the mappings of the range in address order. + * One that turns into data, a private mapping that was not writable, fails with + * ENOMEM when may_expand_vm refuses it as data and would have allowed it as + * what it was. The second half is why a range that RLIMIT_AS already refuses + * goes through. The caller changes the part below that mapping with one + * whole-range mprotect_apply, which does not stop mapping by mapping at a hole + * or a refused mapping the way mprotect_fixup does. + * + * [@start, @end) is in region coordinates. + * + * Returns true when a mapping fails, with @stop set to where the range reaches + * it. Caller holds mmap_lock. + */ +static bool mprotect_exceeds_data(const guest_t *g, + uint64_t start, + uint64_t end, + uint64_t *stop) +{ + uint64_t data_limit = data_rlimit(); + if (data_limit == LINUX_RLIM_INFINITY) + return false; + + uint64_t as_limit = sys_rlimit_cur(LINUX_RLIMIT_AS); + uint64_t total = 0, data = 0, covered = start, lo, hi; + bool summed = false; + + for (int i = guest_region_first_end_above(g, start); + i < g->nregions && g->regions[i].start < end; i++) { + const guest_region_t *r = &g->regions[i]; + + /* As mem_vm_totals counts, so a page two records share is counted once, + * whichever of the two turns into data. + */ + if (!region_counted_span(g, r, end, &covered, &lo, &hi) || + (r->prot & LINUX_PROT_WRITE) || !region_may_be_data(r)) + continue; + + /* Summed only once a mapping turns into data, so a range that is + * already writable or shared skips the walk. + */ + if (!summed) { + mem_vm_totals(g, &total, &data); + summed = true; + } + + uint64_t pages = hi - lo; + bool fits_as = total <= as_limit && pages <= as_limit - total; + bool fits_data = data <= data_limit && pages <= data_limit - data; + if (fits_as && !fits_data) { + *stop = lo; + return true; + } + data += pages; + } + return false; +} + /* How far the break may grow from its current position. * * The first tracked mapping above the break is where Linux stops a grow. An @@ -2741,6 +2957,17 @@ int64_t sys_brk(guest_t *g, uint64_t addr) return (int64_t) ipa_brk; } + /* check_data_rlimit (Linux include/linux/mm.h) holds the break extent to + * the soft RLIMIT_DATA whatever the heap's protection, before brk tells a + * grow from a shrink, so a shrink that stays over the limit fails too. The + * kernel adds the data segment (end_data - start_data), which is not + * tracked here. The soft limit is used as is: the allowance for a soft + * limit of 0 is not for brk. + */ + uint64_t data_cur = sys_rlimit_cur(LINUX_RLIMIT_DATA); + if (data_cur != LINUX_RLIM_INFINITY && new_off - g->brk_base > data_cur) + return (int64_t) ipa_brk; + /* Linux refuses a break that would run into the next mapping (do_brk_flags, * via find_vma_intersection), leaving a page between the two. Nothing here * faults on the way to tell the grow to stop: the heap is premapped RW to @@ -2767,6 +2994,13 @@ int64_t sys_brk(guest_t *g, uint64_t addr) return (int64_t) ipa_brk; } + /* do_brk_flags charges may_expand_vm with the page-aligned growth, so a + * break that stays inside its last page charges nothing. + */ + uint64_t new_pg = PAGE_ALIGN_UP(new_off), old_pg = PAGE_ALIGN_UP(old_brk); + if (new_pg > old_pg && !mem_rlimit_allows(g, new_pg - old_pg, true)) + return (int64_t) ipa_brk; + /* Shrinking can split a file-backed semantic region when a fork child has * grown and reshaped its heap metadata. Reserve the right-hand region's * backing fd before publishing the new break, so EMFILE leaves both the @@ -2912,6 +3146,49 @@ static void mmap_fresh_dispose(mmap_fresh_t *fresh) errno = saved_errno; } +/* The errno for a request the limits refuse. do_mmap reports these before + * mmap_region reaches may_expand_vm, in this order: EBADF for a bad descriptor + * (fget comes first), EEXIST when @overlaps (a MAP_FIXED_NOREPLACE over a + * mapping), EACCES for a file mapping, shared or private, on a descriptor not + * opened for reading, or for a MAP_SHARED PROT_WRITE request on one not opened + * for writing, and ENODEV for a file whose operations have no mmap (a pipe or a + * directory; a socket has one, so it reaches the limit). ENOMEM when none + * applies. Opens the descriptor when @fresh does not hold it yet, and disposes + * @fresh. + */ +static int64_t mmap_refused_errno(mmap_fresh_t *fresh, + bool is_anon, + guest_fd_t fd, + int prot, + int flags, + bool overlaps) +{ + if (!is_anon && fresh->backing_ref.fd < 0) { + int64_t ref_err = host_fd_ref_open(fd, &fresh->backing_ref); + if (ref_err < 0) { + mmap_fresh_dispose(fresh); + return ref_err; + } + } + + int64_t err = -LINUX_ENOMEM; + if (overlaps) + err = -LINUX_EEXIST; + else if (!is_anon && + (!overlay_fd_readable(fresh->backing_ref.fd) || + ((flags & LINUX_MAP_SHARED) && (prot & LINUX_PROT_WRITE) && + !overlay_fd_writable(fresh->backing_ref.fd)))) + err = -LINUX_EACCES; + else if (!is_anon) { + struct stat st; + if (fstat(fresh->backing_ref.fd, &st) == 0 && + (S_ISFIFO(st.st_mode) || S_ISDIR(st.st_mode))) + err = -LINUX_ENODEV; + } + mmap_fresh_dispose(fresh); + return err; +} + /* Pick the guest offset for a non-fixed mapping, or UINT64_MAX when the address * space is exhausted (after logging it). Executable-only mappings come from the * RX region and everything else from the RW one; a hint is honored when it @@ -3426,19 +3703,23 @@ static int mmap_extend_fresh(guest_t *g, return 0; } -/* Why this stays one function. +/* sys_mmap, but the region it records gets the name @name and the shm attach + * serial @shm_attach. Only a non-fixed request with no high-VA hint passes + * them, and that request records its region at one place below. * - * Flag combinations, not steps: MAP_FIXED, MAP_SHARED, the file-backed overlay - * and the anonymous path each qualify the others. Splitting by flag duplicates - * the interactions between them. + * Why this stays one function: flag combinations, not steps. MAP_FIXED, + * MAP_SHARED, the file-backed overlay and the anonymous path each qualify the + * others. Splitting by flag duplicates the interactions between them. */ -int64_t sys_mmap(guest_t *g, - uint64_t addr, - uint64_t length, - int prot, - int flags, - int fd, - int64_t offset) +static int64_t mmap_tagged(guest_t *g, + uint64_t addr, + uint64_t length, + int prot, + int flags, + int fd, + int64_t offset, + const char *name, + uint64_t shm_attach) { bool is_anon = (flags & LINUX_MAP_ANONYMOUS) != 0; bool needs_exec = (prot & LINUX_PROT_EXEC) != 0; @@ -3486,6 +3767,8 @@ int64_t sys_mmap(guest_t *g, */ bool is_noreplace = (flags & LINUX_MAP_FIXED_NOREPLACE) != 0; + bool is_data = request_is_data(prot, flags); + uint64_t result_off; /* Result as offset (0-based) */ if (is_fixed) { /* Addresses above TASK_SIZE (bit 63 set or beyond user VA range) are @@ -3494,9 +3777,21 @@ int64_t sys_mmap(guest_t *g, if (addr > 0x0000FFFFFFFFFFFFULL) return -LINUX_ENOMEM; - if (addr >= g->guest_size) + if (addr >= g->guest_size) { + /* Under a limit the request exceeds, mmap_refused_errno still + * reports what do_mmap checks first, as on the paths below. + */ + uint64_t replaced = !is_noreplace && addr <= UINT64_MAX - length + ? mem_mapped_in(g, addr, addr + length) + : 0; + if (!mem_rlimit_allows(g, length - replaced, is_data)) + return mmap_refused_errno( + &fresh, is_anon, fd, prot, flags, + is_noreplace && addr <= UINT64_MAX - length && + region_range_overlaps(g, addr, addr + length)); return sys_mmap_high_va(g, addr, length, prot, flags, fd, offset, true, is_noreplace); + } uint64_t off; int64_t range_err = mmap_fixed_range(g, addr, length, &off); @@ -3517,6 +3812,18 @@ int64_t sys_mmap(guest_t *g, if (!is_anon) host_backing_fd = fresh.backing_ref.fd; + /* The limits come after the address and the descriptor have been + * checked, as may_expand_vm comes after get_unmapped_area and fget in + * the kernel; mmap_refused_errno reports what do_mmap checks first. + * mmap_fixed_prepare has not changed the region table yet. mmap_region + * charges the net growth, the length less the pages it replaces, + * against totals that still count those pages. A MAP_FIXED_NOREPLACE + * range overlaps nothing by now. + */ + uint64_t replaced = mem_mapped_in(g, result_off, fix_end); + if (!mem_rlimit_allows(g, length - replaced, is_data)) + return mmap_refused_errno(&fresh, is_anon, fd, prot, flags, false); + int64_t commit_err = mmap_fixed_commit(g, &fresh, &overlay, prot, flags, fd, offset, result_off, length, host_backing_fd); @@ -3528,6 +3835,12 @@ int64_t sys_mmap(guest_t *g, * backing once the final guest range is known. */ if (!is_fixed) { + /* Under a limit the request exceeds, mmap_refused_errno still reports + * what do_mmap checks first. + */ + if (!mem_rlimit_allows(g, length, is_data)) + return mmap_refused_errno(&fresh, is_anon, fd, prot, flags, false); + if (g->is_rosetta && addr >= g->guest_size && addr <= 0x0000FFFFFFFFFFFFULL) { int64_t high_hint = sys_mmap_high_va(g, addr, length, prot, flags, @@ -3687,9 +4000,10 @@ int64_t sys_mmap(guest_t *g, */ int owned_backing_fd = fresh.track_backing_fd; fresh.track_backing_fd = -1; - if (guest_region_add_ex_owned(g, result_off, result_off + length, prot, - track_flags, is_anon ? 0 : (uint64_t) offset, - NULL, owned_backing_fd, false, 0) < 0) { + if (guest_region_add_attach_gpa( + g, result_off, result_off + length, result_off, prot, track_flags, + is_anon ? 0 : (uint64_t) offset, name, owned_backing_fd, false, 0, + shm_attach) < 0) { /* Region table was full: undo any host overlay we just installed so the * file is not left mmap'd at host_base+ipa with no tracking. Without * this, a later operation in that range would memset zeros directly @@ -4196,6 +4510,11 @@ int64_t sys_mremap(guest_t *g, uint64_t src_gpa_base = src_reg->gpa_base; uint64_t src_start = src_reg->start; + /* A MREMAP_FIXED move checks the limits once its destination is valid. */ + if (!(flags & LINUX_MREMAP_FIXED) && new_size > old_size && + !mem_rlimit_allows(g, new_size - old_size, region_is_data(src_reg))) + return finish_mremap(&source, -LINUX_ENOMEM); + /* Same size: nothing to do */ if (old_size == new_size && !(flags & LINUX_MREMAP_FIXED)) return finish_mremap(&source, (int64_t) old_addr); @@ -4233,6 +4552,16 @@ int64_t sys_mremap(guest_t *g, if (old_off < new_end && new_off < old_end) return finish_mremap(&source, -LINUX_EINVAL); + /* Linux (mm/mremap.c, since v6.17) checks the destination in + * check_mremap_params, then calls may_expand_vm from check_prep_vma + * before mremap_to unmaps the destination. So a bad destination is + * EINVAL under any limit, what is mapped there is still counted, and a + * move the limits refuse leaves the destination mapped. + */ + if (new_size > old_size && + !mem_rlimit_allows(g, new_size - old_size, region_is_data(src_reg))) + return finish_mremap(&source, -LINUX_ENOMEM); + mremap_move_t move; mremap_move_init(&move); bool source_overlay = mremap_source_has_overlay(&source); @@ -4632,11 +4961,82 @@ int64_t sys_madvise(guest_t *g, uint64_t addr, uint64_t length, int advice) } } -/* Anonymous mmap wrapper for other modules. */ -int64_t sys_mmap_anon(guest_t *g, uint64_t addr, uint64_t length, int prot) +int64_t sys_mmap(guest_t *g, + uint64_t addr, + uint64_t length, + int prot, + int flags, + int fd, + int64_t offset) { - return sys_mmap(g, addr, length, prot, - LINUX_MAP_PRIVATE | LINUX_MAP_ANONYMOUS, -1, 0); + return mmap_tagged(g, addr, length, prot, flags, fd, offset, NULL, 0); +} + +/* Guest memory for a SysV shm attach, which sys_shmat fills by copying from the + * host segment. A Linux attach is VM_SHARED: it counts toward RLIMIT_AS only, + * /proc/self/maps shows it as shared, and a fork child shares it with the + * parent. Mapping it MAP_SHARED | MAP_ANONYMOUS gives all three. The region is + * named as Linux names it in maps, and tagged with a serial no other region + * has, so it never merges with another mapping or another attach. + */ +int64_t sys_mmap_shm(guest_t *g, + uint64_t length, + int prot, + int32_t key, + uint64_t *attach) +{ + char name[sizeof(((guest_region_t *) 0)->name)]; + snprintf(name, sizeof(name), "/SYSV%08x (deleted)", (uint32_t) key); + *attach = ++g->next_shm_attach; + return mmap_tagged(g, 0, length, prot, + LINUX_MAP_SHARED | LINUX_MAP_ANONYMOUS, -1, 0, name, + *attach); +} + +bool mem_attach_next(guest_t *g, + uint64_t base, + uint64_t gva, + uint64_t end, + uint64_t attach, + uint64_t *lo, + uint64_t *hi, + void **host) +{ + if (gva >= end) + return false; + uint64_t off = gva >= g->guest_size ? gva : gva - g->ipa_base; + uint64_t off_end = off + (end - gva); + for (int i = guest_region_first_end_above(g, off); i < g->nregions; i++) { + const guest_region_t *r = &g->regions[i]; + if (r->start >= off_end) + break; + uint64_t piece = r->start > off ? r->start : off; + if (r->shm_attach == attach && + r->offset + (piece - r->start) == gva + (piece - off) - base) { + *lo = gva + (piece - off); + *hi = gva + (r->end - off); + if (host) + *host = host_ptr_for_gpa(g, r->gpa_base + (piece - r->start)); + return true; + } + } + return false; +} + +void mem_attach_mapped(const guest_t *g, + const uint64_t *attach, + bool *mapped, + int n) +{ + for (int k = 0; k < n; k++) + mapped[k] = false; + for (int i = 0; i < g->nregions; i++) { + uint64_t serial = g->regions[i].shm_attach; + for (int k = 0; serial && k < n; k++) { + if (attach[k] == serial) + mapped[k] = true; + } + } } static int compare_range_pair(const void *a, const void *b) @@ -4824,18 +5224,14 @@ static bool mprotect_same_prot_fast_path_safe(int prot) (prot & LINUX_PROT_EXEC); } -int64_t sys_mprotect(guest_t *g, uint64_t addr, uint64_t length, int prot) +/* The page-table and region-table change for a page-aligned range that passed + * the checks in sys_mprotect. + */ +static int64_t mprotect_apply(guest_t *g, + uint64_t addr, + uint64_t length, + int prot) { - if (addr & 4095) - return -LINUX_EINVAL; - if (length == 0) - return 0; - length = PAGE_ALIGN_UP(length); - if (length == 0) - return -LINUX_EINVAL; - if (addr > UINT64_MAX - length) - return -LINUX_EINVAL; - if (addr <= 0x0000FFFFFFFFFFFFULL) { if (addr >= g->guest_size) { uint64_t mprot_end = addr + length; @@ -4923,6 +5319,36 @@ int64_t sys_mprotect(guest_t *g, uint64_t addr, uint64_t length, int prot) return 0; } +int64_t sys_mprotect(guest_t *g, uint64_t addr, uint64_t length, int prot) +{ + if (addr & 4095) + return -LINUX_EINVAL; + if (length == 0) + return 0; + length = PAGE_ALIGN_UP(length); + if (length == 0) + return -LINUX_EINVAL; + if (addr > UINT64_MAX - length) + return -LINUX_EINVAL; + + if (prot & LINUX_PROT_WRITE) { + /* Region coordinates, as both branches of mprotect_apply take them. */ + uint64_t off = addr >= g->guest_size ? addr : addr - g->ipa_base; + uint64_t stop; + if (off <= UINT64_MAX - length && + mprotect_exceeds_data(g, off, off + length, &stop)) { + if (stop > off) { + int64_t err = mprotect_apply(g, addr, stop - off, prot); + if (err < 0) + return err; + } + return -LINUX_ENOMEM; + } + } + + return mprotect_apply(g, addr, length, prot); +} + /* msync helpers. */ static int same_backing_file(int fd_a, int fd_b) diff --git a/src/syscall/sys.c b/src/syscall/sys.c index ac8edd183..03ec84807 100644 --- a/src/syscall/sys.c +++ b/src/syscall/sys.c @@ -1015,6 +1015,12 @@ uint64_t sys_rlimit_cur(int resource) memory_order_relaxed); } +uint64_t sys_rlimit_max(int resource) +{ + return atomic_load_explicit(&rlimit_private.slot[resource].max, + memory_order_relaxed); +} + void sys_rlimit_send_xfsz(void) { if (!current_thread || diff --git a/src/syscall/sys.h b/src/syscall/sys.h index 1920a57a5..187fa64f0 100644 --- a/src/syscall/sys.h +++ b/src/syscall/sys.h @@ -119,6 +119,9 @@ void sys_rlimit_cpu_stop(void); */ uint64_t sys_rlimit_cur(int resource); +/* Hard limit of @resource, read the same way. */ +uint64_t sys_rlimit_max(int resource); + /* Send SIGXFSZ for a request RLIMIT_FSIZE refused. Linux sends it to the thread * that made the request (send_sig to current), so it is queued for the calling * thread alone and a sibling that has it unblocked does not take it. diff --git a/src/syscall/sysvipc.c b/src/syscall/sysvipc.c index 4c6c75560..823f01c39 100644 --- a/src/syscall/sysvipc.c +++ b/src/syscall/sysvipc.c @@ -128,25 +128,76 @@ typedef struct { #define LINUX_MSG_EXCEPT 020000 #define LINUX_MSG_COPY 040000 -/* shmat tracking table. */ +/* shmat tracking table. mmap_lock protects it: shmat and shmdt run under it, + * and shmctl does not touch the table. + */ #define MAX_SHM_ATTACH 64 typedef struct { bool active; int shmid; - void *host_addr; /* shmat() return on host */ - uint64_t guest_gva; /* guest virtual address */ - uint64_t size; /* segment size */ - bool rdonly; /* attached read-only */ + void *host_addr; /* shmat() return on host */ + uint64_t size; /* segment size */ + uint64_t attach; /* serial the guest mapping's regions carry */ + bool rdonly; /* attached read-only */ } shm_attach_t; static shm_attach_t shm_table[MAX_SHM_ATTACH]; -static pthread_mutex_t shm_lock = PTHREAD_MUTEX_INITIALIZER; #define SHM_FOR_EACH(e) \ for (shm_attach_t *e = shm_table; e < shm_table + MAX_SHM_ATTACH; e++) +/* Retire every record whose attach no region carries any more, wherever the + * guest moved it, and release its host attach. Linux detaches an attach when + * its last VMA is unmapped, by munmap or by a mapping placed over it, so such a + * record names nothing, and keeping it would hold one of the host's few + * attaches (kern.sysv.shmseg, 8 by default on macOS) and a table slot. The + * caller holds mmap_lock. + */ +static void shm_retire_unmapped(guest_t *g) +{ + shm_attach_t *rec[MAX_SHM_ATTACH]; + uint64_t attach[MAX_SHM_ATTACH]; + bool mapped[MAX_SHM_ATTACH]; + void *host[MAX_SHM_ATTACH]; + int n = 0, nhost = 0; + SHM_FOR_EACH (e) { + if (e->active) { + rec[n] = e; + attach[n++] = e->attach; + } + } + mem_attach_mapped(g, attach, mapped, n); + for (int k = 0; k < n; k++) { + if (!mapped[k]) { + rec[k]->active = false; + host[nhost++] = rec[k]->host_addr; + } + } + for (int i = 0; i < nhost; i++) + shmdt(host[i]); +} + +/* The next part of the attach at @base that shmdt detaches, from @cur on, with + * @cur equal to @base for the first call. Linux unmaps the first VMA of the + * segment it finds whole, even one that mremap grew past the segment, then only + * the later VMAs that end within the segment size from @base, and stops at the + * first one that reaches past it. Caller must hold mmap_lock. + */ +static bool shm_next_part(guest_t *g, + uint64_t base, + uint64_t cur, + uint64_t end, + uint64_t attach, + uint64_t *lo, + uint64_t *hi, + void **host) +{ + return mem_attach_next(g, base, cur, end, attach, lo, hi, host) && + (cur == base || *hi <= end); +} + /* Helper: macOS ipc_perm -> Linux ipc64_perm. */ static void mac_to_linux_ipc_perm(const struct ipc_perm *mac, @@ -235,6 +286,8 @@ int64_t sys_shmat(guest_t *g, int shmid, uint64_t shmaddr_gva, int shmflg) bool rdonly = (shmflg & LINUX_SHM_RDONLY) != 0; + shm_retire_unmapped(g); + /* Attach on host side */ void *host_addr = shmat(shmid, NULL, rdonly ? SHM_RDONLY : 0); if (host_addr == (void *) -1) @@ -249,8 +302,9 @@ int64_t sys_shmat(guest_t *g, int shmid, uint64_t shmaddr_gva, int shmflg) /* Always allocate RW initially so the code can copy shm content in. For * SHM_RDONLY, downgrade to read-only via mprotect after the copy. */ - int64_t gva = - sys_mmap_anon(g, 0, seg_size, LINUX_PROT_READ | LINUX_PROT_WRITE); + uint64_t attach; + int64_t gva = sys_mmap_shm(g, seg_size, LINUX_PROT_READ | LINUX_PROT_WRITE, + (int32_t) info.shm_perm._key, &attach); if (gva < 0) { shmdt(host_addr); return gva; /* propagate mmap error */ @@ -258,6 +312,7 @@ int64_t sys_shmat(guest_t *g, int shmid, uint64_t shmaddr_gva, int shmflg) /* Copy host shm content into guest memory */ if (guest_write(g, (uint64_t) gva, host_addr, seg_size) < 0) { + sys_munmap(g, (uint64_t) gva, PAGE_ALIGN_UP(seg_size)); shmdt(host_addr); return -LINUX_EFAULT; } @@ -270,7 +325,6 @@ int64_t sys_shmat(guest_t *g, int shmid, uint64_t shmaddr_gva, int shmflg) LINUX_PROT_READ); /* Track the attachment */ - pthread_mutex_lock(&shm_lock); shm_attach_t *entry = NULL; SHM_FOR_EACH (e) { if (!e->active) { @@ -279,55 +333,83 @@ int64_t sys_shmat(guest_t *g, int shmid, uint64_t shmaddr_gva, int shmflg) } } if (!entry) { - pthread_mutex_unlock(&shm_lock); + sys_munmap(g, (uint64_t) gva, PAGE_ALIGN_UP(seg_size)); shmdt(host_addr); return -LINUX_ENOMEM; } entry->active = true; entry->shmid = shmid; entry->host_addr = host_addr; - entry->guest_gva = (uint64_t) gva; entry->size = seg_size; + entry->attach = attach; entry->rdonly = rdonly; - pthread_mutex_unlock(&shm_lock); return gva; } int64_t sys_shmdt(guest_t *g, uint64_t shmaddr_gva) { - pthread_mutex_lock(&shm_lock); + /* Linux finds the attach to detach from the mappings, not from where it was + * made: the first VMA of a segment at or above the address whose page + * offset matches its distance from the address, so an attach the guest + * moved with mremap is detached at its new address and not at its old one. + * The record taken is likewise the one with such a part in its range from + * the address, the lowest part winning. Records whose attach is mapped + * nowhere are retired first. When no record has such a part, Linux finds no + * attach and fails with EINVAL, and what is mapped there now is neither + * copied nor unmapped. Only the parts found are detached: Linux keeps the + * segment attached while another part is mapped elsewhere, so the record + * stays live until no region carries its serial. The caller holds + * mmap_lock, which keeps the record from being taken meanwhile. + */ + shm_retire_unmapped(g); + + uint64_t lo, hi, best = UINT64_MAX; shm_attach_t *match = NULL; SHM_FOR_EACH (e) { - if (e->active && e->guest_gva == shmaddr_gva) { + uint64_t end = shmaddr_gva + PAGE_ALIGN_UP(e->size); + if (e->active && end > shmaddr_gva && + mem_attach_next(g, shmaddr_gva, shmaddr_gva, end, e->attach, &lo, + &hi, NULL) && + lo < best) { match = e; - break; + best = lo; } } - if (!match) { - pthread_mutex_unlock(&shm_lock); + if (!match) return -LINUX_EINVAL; - } - - shm_attach_t entry = *match; - match->active = false; - pthread_mutex_unlock(&shm_lock); - /* Write back guest modifications to host shm (unless read-only) */ - if (!entry.rdonly) { - /* Read guest memory back to host shm buffer */ - guest_read(g, entry.guest_gva, entry.host_addr, entry.size); + /* Write back each part found to host shm (unless read-only), then release + * it as Linux does at detach, so it stops counting toward RLIMIT_AS. The + * copy reads the memory behind the part directly, so a part the guest made + * PROT_NONE is written back too. Each step looks the next part up from the + * end of this one, so unmapping a part does not change which comes next. A + * part that fails to unmap does not stop the others, and the first error is + * returned. + */ + uint64_t gva = shmaddr_gva; + uint64_t end = gva + PAGE_ALIGN_UP(match->size); + void *src; + int64_t err = 0; + for (uint64_t cur = gva; + shm_next_part(g, gva, cur, end, match->attach, &lo, &hi, &src); + cur = hi) { + uint64_t copy_end = hi < gva + match->size ? hi : gva + match->size; + if (!match->rdonly && src) + memcpy((char *) match->host_addr + (lo - gva), src, copy_end - lo); + int64_t rc = sys_munmap(g, lo, hi - lo); + if (rc < 0 && err == 0) + err = rc; } - /* Detach host shm */ - shmdt(entry.host_addr); - - /* Guest memory remains allocated (no munmap). Linux shmdt does not - * guarantee immediate unmap either; the pages become undefined. A real - * implementation would munmap the guest region here. - */ + bool mapped; + mem_attach_mapped(g, &match->attach, &mapped, 1); + if (!mapped) { + match->active = false; + shmdt(match->host_addr); + } - return 0; + return err; } int64_t sys_shmctl(guest_t *g, int shmid, int cmd, uint64_t buf_gva) diff --git a/tests/test-fork-ipc-protocol-host.c b/tests/test-fork-ipc-protocol-host.c index 075a14cc5..8923d3944 100644 --- a/tests/test-fork-ipc-protocol-host.c +++ b/tests/test-fork-ipc-protocol-host.c @@ -24,9 +24,10 @@ #define PREVIOUS_ELFP_MAGIC 0x454C4650U #define PREVIOUS_ELFQ_MAGIC 0x454C4651U #define PREVIOUS_ELFR_MAGIC 0x454C4652U +#define PREVIOUS_ELFS_MAGIC 0x454C4653U -_Static_assert(FORK_IPC_PROTOCOL_MAGIC == 0x454C4653U, - "fork IPC protocol magic must remain ELFS until the next " +_Static_assert(FORK_IPC_PROTOCOL_MAGIC == 0x454C4654U, + "fork IPC protocol magic must remain ELFT until the next " "incompatible wire-format change"); _Static_assert(IPC_MAGIC_HEADER == FORK_IPC_PROTOCOL_MAGIC, "header magic must be the protocol identity"); @@ -46,6 +47,8 @@ _Static_assert(FORK_IPC_PROTOCOL_MAGIC != PREVIOUS_ELFQ_MAGIC, "inherited pidfd state requires rejecting ELFQ peers"); _Static_assert(FORK_IPC_PROTOCOL_MAGIC != PREVIOUS_ELFR_MAGIC, "the rlimit table in the header requires rejecting ELFR peers"); +_Static_assert(FORK_IPC_PROTOCOL_MAGIC != PREVIOUS_ELFS_MAGIC, + "the region shm_attach field requires rejecting ELFS peers"); _Static_assert(IPC_MAGIC_SENTINEL != FORK_IPC_PROTOCOL_MAGIC, "process-state sentinel must not alias the header protocol"); diff --git a/tests/test-matrix.sh b/tests/test-matrix.sh index 4b56f39ce..37bd6ac1b 100755 --- a/tests/test-matrix.sh +++ b/tests/test-matrix.sh @@ -756,6 +756,7 @@ run_unit_tests() test_check "$runner" "test-cloexec" "PASS" "$bindir/test-cloexec" test_rc "$runner" "test-exec-limits" 0 "$bindir/test-exec-limits" test_rc "$runner" "test-rlimit-fsize" 0 "$bindir/test-rlimit-fsize" + test_rc "$runner" "test-rlimit-mem" 0 "$bindir/test-rlimit-mem" test_rc "$runner" "test-session" 0 "$bindir/test-session" test_check "$runner" "test-pidfd" "0 failed" "$bindir/test-pidfd" test_check "$runner" "test-pidfd-dup" "0 failed" "$bindir/test-pidfd-dup" diff --git a/tests/test-rlimit-mem.c b/tests/test-rlimit-mem.c new file mode 100644 index 000000000..c2ce95e8c --- /dev/null +++ b/tests/test-rlimit-mem.c @@ -0,0 +1,794 @@ +/* + * Test RLIMIT_AS and RLIMIT_DATA on brk, mmap, mremap and mprotect + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + * + * A request that would take the mapped total past RLIMIT_AS, or the private + * writable total past RLIMIT_DATA, fails with ENOMEM, and the break stays where + * it was. Each limit is lowered in a fork child, so the process that reports + * the results keeps its own limits. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test-harness.h" + +int passes = 0, fails = 0; + +#define MIB ((size_t) 1 << 20) + +/* VmSize from /proc/self/status in bytes, which is what RLIMIT_AS bounds. */ +static size_t vm_size(void) +{ + char buf[4096]; + int fd = open("/proc/self/status", O_RDONLY); + ssize_t n = fd < 0 ? -1 : read(fd, buf, sizeof(buf) - 1); + if (fd >= 0) + close(fd); + if (n <= 0) + return 0; + buf[n] = '\0'; + char *p = strstr(buf, "VmSize:"); + return p ? (size_t) strtoul(p + 7, NULL, 10) * 1024 : 0; +} + +static int set_soft(int resource, rlim_t cur) +{ + struct rlimit rl; + if (getrlimit(resource, &rl) < 0) + return -1; + rl.rlim_cur = cur; + return setrlimit(resource, &rl); +} + +static void *map_anon(void *addr, size_t len, int prot, int flags) +{ + return mmap(addr, len, prot, flags | MAP_ANONYMOUS, -1, 0); +} + +/* Runs in a child with RLIMIT_AS 24 MiB above what is mapped. The exit status + * is the number of the first step that went wrong, 0 if none did. + */ +static int as_child(void) +{ + size_t base = vm_size(); + int rw = PROT_READ | PROT_WRITE; + if (base == 0 || set_soft(RLIMIT_AS, base + 24 * MIB) < 0) + return 100; + + /* 1: a mapping that stays below the limit. */ + void *a = map_anon(NULL, 16 * MIB, rw, MAP_PRIVATE); + if (a == MAP_FAILED) + return 1; + + /* 2: one that crosses it, with 8 MiB left. */ + errno = 0; + if (map_anon(NULL, 16 * MIB, rw, MAP_PRIVATE) != MAP_FAILED || + errno != ENOMEM) + return 2; + + /* 3: a shared or read-only mapping is address space too. */ + errno = 0; + if (map_anon(NULL, 16 * MIB, PROT_READ, MAP_SHARED) != MAP_FAILED || + errno != ENOMEM) + return 3; + + /* 4: MAP_FIXED over the first mapping replaces it and adds nothing. */ + if (map_anon(a, 16 * MIB, rw, MAP_PRIVATE | MAP_FIXED) != a) + return 4; + + /* 5: growing it by mremap crosses the limit. */ + errno = 0; + if (mremap(a, 16 * MIB, 32 * MIB, MREMAP_MAYMOVE) != MAP_FAILED || + errno != ENOMEM) + return 5; + + /* 6: what munmap gives back can be mapped again. */ + char *c; + if (munmap(a, 16 * MIB) < 0 || + (c = map_anon(NULL, 16 * MIB, rw, MAP_PRIVATE)) == MAP_FAILED) + return 6; + + /* 7: a bad descriptor is reported before the limit is, for a hinted, a + * placed and a fixed request alike. + */ + void *at = (char *) a + 16 * MIB; + int fixed = MAP_PRIVATE | MAP_FIXED; + errno = 0; + if (mmap(NULL, 32 * MIB, PROT_READ, MAP_PRIVATE, -1, 0) != MAP_FAILED || + errno != EBADF) + return 7; + errno = 0; + if (mmap(at, 32 * MIB, PROT_READ, fixed, -1, 0) != MAP_FAILED || + errno != EBADF) + return 7; + + /* 8: MREMAP_FIXED onto a mapping is charged with that mapping still + * counted, as Linux checks the growth before it unmaps the destination + * (since v6.17). With the limit reached, moving 4 MiB of the mapping from + * step 6 onto an 8 MiB one as 8 MiB fails, and the destination stays. + */ + void *d = map_anon(NULL, 8 * MIB, rw, MAP_PRIVATE); + if (d == MAP_FAILED) + return 100; + errno = 0; + if (mremap(c, 4 * MIB, 8 * MIB, MREMAP_MAYMOVE | MREMAP_FIXED, d) != + MAP_FAILED || + errno != ENOMEM || msync(d, 4096, MS_ASYNC) < 0) + return 8; + + /* 9: an unaligned MREMAP_FIXED destination is EINVAL even when the growth + * would cross the limit. + */ + void *far = (void *) ((1UL << 46) + 1); + errno = 0; + if (mremap(d, 8 * MIB, 16 * MIB, MREMAP_MAYMOVE | MREMAP_FIXED, far) != + MAP_FAILED || + errno != EINVAL) + return 9; + + /* 10: a shared writable mapping of a read-only descriptor is EACCES even + * when it would also cross the limit, and so is a private read-only one of + * a write-only descriptor. A pipe or a directory, which has no mmap, is + * ENODEV, while a socket, which has one, reaches the limit. + */ + char path[] = "/tmp/test-rlimit-mem-XXXXXX"; + int wfd = mkstemp(path); + int rfd = wfd < 0 ? -1 : open(path, O_RDONLY); + int ofd = wfd < 0 ? -1 : open(path, O_WRONLY); + if (wfd >= 0) + unlink(path); + if (rfd < 0 || ofd < 0 || ftruncate(wfd, 32 * MIB) < 0) + return 100; + errno = 0; + if (mmap(NULL, 32 * MIB, rw, MAP_SHARED, rfd, 0) != MAP_FAILED || + errno != EACCES) + return 10; + errno = 0; + void *wronly = mmap(NULL, 32 * MIB, PROT_READ, MAP_PRIVATE, ofd, 0); + int wronly_errno = errno; + close(ofd); + if (wronly != MAP_FAILED || wronly_errno != EACCES) + return 10; + int pf[2], sv[2]; + if (pipe(pf) < 0 || socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) + return 100; + int dfd = open("/", O_RDONLY | O_DIRECTORY); + if (dfd < 0) + return 100; + int fds[] = {pf[0], dfd, sv[0]}; + int want[] = {ENODEV, ENODEV, ENOMEM}; + for (int i = 0; i < 3; i++) { + errno = 0; + if (mmap(NULL, 32 * MIB, PROT_READ, MAP_PRIVATE, fds[i], 0) != + MAP_FAILED || + errno != want[i]) + return 10; + } + close(pf[0]); + close(pf[1]); + close(sv[0]); + close(sv[1]); + close(dfd); + + /* 11: so is a MAP_FIXED one. The hole it goes into is reserved with the + * soft limit raised to the hard one and unmapped again, so the request maps + * over nothing. A finite hard limit may leave no room for the hole, and the + * step is then skipped. + */ + struct rlimit as; + if (getrlimit(RLIMIT_AS, &as) < 0 || set_soft(RLIMIT_AS, as.rlim_max) < 0) + return 100; + void *hole = map_anon(NULL, 32 * MIB, PROT_NONE, MAP_PRIVATE); + if (hole == MAP_FAILED && as.rlim_max != RLIM_INFINITY) { + close(wfd); + close(rfd); + return 0; + } + if (hole == MAP_FAILED || munmap(hole, 32 * MIB) < 0 || + set_soft(RLIMIT_AS, base + 24 * MIB) < 0) + return 100; + errno = 0; + if (mmap(hole, 32 * MIB, rw, MAP_SHARED | MAP_FIXED, rfd, 0) != + MAP_FAILED || + errno != EACCES) + return 11; + close(wfd); + close(rfd); + return 0; +} + +/* Bytes /proc/self/maps lists, less the elfuse runtime records ([shim], + * [shim-data]) that a Linux process has no mapping for. With @data_only, only + * the private writable lines other than [stack], which RLIMIT_DATA counts. + */ +static size_t maps_size(int data_only) +{ + static char buf[64 * 1024]; + size_t len = 0; + ssize_t n = 0; + int fd = open("/proc/self/maps", O_RDONLY); + while (fd >= 0 && len < sizeof(buf) - 1 && + (n = read(fd, buf + len, sizeof(buf) - 1 - len)) > 0) + len += (size_t) n; + if (fd >= 0) + close(fd); + if (fd < 0 || n < 0 || len == sizeof(buf) - 1) + return 0; + buf[len] = '\0'; + + size_t total = 0; + for (char *line = buf, *nl; *line; line = nl + 1) { + char *end; + unsigned long lo = strtoul(line, &end, 16); + unsigned long hi = strtoul(end + 1, NULL, 16); + nl = strchr(line, '\n'); + if (!nl) + break; + *nl = '\0'; + char *perms = strchr(line, ' '); + int data = perms && perms[2] == 'w' && perms[4] == 'p' && + !strstr(line, "[stack]"); + if (!strstr(line, "[shim") && (!data_only || data)) + total += hi - lo; + } + return total; +} + +/* brk(2) called directly: musl's sbrk fails any non-zero increment without + * making the syscall. + */ +static unsigned long raw_brk(unsigned long addr) +{ + return (unsigned long) syscall(SYS_brk, addr); +} + +/* Runs in a child with RLIMIT_AS 1 MiB above what maps lists. The exit status + * is the number of the first step that went wrong, 0 if none did. + */ +static int as_exact_child(void) +{ + /* The break is left inside a page, for step 3. */ + unsigned long start = raw_brk(0); + if (raw_brk(start + 100) != start + 100) + return 100; + + size_t base = maps_size(0); + int rw = PROT_READ | PROT_WRITE; + if (base == 0 || set_soft(RLIMIT_AS, base + MIB) < 0) + return 100; + + /* 1: the last MiB under the limit. */ + if (map_anon(NULL, MIB, rw, MAP_PRIVATE) == MAP_FAILED) + return 1; + + /* 2: another MiB past it. */ + errno = 0; + if (map_anon(NULL, MIB, rw, MAP_PRIVATE) != MAP_FAILED || errno != ENOMEM) + return 2; + + /* 3: a break that stays inside its last page adds no page, so it passes a + * limit that ends inside that page. + */ + if (set_soft(RLIMIT_AS, base + MIB - (4096 - 100)) < 0) + return 100; + if (raw_brk(start + 200) != start + 200) + return 3; + + /* 4: in a fork child, a break grown inside its page starts a second heap + * record in that page. The page is counted once, so a MAP_FIXED over it + * fits a limit at the mapped total. + */ + pid_t pid = fork(); + if (pid == 0) { + size_t now = maps_size(0); + if (now == 0 || set_soft(RLIMIT_AS, now) < 0) + _exit(100); + void *page = (void *) start; + if (raw_brk(start + 300) != start + 300 || + map_anon(page, 4096, rw, MAP_PRIVATE | MAP_FIXED) != page) + _exit(4); + _exit(0); + } + int status; + if (pid < 0 || waitpid(pid, &status, 0) < 0 || !WIFEXITED(status)) + return 100; + return WEXITSTATUS(status); +} + +/* Runs in a child with RLIMIT_DATA at 24 MiB, far above the data this static + * binary starts with. The sizes stay small enough that the break has room to + * grow where no limit is set. + */ +static int data_child(void) +{ + int rw = PROT_READ | PROT_WRITE; + if (set_soft(RLIMIT_DATA, 24 * MIB) < 0) + return 100; + + /* 1: a break that stays below the limit. */ + unsigned long start = raw_brk(0); + if (raw_brk(start + 8 * MIB) != start + 8 * MIB) + return 1; + + /* 2: one that crosses it leaves the break where it was, which brk(2) + * returns. + */ + if (raw_brk(start + 40 * MIB) != start + 8 * MIB || + raw_brk(0) != start + 8 * MIB) + return 2; + + /* 3: a private writable mapping is data. */ + errno = 0; + if (map_anon(NULL, 32 * MIB, rw, MAP_PRIVATE) != MAP_FAILED || + errno != ENOMEM) + return 3; + + /* 4: a read-only or shared mapping of the same size is not. */ + char *ro = map_anon(NULL, 32 * MIB, PROT_READ, MAP_PRIVATE); + char *shared = map_anon(NULL, 32 * MIB, PROT_READ, MAP_SHARED); + if (ro == MAP_FAILED || shared == MAP_FAILED) + return 4; + + /* 5: making the private one writable turns it into data. */ + errno = 0; + if (mprotect(ro, 32 * MIB, rw) != -1 || errno != ENOMEM) + return 5; + + /* 6: a part that fits, and the whole of the shared one, may be. */ + if (mprotect(ro, 4 * MIB, rw) < 0 || mprotect(shared, 32 * MIB, rw) < 0) + return 6; + ro[0] = shared[0] = 1; + + /* 7: a range over two mappings stops at the one that does not fit, with the + * one below it already writable. The second mprotect below splits one + * mapping into the two. + */ + char *two = map_anon(NULL, 36 * MIB, PROT_NONE, MAP_PRIVATE); + int zero = open("/dev/zero", O_RDONLY); + if (two == MAP_FAILED || zero < 0 || + mprotect(two + 4 * MIB, 32 * MIB, PROT_READ) < 0) + return 100; + errno = 0; + if (mprotect(two, 36 * MIB, rw) != -1 || errno != ENOMEM || + read(zero, two, 1) != 1 || read(zero, two + 4 * MIB, 1) != -1) + return 7; + close(zero); + if (munmap(ro, 32 * MIB) < 0 || munmap(shared, 32 * MIB) < 0 || + munmap(two, 36 * MIB) < 0) + return 100; + + /* 8: a mapping below the one that does not fit stops the range first with + * its own error: a shared mapping of a read-only descriptor is EACCES. + */ + char path[] = "/tmp/test-rlimit-mem-XXXXXX"; + int wfd = mkstemp(path); + int rfd = wfd < 0 ? -1 : open(path, O_RDONLY); + if (wfd >= 0) + unlink(path); + char *pair = map_anon(NULL, 36 * MIB, PROT_READ, MAP_PRIVATE); + if (rfd < 0 || ftruncate(wfd, 4 * MIB) < 0 || pair == MAP_FAILED) + return 100; + + /* The shared mapping goes into a hole cut from the reserved range, so the + * hint is free to honor. Not MAP_FIXED: elfuse refuses a MAP_FIXED shared + * mapping of a read-only descriptor with EACCES. + */ + if (munmap(pair, 4 * MIB) < 0 || + mmap(pair, 4 * MIB, PROT_READ, MAP_SHARED, rfd, 0) != pair) + return 100; + errno = 0; + if (mprotect(pair, 36 * MIB, rw) != -1 || errno != EACCES) + return 8; + close(wfd); + close(rfd); + if (munmap(pair, 36 * MIB) < 0) + return 100; + + /* 9: a read-only heap is not data, but the break itself is still held to + * the limit. + */ + if (mprotect((void *) start, 8 * MIB, PROT_READ) < 0) + return 100; + if (raw_brk(start + 26 * MIB) != start + 8 * MIB) + return 9; + + /* 10: so is a shrink that leaves the break over the limit. */ + if (set_soft(RLIMIT_DATA, 64 * MIB) < 0 || + raw_brk(start + 32 * MIB) != start + 32 * MIB || + set_soft(RLIMIT_DATA, 24 * MIB) < 0) + return 100; + if (raw_brk(start + 32 * MIB - 4096) != start + 32 * MIB || + raw_brk(start + 8 * MIB) != start + 8 * MIB) + return 10; + + /* 11: with the data total at the limit, a private writable MAP_FIXED over a + * private read-only page grows data by a page and is charged that page, not + * counted twice and not set against a total that left it out. + */ + void *page = map_anon(NULL, 4096, PROT_READ, MAP_PRIVATE); + size_t data = maps_size(1); + if (page == MAP_FAILED || data == 0 || set_soft(RLIMIT_DATA, data) < 0) + return 100; + if (map_anon(page, 4096, rw, MAP_PRIVATE | MAP_FIXED) != page) + return 11; + + /* 12: a MREMAP_FIXED move the limit refuses leaves its destination mapped, + * as Linux checks the growth before it unmaps the destination (since + * v6.17). + */ + void *dst = map_anon(NULL, 8 * MIB, PROT_READ, MAP_PRIVATE); + if (dst == MAP_FAILED) + return 100; + errno = 0; + if (mremap(page, 4096, 8 * MIB, MREMAP_MAYMOVE | MREMAP_FIXED, dst) != + MAP_FAILED || + errno != ENOMEM) + return 12; + if (msync(dst, 4096, MS_ASYNC) < 0) + return 12; + + /* 13: a SysV shm attach is shared, so RLIMIT_DATA does not count it. */ + int id = shmget(IPC_PRIVATE, 2 * MIB, IPC_CREAT | 0600); + data = maps_size(1); + if (id < 0 || data == 0 || set_soft(RLIMIT_DATA, data + MIB / 2) < 0) { + if (id >= 0) + shmctl(id, IPC_RMID, NULL); + return 100; + } + char *seg = shmat(id, NULL, 0); + shmctl(id, IPC_RMID, NULL); + if (seg == (void *) -1) + return 13; + + /* 14: and a fork child shares it, so its write reaches the parent. */ + seg[0] = 'P'; + pid_t pid = fork(); + if (pid == 0) { + seg[0] = 'C'; + _exit(0); + } + int status; + if (pid < 0 || waitpid(pid, &status, 0) < 0 || !WIFEXITED(status)) + return 100; + if (seg[0] != 'C') + return 14; + shmdt(seg); + + /* 15: a detach releases the attach, so attaching and detaching more than + * RLIMIT_AS leaves room for in all does not run out. + */ + id = shmget(IPC_PRIVATE, 2 * MIB, IPC_CREAT | 0600); + size_t vm = vm_size(); + if (id < 0 || vm == 0 || set_soft(RLIMIT_AS, vm + 8 * MIB) < 0) { + if (id >= 0) + shmctl(id, IPC_RMID, NULL); + return 100; + } + for (int i = 0; i < 16; i++) { + seg = shmat(id, NULL, 0); + if (seg == (void *) -1 || shmdt(seg) < 0) { + shmctl(id, IPC_RMID, NULL); + return 15; + } + } + + /* 16: a detach after the attach was mapped over fails with EINVAL and + * leaves the new mapping in place. + */ + seg = shmat(id, NULL, 0); + shmctl(id, IPC_RMID, NULL); + if (seg == (void *) -1 || + map_anon(seg, 2 * MIB, rw, MAP_PRIVATE | MAP_FIXED) != seg) + return 100; + seg[0] = 'N'; + errno = 0; + if (shmdt(seg) != -1 || errno != EINVAL || msync(seg, 4096, MS_ASYNC) < 0 || + seg[0] != 'N') + return 16; + + /* 17: when only part of the attach was mapped over, the detach writes back + * and unmaps the part that remains and leaves the new mapping alone. The + * new mapping is data, so the soft RLIMIT_DATA from step 13 is raised. + */ + struct rlimit dl; + if (getrlimit(RLIMIT_DATA, &dl) < 0 || + set_soft(RLIMIT_DATA, dl.rlim_max) < 0) + return 100; + id = shmget(IPC_PRIVATE, 2 * MIB, IPC_CREAT | 0600); + seg = id < 0 ? (void *) -1 : shmat(id, NULL, 0); + if (seg == (void *) -1 || + map_anon(seg + MIB, MIB, rw, MAP_PRIVATE | MAP_FIXED) != seg + MIB) { + if (id >= 0) + shmctl(id, IPC_RMID, NULL); + return 100; + } + seg[0] = 'A'; + seg[MIB] = 'X'; + char *again = shmdt(seg) < 0 ? (void *) -1 : shmat(id, NULL, 0); + shmctl(id, IPC_RMID, NULL); + if (again == (void *) -1 || again[0] != 'A' || again[MIB] != 0 || + (again != seg + MIB && seg[MIB] != 'X')) + return 17; + + /* 18: so is a shared anonymous one over the first page, which Linux keeps + * apart from the attach. + */ + id = shmget(IPC_PRIVATE, 64 * 1024, IPC_CREAT | 0600); + seg = id < 0 ? (void *) -1 : shmat(id, NULL, 0); + if (seg == (void *) -1 || munmap(seg, 4096) < 0 || + map_anon(seg, 4096, rw, MAP_SHARED | MAP_FIXED) != seg) { + if (id >= 0) + shmctl(id, IPC_RMID, NULL); + return 100; + } + seg[0] = 'S'; + seg[4096] = 'B'; + again = shmdt(seg) < 0 ? (void *) -1 : shmat(id, NULL, 0); + shmctl(id, IPC_RMID, NULL); + if (again == (void *) -1 || again[0] != 0 || again[4096] != 'B' || + (again != seg && seg[0] != 'S')) + return 18; + + /* 19: an attach that was unmapped does not hide one made at its address + * since, which the detach then takes. + */ + int ida = shmget(IPC_PRIVATE, 64 * 1024, IPC_CREAT | 0600); + int idb = shmget(IPC_PRIVATE, 64 * 1024, IPC_CREAT | 0600); + char *sa = ida < 0 ? (void *) -1 : shmat(ida, NULL, 0); + char *sb = sa == (void *) -1 || munmap(sa, 64 * 1024) < 0 || idb < 0 + ? (void *) -1 + : shmat(idb, sa, 0); + if (ida >= 0) + shmctl(ida, IPC_RMID, NULL); + if (idb >= 0) + shmctl(idb, IPC_RMID, NULL); + if (sb == (void *) -1) + return 100; + if (sb == sa && (shmdt(sa) < 0 || msync(sa, 4096, MS_ASYNC) == 0)) + return 19; + + /* 20: a detach writes back a part the guest made PROT_NONE. */ + id = shmget(IPC_PRIVATE, 64 * 1024, IPC_CREAT | 0600); + seg = id < 0 ? (void *) -1 : shmat(id, NULL, 0); + if (seg == (void *) -1) { + if (id >= 0) + shmctl(id, IPC_RMID, NULL); + return 100; + } + seg[100] = 'W'; + if (mprotect(seg, 64 * 1024, PROT_NONE) < 0 || shmdt(seg) < 0) { + shmctl(id, IPC_RMID, NULL); + return 20; + } + again = shmat(id, NULL, 0); + if (again == (void *) -1 || again[100] != 'W') { + shmctl(id, IPC_RMID, NULL); + return 20; + } + + /* 21: so is an attach that mremap shrank and grew back in place. */ + if (mremap(again, 64 * 1024, 32 * 1024, 0) != again || + mremap(again, 32 * 1024, 64 * 1024, 0) != again) { + shmctl(id, IPC_RMID, NULL); + return 100; + } + again[0] = 'Q'; + if (shmdt(again) < 0 || msync(again, 4096, MS_ASYNC) == 0) { + shmctl(id, IPC_RMID, NULL); + return 21; + } + seg = shmat(id, NULL, 0); + shmctl(id, IPC_RMID, NULL); + if (seg == (void *) -1 || seg[0] != 'Q') + return 21; + + /* 22: an attach the guest unmapped is detached, so unmapping attaches + * without shmdt does not run out of them. elfuse keeps 64 records, and the + * macOS host allows 8 attaches per process by default. + */ + id = shmget(IPC_PRIVATE, 64 * 1024, IPC_CREAT | 0600); + if (id < 0) + return 100; + for (int i = 0; i < 80; i++) { + char *a = shmat(id, NULL, 0); + if (a == (void *) -1 || munmap(a, 64 * 1024) < 0) { + shmctl(id, IPC_RMID, NULL); + return 22; + } + } + shmctl(id, IPC_RMID, NULL); + + /* 23: an attach moved with mremap is detached at its new address, with the + * writes made there, and not at its old one. + */ + char *to = map_anon(NULL, 4096, rw, MAP_PRIVATE); + if (to == MAP_FAILED) + return 100; + id = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0600); + if (id < 0) + return 100; + seg = shmat(id, NULL, 0); + if (seg == (void *) -1 || + mremap(seg, 4096, 4096, MREMAP_MAYMOVE | MREMAP_FIXED, to) != to) { + shmctl(id, IPC_RMID, NULL); + return 23; + } + to[0] = 'M'; + errno = 0; + int old_rc = shmdt(seg); + int old_errno = errno; + int new_rc = shmdt(to); + seg = shmat(id, NULL, 0); + shmctl(id, IPC_RMID, NULL); + if (old_rc != -1 || old_errno != EINVAL || new_rc != 0 || + seg == (void *) -1 || seg[0] != 'M') + return 23; + shmdt(seg); + + /* 24: a fork child's attaches do not take the serials of the attaches it + * inherited. In a child holding 8 attaches, a grandchild attaches and + * unmaps 20 times; an attach that shared an inherited one's serial would + * look still mapped and never be detached. + */ + id = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0600); + if (id < 0) + return 100; + pid_t holder = fork(); + if (holder == 0) { + for (int i = 0; i < 8; i++) { + if (shmat(id, NULL, 0) == (void *) -1) + _exit(100); + } + pid_t gc = fork(); + if (gc == 0) { + for (int i = 0; i < 20; i++) { + char *a = shmat(id, NULL, 0); + if (a == (void *) -1 || munmap(a, 4096) < 0) + _exit(24); + } + _exit(0); + } + int gst; + _exit(gc < 0 || waitpid(gc, &gst, 0) < 0 || !WIFEXITED(gst) + ? 24 + : WEXITSTATUS(gst)); + } + int hst; + int hrc = holder < 0 || waitpid(holder, &hst, 0) < 0 || !WIFEXITED(hst) + ? 24 + : WEXITSTATUS(hst); + shmctl(id, IPC_RMID, NULL); + if (hrc != 0) + return hrc; + + /* 25: a detach at the address of a part moved away detaches only that part, + * and the part left behind still detaches at the old address, with the + * writes made there. + */ + to = map_anon(NULL, 4096, rw, MAP_PRIVATE); + if (to == MAP_FAILED) + return 100; + id = shmget(IPC_PRIVATE, 2 * 4096, IPC_CREAT | 0600); + if (id < 0) + return 100; + seg = shmat(id, NULL, 0); + if (seg == (void *) -1 || + mremap(seg, 4096, 4096, MREMAP_MAYMOVE | MREMAP_FIXED, to) != to) { + shmctl(id, IPC_RMID, NULL); + return 25; + } + seg[4096] = 'S'; + new_rc = shmdt(to); + old_rc = shmdt(seg); + seg = shmat(id, NULL, 0); + shmctl(id, IPC_RMID, NULL); + if (new_rc != 0 || old_rc != 0 || seg == (void *) -1 || seg[4096] != 'S') + return 25; + shmdt(seg); + + /* 26: an attach mremap grew in place past its segment is detached whole, + * with the writes made in the segment. + */ + char *at = map_anon(NULL, 2 * 4096, PROT_NONE, MAP_PRIVATE); + if (at == MAP_FAILED || munmap(at, 2 * 4096) < 0) + return 100; + id = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0600); + if (id < 0) + return 100; + seg = shmat(id, at, 0); + if (seg != at || mremap(seg, 4096, 2 * 4096, 0) != seg) { + shmctl(id, IPC_RMID, NULL); + return 26; + } + seg[0] = 'G'; + if (shmdt(seg) < 0 || msync(seg, 4096, MS_ASYNC) == 0 || + msync(seg + 4096, 4096, MS_ASYNC) == 0) { + shmctl(id, IPC_RMID, NULL); + return 26; + } + seg = shmat(id, NULL, 0); + shmctl(id, IPC_RMID, NULL); + if (seg == (void *) -1 || seg[0] != 'G') + return 26; + shmdt(seg); + + /* 27: a page moved past the next page of its attach does not merge with it. + * Detaching at the old address leaves the moved page attached, and each + * page's write reaches its own segment offset. + */ + at = map_anon(NULL, 3 * 4096, PROT_NONE, MAP_PRIVATE); + if (at == MAP_FAILED || munmap(at, 3 * 4096) < 0) + return 100; + id = shmget(IPC_PRIVATE, 2 * 4096, IPC_CREAT | 0600); + if (id < 0) + return 100; + seg = shmat(id, at, 0); + to = at + 2 * 4096; + if (seg != at || + mremap(seg, 4096, 4096, MREMAP_MAYMOVE | MREMAP_FIXED, to) != to) { + shmctl(id, IPC_RMID, NULL); + return 27; + } + to[0] = 'L'; + seg[4096] = 'H'; + old_rc = shmdt(seg); + int moved_mapped = msync(to, 4096, MS_ASYNC) == 0; + new_rc = shmdt(to); + seg = shmat(id, NULL, 0); + shmctl(id, IPC_RMID, NULL); + if (old_rc != 0 || !moved_mapped || new_rc != 0 || seg == (void *) -1 || + seg[0] != 'L' || seg[4096] != 'H') + return 27; + shmdt(seg); + + return 0; +} + + +/* Run @child_main in a fork child and report its first failed step. */ +static void expect_child_steps(int (*child_main)(void)) +{ + int status = -1; + pid_t pid = fork(); + if (pid == 0) + _exit(child_main()); + if (pid < 0 || waitpid(pid, &status, 0) < 0 || !WIFEXITED(status)) { + FAIL("child did not exit"); + } else if (WEXITSTATUS(status) != 0) { + printf("FAIL: step %d\n", WEXITSTATUS(status)); + fails++; + } else { + PASS(); + } +} + +int main(void) +{ + printf("test-rlimit-mem: RLIMIT_AS and RLIMIT_DATA are enforced\n"); + + TEST("RLIMIT_AS on mmap and mremap"); + expect_child_steps(as_child); + + TEST("RLIMIT_AS counts only guest mappings"); + expect_child_steps(as_exact_child); + + TEST("RLIMIT_DATA on brk, mmap and mprotect"); + expect_child_steps(data_child); + + SUMMARY("test-rlimit-mem"); + return fails ? 1 : 0; +}