已开启
Add 18 testcases cover P0 to P2 testpoints in issue 1 #20
gao-tianxiong创建于 26 天前
Add 18 testcases cover P0 to P2 testpoints in issue 1 #20
已开启
共 33 个文件变更+3581-127
| @@ -556,7 +556,9 @@ Current utility headers: | |||
| 556 | - `utils/numa_setup.h`: NUMA-backed user-memory mapping and migration | 556 | - `utils/numa_setup.h`: NUMA-backed user-memory mapping and migration |
| 557 | helpers (libnuma allocation, mmap+move_pages, mmap+mbind, with optional | 557 | helpers (libnuma allocation, mmap+move_pages, mmap+mbind, with optional |
| 558 | read/write faulting before migration); pair `map_numa_memory()` with | 558 | read/write faulting before migration); pair `map_numa_memory()` with |
| 559 | - `unmap_numa_memory()` using the same `numa_map_method` | 559 | + `unmap_numa_memory()` using the same `numa_map_method`; use |
| 560 | + `mbind_numa_memory()` to bind an already-mapped range to one node for | ||
| 561 | + future faults (e.g. a hugetlb mapping faulted later by page pinning) | ||
| 560 | - `utils/param_fmt.h`: parameter formatting helpers for formatting complex | 562 | - `utils/param_fmt.h`: parameter formatting helpers for formatting complex |
| 561 | types and function-call parameters in error output; provides sprintf-style | 563 | types and function-call parameters in error output; provides sprintf-style |
| 562 | leaf formatters and convenience wrappers returning `const char*` | 564 | leaf formatters and convenience wrappers returning `const char*` |
| @@ -29,6 +29,7 @@ where the group's test files live. | |||
| 29 | | `tests/obmm/datapath/` | `obmm_datapath` | | 29 | | `tests/obmm/datapath/` | `obmm_datapath` | |
| 30 | | `tests/obmm/performance/` | `performance` | | 30 | | `tests/obmm/performance/` | `performance` | |
| 31 | | `tests/obmm/observability/` | `observability` | | 31 | | `tests/obmm/observability/` | `observability` | |
| 32 | +| `tests/obmm/ptrace_vma_access/` | `ptrace_vma_access` | | ||
| 32 | | `tests/obmm/bugfix/` | `bugfix` | | 33 | | `tests/obmm/bugfix/` | `bugfix` | |
| 33 | | `tests/obmm/ras/` | `ras` | | 34 | | `tests/obmm/ras/` | `ras` | |
| 34 | 35 | ||
| @@ -120,6 +121,17 @@ Observability tests verifying `/proc` and `/sys` interfaces: preimport | |||
| 120 | procfs, address query API round-trip, `/proc/pid/maps` display, and | 121 | procfs, address query API round-trip, `/proc/pid/maps` display, and |
| 121 | sysfs attribute reads. | 122 | sysfs attribute reads. |
| 122 | 123 | ||
| 124 | +### `ptrace_vma_access` — Ptrace / VMA Access Tests | ||
| 125 | + | ||
| 126 | +Tests verifying the obmm_shmdev custom access handler | ||
| 127 | +(`vm_ops->access`) used by cross-process inspection paths (ptrace, | ||
| 128 | +`/proc/pid/mem`) on `VM_PFNMAP` VMAs: permission behavior against VMA | ||
| 129 | +protections, region type dispatch (export vs import), and page-table | ||
| 130 | +lookup paths. Cases are single-actor and fork-based on one machine; | ||
| 131 | +cross-process inspection reaches the driver through | ||
| 132 | +`PTRACE_PEEKDATA`/`PTRACE_POKEDATA`, not `process_vm_readv()` (GUP | ||
| 133 | +rejects `VM_PFNMAP` VMAs). | ||
| 134 | + | ||
| 123 | ### `bugfix` — Regression Tests | 135 | ### `bugfix` — Regression Tests |
| 124 | 136 | ||
| 125 | Regression tests guarding against previously-fixed bugs: open/mmap | 137 | Regression tests guarding against previously-fixed bugs: open/mmap |
| @@ -97,6 +97,23 @@ int map_numa_memory(void **ptr, size_t size, unsigned int numa_id, enum numa_map | |||
| 97 | */ | 97 | */ |
| 98 | int unmap_numa_memory(void *ptr, size_t size, enum numa_map_method method); | 98 | int unmap_numa_memory(void *ptr, size_t size, enum numa_map_method method); |
| 99 | 99 | ||
| 100 | +/** | ||
| 101 | + * @brief Bind an existing mapping to one NUMA node with `mbind()` | ||
| 102 | + * @param ptr Start of the mapped range (must be page-aligned) | ||
| 103 | + * @param size Range size in bytes | ||
| 104 | + * @param numa_id Target NUMA node ID | ||
| 105 | + * @return 0 on success, -1 on error | ||
| 106 | + * @pre ptr != NULL | ||
| 107 | + * | ||
| 108 | + * Applies `MPOL_BIND` with zero flags: the policy only affects future page | ||
| 109 | + * faults, and existing pages are neither migrated nor verified. Binding a | ||
| 110 | + * freshly created mapping before its first fault pins the allocation node | ||
| 111 | + * regardless of the CPU the faulting thread runs on -- for example a hugetlb | ||
| 112 | + * mapping whose first fault happens implicitly when a later call pins the | ||
| 113 | + * pages. | ||
| 114 | + */ | ||
| 115 | +int mbind_numa_memory(void *ptr, size_t size, unsigned int numa_id); | ||
| 116 | + | ||
| 100 | /** | 117 | /** |
| 101 | * @brief Migrate physical pages of a user-space memory region to a target NUMA node | 118 | * @brief Migrate physical pages of a user-space memory region to a target NUMA node |
| 102 | * @param ptr Start of the memory region (must be page-aligned) | 119 | * @param ptr Start of the memory region (must be page-aligned) |
| @@ -21,7 +21,8 @@ struct symbol_entry { | |||
| 21 | size_t storage_size; | 21 | size_t storage_size; |
| 22 | }; | 22 | }; |
| 23 | 23 | ||
| 24 | -#define SYMBOL_ENTRY(func) {#func, &func, sizeof(func)} | 24 | +#define SYMBOL_ENTRY(func) \ |
| 25 | + { #func, &func, sizeof(func) } | ||
| 25 | 26 | ||
| 26 | /** | 27 | /** |
| 27 | * @brief Runtime module descriptor for optional library loading | 28 | * @brief Runtime module descriptor for optional library loading |
| @@ -328,3 +328,13 @@ int unmap_numa_memory(void *ptr, size_t size, enum numa_map_method method) { | |||
| 328 | return -1; | 328 | return -1; |
| 329 | } | 329 | } |
| 330 | } | 330 | } |
| 331 | + | ||
| 332 | +int mbind_numa_memory(void *ptr, size_t size, unsigned int numa_id) { | ||
| 333 | + if (ptr == NULL) { | ||
| 334 | + LOG_ERROR("%s: ptr is NULL", __func__); | ||
| 335 | + errno = EINVAL; | ||
| 336 | + return -1; | ||
| 337 | + } | ||
| 338 | + | ||
| 339 | + return do_mbind(ptr, size, (int)numa_id, 0); | ||
| 340 | +} | ||
| @@ -50,6 +50,12 @@ _Static_assert((UBMEM_DECODER_CONFIG_KP950_FLAGS_NC_DEFAULT & ~0xFFu) == 0, "KP9 | |||
| 50 | * Translates vendor-agnostic profile and flags into the KP950-specific | 50 | * Translates vendor-agnostic profile and flags into the KP950-specific |
| 51 | * payload format. Rejects unknown generic flag bits. | 51 | * payload format. Rejects unknown generic flag bits. |
| 52 | * | 52 | * |
| 53 | + * KP950 forces single-path routing: the ONEPATH bit is always set in | ||
| 54 | + * the final flags regardless of the caller's request, so multipath | ||
| 55 | + * (onepath=0) params are silently upgraded to single-path on KP950. | ||
| 56 | + * Other die versions are unaffected because they use their own vendor | ||
| 57 | + * implementations. | ||
| 58 | + * | ||
| 53 | * Bit-alignment invariant: the generic ubmem_decoder_config_flag bits and | 59 | * Bit-alignment invariant: the generic ubmem_decoder_config_flag bits and |
| 54 | * the KP950 UBMEM_DECODER_CONFIG_KP950_FLAG_* bits share identical | 60 | * the KP950 UBMEM_DECODER_CONFIG_KP950_FLAG_* bits share identical |
| 55 | * positions (1<<0..1<<5), so a direct OR of the validated generic bits | 61 | * positions (1<<0..1<<5), so a direct OR of the validated generic bits |
| @@ -89,6 +95,9 @@ static int kp950_fill_decoder_config(struct ubmem_decoder_config *config, unsign | |||
| 89 | 95 | ||
| 90 | translated_flags |= decoder_flags; | 96 | translated_flags |= decoder_flags; |
| 91 | 97 | ||
| 98 | + /* KP950 only supports single-path routing; force ONEPATH on. */ | ||
| 99 | + translated_flags |= UBMEM_DECODER_CONFIG_KP950_FLAG_ONEPATH; | ||
| 100 | + | ||
| 92 | config->kp950.flags = (uint8_t)translated_flags; | 101 | config->kp950.flags = (uint8_t)translated_flags; |
| 93 | return 0; | 102 | return 0; |
| 94 | } | 103 | } |
| @@ -737,19 +737,6 @@ static int nrt_run_test_for_param(const struct nrt_run_config *cfg) { | |||
| 737 | return 0; | 737 | return 0; |
| 738 | } | 738 | } |
| 739 | 739 | ||
| 740 | -static _Bool nrt_param_filter(const void *param) { | ||
Y | |||
| 741 | - const struct nrt_test_param *p = (const struct nrt_test_param *)param; | ||
| 742 | - unsigned int profile; | ||
| 743 | - unsigned long flags; | ||
| 744 | - | ||
| 745 | - nrt_dcfg_to_profile_flags(p->predefined_dcfg_type, &profile, &flags); | ||
| 746 | - if ((flags & UBMEM_DECODER_CONFIG_FLAG_ONEPATH) == 0) { | ||
| 747 | - LOG_DEBUG("[filter] skipping %s: multipath (onepath=0) not supported", p->name); | ||
| 748 | - return false; | ||
| 749 | - } | ||
| 750 | - return true; | ||
| 751 | -} | ||
| 752 | - | ||
| 753 | static int nrt_test(void) { | 740 | static int nrt_test(void) { |
| 754 | const struct test_case *tc = get_test_case(); | 741 | const struct test_case *tc = get_test_case(); |
| 755 | const struct nrt_test_param_set *param_set; | 742 | const struct nrt_test_param_set *param_set; |
| @@ -769,11 +756,6 @@ static int nrt_test(void) { | |||
| 769 | for (i = 0; i < param_set->count; i++) { | 756 | for (i = 0; i < param_set->count; i++) { |
| 770 | const struct nrt_test_param *p = ¶m_set->params[i]; | 757 | const struct nrt_test_param *p = ¶m_set->params[i]; |
| 771 | 758 | ||
| 772 | - if (!nrt_param_filter(p)) { | ||
| 773 | - LOG_DEBUG("nrt_test: skipping param[%u] %s (filtered)", i, p->name); | ||
| 774 | - continue; | ||
| 775 | - } | ||
| 776 | - | ||
| 777 | { | 759 | { |
| 778 | struct nrt_test_param p0 = *p; | 760 | struct nrt_test_param p0 = *p; |
| 779 | p0.resolve_port_roles = SMALLER_AS_EXP; | 761 | p0.resolve_port_roles = SMALLER_AS_EXP; |
| @@ -12,6 +12,8 @@ | |||
| 12 | * - mmap-perm-combos: open-flags x mmap-prot permission combinations | 12 | * - mmap-perm-combos: open-flags x mmap-prot permission combinations |
| 13 | * - mmap-partial-offset: partial mmap with non-zero offset and VMA size check | 13 | * - mmap-partial-offset: partial mmap with non-zero offset and VMA size check |
| 14 | * - mmap-offset-overflow: mmap with offset beyond region (negative test) | 14 | * - mmap-offset-overflow: mmap with offset beyond region (negative test) |
| 15 | + * - mmap-length-overflow: offset within region but size spilling past the | ||
| 16 | + * region end (negative test), with an exactly-fitting boundary control | ||
| 15 | * - mmap-rdonly-write: PROT_WRITE on O_RDONLY fd (negative test) | 17 | * - mmap-rdonly-write: PROT_WRITE on O_RDONLY fd (negative test) |
| 16 | * | 18 | * |
| 17 | * The test exports one shmdev with ALLOW_MMAP, then runs each step | 19 | * The test exports one shmdev with ALLOW_MMAP, then runs each step |
| @@ -203,7 +205,53 @@ static int verify_offset_overflow_rejected(mem_id id, size_t region_size) { | |||
| 203 | return 0; | 205 | return 0; |
| 204 | } | 206 | } |
| 205 | 207 | ||
| 206 | -/* mmap-rdonly-write: O_RDONLY fd + PROT_WRITE mmap (negative) */ | 208 | +/* |
| 209 | + * mmap-length-overflow: offset within region but size spilling past the | ||
| 210 | + * region end — size > mem_size - offset must fail with EINVAL. A size | ||
| 211 | + * exactly filling the remaining space at the same offset is the | ||
| 212 | + * boundary success control. | ||
| 213 | + */ | ||
| 214 | +static int verify_length_overflow_rejected(mem_id id, size_t region_size) { | ||
| 215 | + int fd; | ||
| 216 | + void *addr; | ||
| 217 | + size_t page_size; | ||
| 218 | + size_t ok_size = 2 * 1024 * 1024; | ||
| 219 | + size_t offset; | ||
| 220 | + size_t bad_size; | ||
| 221 | + | ||
| 222 | + ASSERT_ERRNO_MSG(get_page_size(&page_size) == 0, "mmap-length-overflow: get_page_size failed"); | ||
| 223 | + ASSERT_MSG(region_size > ok_size, "mmap-length-overflow: region %#zx too small for the overflow step", region_size); | ||
| 224 | + | ||
| 225 | + offset = region_size - ok_size; | ||
| 226 | + bad_size = ok_size + page_size; | ||
| 227 | + | ||
| 228 | + ASSERT_ERRNO_MSG(open_obmm_shmdev(&fd, id, O_RDWR) == 0, "mmap-length-overflow: open failed"); | ||
| 229 | + | ||
| 230 | + /* boundary control: size exactly fills [offset, region) */ | ||
| 231 | + addr = mmap(NULL, ok_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, offset); | ||
| 232 | + ASSERT_MSG(addr != MAP_FAILED, "mmap-length-overflow: boundary mmap failed: %s", strerror(errno)); | ||
| 233 | + ASSERT_MSG(munmap(addr, ok_size) == 0, "mmap-length-overflow: boundary munmap failed"); | ||
| 234 | + | ||
| 235 | + /* one page beyond the remaining space must be rejected */ | ||
| 236 | + addr = mmap(NULL, bad_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, offset); | ||
| 237 | + if (addr != MAP_FAILED) { | ||
| 238 | + LOG_ERROR("mmap-length-overflow: size %#zx at offset %#zx should fail", bad_size, offset); | ||
| 239 | + munmap(addr, bad_size); | ||
| 240 | + close(fd); | ||
| 241 | + return -1; | ||
| 242 | + } | ||
| 243 | + | ||
| 244 | + ASSERT_MSG(errno == EINVAL, "mmap-length-overflow: unexpected errno %d (%s)", errno, strerror(errno)); | ||
| 245 | + | ||
| 246 | + LOG_DEBUG("mmap-length-overflow: size overflow rejected (offset=%#zx ok=%#zx bad=%#zx)", offset, ok_size, bad_size); | ||
| 247 | + | ||
| 248 | + close(fd); | ||
| 249 | + return 0; | ||
| 250 | +} | ||
| 251 | + | ||
| 252 | +/* | ||
| 253 | + * mmap-rdonly-write: O_RDONLY fd + PROT_WRITE mmap (negative) | ||
| 254 | + */ | ||
| 207 | static int verify_rdonly_write_rejected(mem_id id) { | 255 | static int verify_rdonly_write_rejected(mem_id id) { |
| 208 | int fd; | 256 | int fd; |
| 209 | void *addr; | 257 | void *addr; |
| @@ -262,6 +310,7 @@ static int basic_mmap_test(void) { | |||
| 262 | ASSERT_MSG(verify_permission_combinations(id) == 0, "mmap-perm-combos failed"); | 310 | ASSERT_MSG(verify_permission_combinations(id) == 0, "mmap-perm-combos failed"); |
| 263 | ASSERT_MSG(verify_partial_mmap_offset(id, region_size) == 0, "mmap-partial-offset failed"); | 311 | ASSERT_MSG(verify_partial_mmap_offset(id, region_size) == 0, "mmap-partial-offset failed"); |
| 264 | ASSERT_MSG(verify_offset_overflow_rejected(id, region_size) == 0, "mmap-offset-overflow failed"); | 312 | ASSERT_MSG(verify_offset_overflow_rejected(id, region_size) == 0, "mmap-offset-overflow failed"); |
| 313 | + ASSERT_MSG(verify_length_overflow_rejected(id, region_size) == 0, "mmap-length-overflow failed"); | ||
| 265 | ASSERT_MSG(verify_rdonly_write_rejected(id) == 0, "mmap-rdonly-write failed"); | 314 | ASSERT_MSG(verify_rdonly_write_rejected(id) == 0, "mmap-rdonly-write failed"); |
| 266 | 315 | ||
| 267 | obmm_unexport(id, 0); | 316 | obmm_unexport(id, 0); |
| @@ -0,0 +1,190 @@ | |||
| 1 | +/* SPDX-License-Identifier: MulanPSL-2.0 */ | ||
| 2 | +/** | ||
| 3 | + * @file fault_sigbus_on_no_access.c | ||
| 4 | + * @brief Validates fatal-fault enforcement on a NO_ACCESS-owned subrange. | ||
| 5 | + * | ||
| 6 | + * Single-actor test (with a fork-isolated fault access) verifying that after | ||
| 7 | + * obmm_set_ownership() switches part of a cacheable mapping to PROT_NONE, | ||
| 8 | + * any access to that range kills the faulting process with a fatal fault | ||
| 9 | + * signal, while the surrounding ranges keep working and the transition is | ||
| 10 | + * reversible. | ||
| 11 | + * | ||
| 12 | + * The case is intentionally independent of OBMM read/write data | ||
| 13 | + * consistency, which virtual environments do not provide: accessibility | ||
| 14 | + * is proven by touch-based access (one write and one read per page | ||
| 15 | + * without comparing contents) instead of data-pattern compares. | ||
| 16 | + * | ||
| 17 | + * Coverage context: ownership_multiprocess_protection_change covers | ||
| 18 | + * whole-region transitions without asserting the signal; this case adds the | ||
| 19 | + * subrange form (the VMA splits into three VMAs with a ---s middle), the | ||
| 20 | + * boundary-range behavior, and the restore transition. | ||
| 21 | + * | ||
| 22 | + * Signal expectation: the gap-analysis text says SIGBUS (the driver fault | ||
| 23 | + * path returns VM_FAULT_SIGBUS for export regions), but that path is not | ||
| 24 | + * reachable here: the ownership update rewrites the PTEs in place and strips | ||
| 25 | + * VM_READ/VM_WRITE from the VMA, so the arch-level permission check fires | ||
| 26 | + * first. Per the OBMM spec ("access inconsistent with permissions may cause | ||
| 27 | + * bus error") and the export_mmap_prot convention, the child is expected to | ||
| 28 | + * die from SIGSEGV or SIGBUS. | ||
| 29 | + * | ||
| 30 | + * Steps: | ||
| 31 | + * - mmap 6 MB RW on an ALLOW_MMAP export and touch every page (pre-fault) | ||
| 32 | + * - set_ownership [2M, 4M) → PROT_NONE; /proc/pid/maps must show 3 VMAs | ||
| 33 | + * with a ---s middle | ||
| 34 | + * - boundary ranges [0,2M) and [4M,6M) stay writable and readable | ||
| 35 | + * - fork: child writes+reads the NO_ACCESS range; parent waits and asserts | ||
| 36 | + * the child was killed by SIGSEGV or SIGBUS | ||
| 37 | + * - restore: set_ownership [2M,4M) → RW; the middle range is writable and | ||
| 38 | + * readable again. The split VMAs intentionally do not merge back: | ||
| 39 | + * obmm vm_ops carry a .close callback, which makes the VMAs unmergeable | ||
| 40 | + * for the kernel (is_mergeable_vma), so no VMA-count assertion is made | ||
| 41 | + * after the restore | ||
| 42 | + */ | ||
| 43 | + | ||
| 44 | +#include <errno.h> | ||
| 45 | +#include <fcntl.h> | ||
| 46 | +#include <signal.h> | ||
| 47 | +#include <string.h> | ||
| 48 | +#include <sys/mman.h> | ||
| 49 | +#include <sys/wait.h> | ||
| 50 | +#include <unistd.h> | ||
| 51 | + | ||
| 52 | +#include "compat/libobmm-compat.h" | ||
| 53 | + | ||
| 54 | +#include "framework/obmm_test.h" | ||
| 55 | + | ||
| 56 | +#include "utils/export.h" | ||
| 57 | +#include "utils/obmm_dev.h" | ||
| 58 | +#include "utils/os_info.h" | ||
| 59 | +#include "utils/proc_maps.h" | ||
| 60 | +#include "utils/sizes.h" | ||
| 61 | + | ||
| 62 | +#define EXPORT_SIZE (8 * 1024 * 1024) | ||
| 63 | +#define MAP_SIZE SZ_6M | ||
| 64 | +#define SPLIT_LOW SZ_2M | ||
| 65 | +#define SPLIT_HIGH SZ_4M | ||
| 66 | + | ||
| 67 | +static mem_id export_sigbus_region(void) { | ||
| 68 | + size_t lengths[OBMM_MAX_LOCAL_NUMA_NODES]; | ||
| 69 | + struct obmm_mem_desc desc; | ||
| 70 | + mem_id id; | ||
| 71 | + | ||
| 72 | + memset(&desc, 0, sizeof(desc)); | ||
| 73 | + | ||
| 74 | + ASSERT_ERRNO_MSG(fill_obmm_export_length_array( | ||
| 75 | + lengths, | ||
| 76 | + &(struct obmm_alloc_info){ | ||
| 77 | + .policy = OBMM_ALLOC_INTERLEAVE, | ||
| 78 | + .scope = {.unit = OBMM_ALLOC_UNIT_HOST, .unit_id = 0, .total_size = EXPORT_SIZE}, | ||
| 79 | + }) == 0, | ||
| 80 | + "fill_obmm_export_length_array failed"); | ||
| 81 | + ASSERT_ERRNO_MSG(fill_obmm_mem_desc_by_lengths(&desc, lengths) == 0, "fill_obmm_mem_desc_by_lengths failed"); | ||
| 82 | + errno = 0; | ||
| 83 | + id = obmm_export(lengths, OBMM_EXPORT_FLAG_ALLOW_MMAP, &desc); | ||
| 84 | + ASSERT_ERRNO_MSG(id != OBMM_INVALID_MEMID, "obmm_export failed"); | ||
| 85 | + | ||
| 86 | + return id; | ||
| 87 | +} | ||
| 88 | + | ||
| 89 | +/* | ||
| 90 | + * touch every page with one write and one read: proves the range is still | ||
| 91 | + * writable and readable without comparing data contents, so the case stays | ||
| 92 | + * independent of OBMM read/write data consistency | ||
| 93 | + */ | ||
| 94 | +static void touch_range(void *addr, size_t size) { | ||
| 95 | + size_t page_size; | ||
| 96 | + size_t i; | ||
| 97 | + unsigned char value = 0; | ||
| 98 | + | ||
| 99 | + ASSERT_ERRNO_MSG(get_page_size(&page_size) == 0, "touch_range: get_page_size failed"); | ||
| 100 | + for (i = 0; i < size; i += page_size) { | ||
| 101 | + volatile unsigned char *p = (volatile unsigned char *)addr + i; | ||
| 102 | + | ||
| 103 | + *p = (unsigned char)(i / page_size); | ||
| 104 | + value = *p; | ||
| 105 | + } | ||
| 106 | + (void)value; | ||
| 107 | +} | ||
| 108 | + | ||
| 109 | +static int fault_sigbus_on_no_access_test(void) { | ||
| 110 | + mem_id id; | ||
| 111 | + int fd; | ||
| 112 | + void *addr; | ||
| 113 | + int vma_count; | ||
| 114 | + pid_t child; | ||
| 115 | + int wstatus; | ||
| 116 | + int ret; | ||
| 117 | + | ||
| 118 | + id = export_sigbus_region(); | ||
| 119 | + LOG_DEBUG("exported memid=%lu for NO_ACCESS fault test", (unsigned long)id); | ||
| 120 | + | ||
| 121 | + ASSERT_ERRNO_MSG(open_obmm_shmdev(&fd, id, O_RDWR) == 0, "open failed"); | ||
| 122 | + | ||
| 123 | + addr = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); | ||
| 124 | + ASSERT_MSG(addr != MAP_FAILED, "mmap failed: %s", strerror(errno)); | ||
| 125 | + | ||
| 126 | + /* pre-fault the whole mapping so every page exists before the split */ | ||
| 127 | + touch_range(addr, MAP_SIZE); | ||
| 128 | + | ||
| 129 | + /* switch the middle subrange to NO_ACCESS: the VMA must split into | ||
| 130 | + * RW / NO_ACCESS / RW */ | ||
| 131 | + errno = 0; | ||
| 132 | + ret = obmm_set_ownership(fd, (char *)addr + SPLIT_LOW, (char *)addr + SPLIT_HIGH, PROT_NONE); | ||
| 133 | + ASSERT_ERRNO_MSG(ret == 0, "set_ownership [2M,4M)→NO_ACCESS failed"); | ||
| 134 | + | ||
| 135 | + vma_count = count_vmas_for_memid(id); | ||
| 136 | + ASSERT_MSG(vma_count == 3, "expected 3 VMAs after subrange NO_ACCESS, got %d", vma_count); | ||
| 137 | + ASSERT_MSG(check_proc_maps(0, id, NULL, NULL, "---s") == 0, "no ---s VMA found after NO_ACCESS transition"); | ||
| 138 | + | ||
| 139 | + /* boundary ranges must be unaffected by the middle transition */ | ||
| 140 | + touch_range(addr, SPLIT_LOW); | ||
| 141 | + touch_range((char *)addr + SPLIT_HIGH, MAP_SIZE - SPLIT_HIGH); | ||
| 142 | + | ||
| 143 | + /* fork-isolated violation: the child touches the NO_ACCESS range and | ||
| 144 | + * must die from a fatal fault signal */ | ||
| 145 | + child = fork(); | ||
| 146 | + ASSERT_MSG(child >= 0, "fork failed: errno=%d (%s)", errno, strerror(errno)); | ||
| 147 | + | ||
| 148 | + if (child == 0) { | ||
| 149 | + volatile unsigned char *p = (volatile unsigned char *)addr + SPLIT_LOW; | ||
| 150 | + unsigned char value; | ||
| 151 | + | ||
| 152 | + /* write violation: the first touch must already be fatal */ | ||
| 153 | + *p = 0x5A; | ||
| 154 | + value = *p; | ||
| 155 | + (void)value; | ||
| 156 | + _exit(0); /* unreachable while ownership enforcement works */ | ||
| 157 | + } | ||
| 158 | + | ||
| 159 | + errno = 0; | ||
| 160 | + ret = (int)waitpid(child, &wstatus, 0); | ||
| 161 | + ASSERT_MSG(ret == child, "waitpid failed: errno=%d (%s)", errno, strerror(errno)); | ||
| 162 | + | ||
| 163 | + ASSERT_MSG(WIFSIGNALED(wstatus), "expected the child to be killed by a fault signal, but it exited with 0x%x", | ||
| 164 | + (unsigned int)wstatus); | ||
| 165 | + ASSERT_MSG(WTERMSIG(wstatus) == SIGSEGV || WTERMSIG(wstatus) == SIGBUS, "child killed by unexpected signal %d (%s)", | ||
| 166 | + WTERMSIG(wstatus), strsignal(WTERMSIG(wstatus))); | ||
| 167 | + LOG_DEBUG("child killed by signal %d (%s) as expected", WTERMSIG(wstatus), strsignal(WTERMSIG(wstatus))); | ||
| 168 | + | ||
| 169 | + /* restore: switching the range back to RW must make it usable again | ||
| 170 | + * and merge the split VMAs */ | ||
| 171 | + errno = 0; | ||
| 172 | + ret = obmm_set_ownership(fd, (char *)addr + SPLIT_LOW, (char *)addr + SPLIT_HIGH, PROT_WRITE); | ||
| 173 | + ASSERT_ERRNO_MSG(ret == 0, "set_ownership [2M,4M)→RW restore failed"); | ||
| 174 | + | ||
| 175 | + /* the middle range must be writable and readable again after restore */ | ||
| 176 | + touch_range((char *)addr + SPLIT_LOW, SPLIT_HIGH - SPLIT_LOW); | ||
| 177 | + | ||
| 178 | + /* the split VMAs do not merge back: obmm vm_ops carry a .close callback | ||
| 179 | + * (is_mergeable_vma rejects such VMAs), so they stay split by design */ | ||
| 180 | + vma_count = count_vmas_for_memid(id); | ||
| 181 | + LOG_DEBUG("VMA count after restore: %d", vma_count); | ||
| 182 | + | ||
| 183 | + ASSERT_ERRNO_MSG(munmap(addr, MAP_SIZE) == 0, "munmap failed"); | ||
| 184 | + ASSERT_ERRNO_MSG(close(fd) == 0, "close failed"); | ||
| 185 | + | ||
| 186 | + obmm_unexport(id, 0); | ||
| 187 | + return 0; | ||
| 188 | +} | ||
| 189 | + | ||
| 190 | +REGISTER_SINGLE_ACTOR_TEST_FUNC("mmap_and_ownership", "fault_sigbus_on_no_access", fault_sigbus_on_no_access_test, 60); | ||
| @@ -0,0 +1,155 @@ | |||
| 1 | +/* SPDX-License-Identifier: MulanPSL-2.0 */ | ||
| 2 | +/** | ||
| 3 | + * @file fork_child_no_inherit.c | ||
| 4 | + * @brief Validates VM_DONTCOPY: fork children do not inherit obmm_shmdev VMAs. | ||
| 5 | + * | ||
| 6 | + * Single-actor test verifying that obmm_shmdev mappings are marked | ||
| 7 | + * VM_DONTCOPY: after fork(), the child's /proc/self/maps must not contain | ||
| 8 | + * any VMA backed by the parent's obmm_shmdev device, while the parent's | ||
| 9 | + * mapping is unaffected and the child can still establish its own fresh | ||
| 10 | + * mapping of the same device. | ||
| 11 | + * | ||
| 12 | + * Steps: | ||
| 13 | + * - parent: export an ALLOW_MMAP region, open O_RDWR, mmap RW, fill pattern, | ||
| 14 | + * verify the VMA appears in /proc/self/maps (control) | ||
| 15 | + * - fork: child asserts zero inherited VMAs for the mem_id (the VM_DONTCOPY | ||
| 16 | + * core assertion), then opens and mmaps the same device itself and | ||
| 17 | + * verifies the parent-written pattern is readable through its own fresh | ||
| 18 | + * mapping (control: the ban is on inheritance, not on child access), | ||
| 19 | + * and exits 0 on success | ||
| 20 | + * - parent: waitpid, assert exit status 0, verify its own VMA is still | ||
| 21 | + * present after the fork round-trip, then munmap/close/unexport | ||
| 22 | + * | ||
| 23 | + * The child owns no OBMM resource of its own beyond the fresh mapping it | ||
| 24 | + * releases before exiting, so no cross-process cleanup is needed. | ||
| 25 | + * | ||
| 26 | + * No export/data-rw test points involved, single-machine mode is sufficient. | ||
| 27 | + */ | ||
| 28 | + | ||
| 29 | +#include <errno.h> | ||
| 30 | +#include <fcntl.h> | ||
| 31 | +#include <string.h> | ||
| 32 | +#include <sys/mman.h> | ||
| 33 | +#include <sys/wait.h> | ||
| 34 | +#include <unistd.h> | ||
| 35 | + | ||
| 36 | +#include "compat/libobmm-compat.h" | ||
| 37 | + | ||
| 38 | +#include "framework/obmm_test.h" | ||
| 39 | + | ||
| 40 | +#include "utils/data.h" | ||
| 41 | +#include "utils/export.h" | ||
| 42 | +#include "utils/obmm_dev.h" | ||
| 43 | +#include "utils/proc_maps.h" | ||
| 44 | + | ||
| 45 | +#define EXPORT_SIZE (8 * 1024 * 1024) | ||
| 46 | +#define MAP_SIZE (1024 * 1024) | ||
| 47 | +#define DATA_WORD_LEN 8U | ||
| 48 | + | ||
| 49 | +static mem_id export_no_inherit_region(void) { | ||
| 50 | + size_t lengths[OBMM_MAX_LOCAL_NUMA_NODES]; | ||
| 51 | + struct obmm_mem_desc desc; | ||
| 52 | + mem_id id; | ||
| 53 | + | ||
| 54 | + memset(&desc, 0, sizeof(desc)); | ||
| 55 | + | ||
| 56 | + ASSERT_ERRNO_MSG(fill_obmm_export_length_array( | ||
| 57 | + lengths, | ||
| 58 | + &(struct obmm_alloc_info){ | ||
| 59 | + .policy = OBMM_ALLOC_INTERLEAVE, | ||
| 60 | + .scope = {.unit = OBMM_ALLOC_UNIT_HOST, .unit_id = 0, .total_size = EXPORT_SIZE}, | ||
| 61 | + }) == 0, | ||
| 62 | + "fill_obmm_export_length_array failed"); | ||
| 63 | + ASSERT_ERRNO_MSG(fill_obmm_mem_desc_by_lengths(&desc, lengths) == 0, "fill_obmm_mem_desc_by_lengths failed"); | ||
| 64 | + errno = 0; | ||
| 65 | + id = obmm_export(lengths, OBMM_EXPORT_FLAG_ALLOW_MMAP, &desc); | ||
| 66 | + ASSERT_ERRNO_MSG(id != OBMM_INVALID_MEMID, "obmm_export failed"); | ||
| 67 | + | ||
| 68 | + return id; | ||
| 69 | +} | ||
| 70 | + | ||
| 71 | +/* child side: no inherited VMA, but a fresh mapping of the same device works */ | ||
| 72 | +static void run_no_inherit_child(mem_id id) { | ||
| 73 | + int fd; | ||
| 74 | + void *addr; | ||
| 75 | + | ||
| 76 | + /* VM_DONTCOPY core assertion: the parent's VMA must not appear in the | ||
| 77 | + * child's /proc/self/maps */ | ||
| 78 | + if (count_vmas_for_memid(id) != 0) | ||
| 79 | + _exit(1); | ||
| 80 | + | ||
| 81 | + /* control: the child can still establish its own mapping — the driver | ||
| 82 | + * bans inheritance, not child access */ | ||
| 83 | + if (open_obmm_shmdev(&fd, id, O_RDWR) != 0) | ||
| 84 | + _exit(2); | ||
| 85 | + | ||
| 86 | + addr = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); | ||
| 87 | + if (addr == MAP_FAILED) | ||
| 88 | + _exit(3); | ||
| 89 | + | ||
| 90 | + /* the fresh mapping reaches the same physical region: the parent's | ||
| 91 | + * pattern must be visible */ | ||
| 92 | + if (check_data(addr, MAP_SIZE, DATA_WORD_LEN, 0x77) != 0) | ||
| 93 | + _exit(4); | ||
| 94 | + | ||
| 95 | + if (munmap(addr, MAP_SIZE) != 0) | ||
| 96 | + _exit(5); | ||
| 97 | + | ||
| 98 | + if (close(fd) != 0) | ||
| 99 | + _exit(6); | ||
| 100 | + | ||
| 101 | + _exit(0); | ||
| 102 | +} | ||
| 103 | + | ||
| 104 | +static int fork_child_no_inherit_test(void) { | ||
| 105 | + struct obmm_mem_desc desc; | ||
| 106 | + mem_id id; | ||
| 107 | + int fd; | ||
| 108 | + void *addr; | ||
| 109 | + unsigned long start, end; | ||
| 110 | + pid_t child; | ||
| 111 | + int wstatus; | ||
| 112 | + int ret; | ||
| 113 | + | ||
| 114 | + memset(&desc, 0, sizeof(desc)); | ||
| 115 | + | ||
| 116 | + id = export_no_inherit_region(); | ||
| 117 | + LOG_DEBUG("exported memid=%lu for fork no-inherit test", (unsigned long)id); | ||
| 118 | + | ||
| 119 | + ASSERT_ERRNO_MSG(open_obmm_shmdev(&fd, id, O_RDWR) == 0, "parent open failed"); | ||
| 120 | + | ||
| 121 | + addr = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); | ||
| 122 | + ASSERT_MSG(addr != MAP_FAILED, "parent mmap failed: %s", strerror(errno)); | ||
| 123 | + | ||
| 124 | + /* control: the parent's mapping is real and visible in its own maps */ | ||
| 125 | + fill_data(addr, MAP_SIZE, DATA_WORD_LEN, 0x77); | ||
| 126 | + ASSERT_MSG(check_data(addr, MAP_SIZE, DATA_WORD_LEN, 0x77) == 0, "parent pattern verify failed"); | ||
| 127 | + ASSERT_MSG(check_proc_maps(0, id, &start, &end, "rw-s") == 0, "parent VMA not found before fork"); | ||
| 128 | + | ||
| 129 | + child = fork(); | ||
| 130 | + ASSERT_MSG(child >= 0, "fork failed: errno=%d (%s)", errno, strerror(errno)); | ||
| 131 | + | ||
| 132 | + if (child == 0) { | ||
| 133 | + run_no_inherit_child(id); | ||
| 134 | + _exit(99); /* not reached: run_no_inherit_child always exits */ | ||
| 135 | + } | ||
| 136 | + | ||
| 137 | + errno = 0; | ||
| 138 | + ret = (int)waitpid(child, &wstatus, 0); | ||
| 139 | + ASSERT_MSG(ret == child, "waitpid failed: errno=%d (%s)", errno, strerror(errno)); | ||
| 140 | + | ||
| 141 | + ASSERT_MSG(WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == 0, "child exited with status 0x%x (exit code %d)", | ||
| 142 | + (unsigned int)wstatus, WIFEXITED(wstatus) ? WEXITSTATUS(wstatus) : -1); | ||
| 143 | + | ||
| 144 | + /* the fork round-trip must not disturb the parent's own mapping */ | ||
| 145 | + ASSERT_MSG(check_proc_maps(0, id, NULL, NULL, "rw-s") == 0, "parent VMA missing after fork"); | ||
| 146 | + ASSERT_MSG(check_data(addr, MAP_SIZE, DATA_WORD_LEN, 0x77) == 0, "parent data corrupted after fork"); | ||
| 147 | + | ||
| 148 | + ASSERT_ERRNO_MSG(munmap(addr, MAP_SIZE) == 0, "parent munmap failed"); | ||
| 149 | + ASSERT_ERRNO_MSG(close(fd) == 0, "parent close failed"); | ||
| 150 | + | ||
| 151 | + obmm_unexport(id, 0); | ||
| 152 | + return 0; | ||
| 153 | +} | ||
| 154 | + | ||
| 155 | +REGISTER_SINGLE_ACTOR_TEST_FUNC("mmap_and_ownership", "fork_child_no_inherit", fork_child_no_inherit_test, 60); | ||
| @@ -0,0 +1,170 @@ | |||
| 1 | +/* SPDX-License-Identifier: MulanPSL-2.0 */ | ||
| 2 | +/** | ||
| 3 | + * @file mmap_mixed_cache_mode.c | ||
| 4 | + * @brief Validates per-region cacheable/O_SYNC mmap mode exclusivity. | ||
| 5 | + * | ||
| 6 | + * Single-actor test verifying that one obmm_shmdev region cannot be mapped | ||
| 7 | + * cacheable and non-cacheable (O_SYNC) at the same time: the first mmap | ||
| 8 | + * fixes the region-wide mmap mode, and a second mmap of the other mode is | ||
| 9 | + * rejected with -EPERM ("region cannot be mapped to cc and nc at the same | ||
| 10 | + * time"). The mode is released again when the last mapping of the region | ||
| 11 | + * closes; each direction ends with a reset control proving exactly that. | ||
| 12 | + * | ||
| 13 | + * A pool export (obmm_export) carries both OBMM_MEM_ALLOW_CACHEABLE_MMAP | ||
| 14 | + * and OBMM_MEM_ALLOW_NONCACHEABLE_MMAP, so both open modes pass the | ||
| 15 | + * open-time mem_cap check and the exclusivity rule is exercised at mmap | ||
| 16 | + * time. (The open-time mem_cap rejection for single-mode import regions is | ||
| 17 | + * covered separately by obmm_import_api/mmap_o_sync_mem_cap_check.) | ||
| 18 | + * | ||
| 19 | + * Steps (one shared export): | ||
| 20 | + * - direction 1: cacheable mmap succeeds (data control), a second | ||
| 21 | + * O_SYNC open succeeds, but the O_SYNC mmap is rejected with -EPERM; | ||
| 22 | + * after full teardown a fresh O_SYNC mmap succeeds (mode-reset control) | ||
| 23 | + * - direction 2: O_SYNC mmap succeeds (data control), a cacheable mmap is | ||
| 24 | + * rejected with -EPERM; after full teardown a fresh cacheable mmap | ||
| 25 | + * succeeds (mode-reset control) | ||
| 26 | + * | ||
| 27 | + * No export/data-rw test points involved, single-machine mode is sufficient. | ||
| 28 | + */ | ||
| 29 | + | ||
| 30 | +#include <errno.h> | ||
| 31 | +#include <fcntl.h> | ||
| 32 | +#include <string.h> | ||
| 33 | +#include <sys/mman.h> | ||
| 34 | +#include <unistd.h> | ||
| 35 | + | ||
| 36 | +#include "compat/libobmm-compat.h" | ||
| 37 | + | ||
| 38 | +#include "framework/obmm_test.h" | ||
| 39 | + | ||
| 40 | +#include "utils/data.h" | ||
| 41 | +#include "utils/export.h" | ||
| 42 | +#include "utils/obmm_dev.h" | ||
| 43 | + | ||
| 44 | +#define EXPORT_SIZE (8 * 1024 * 1024) | ||
| 45 | +#define MAP_SIZE (1024 * 1024) | ||
| 46 | +#define DATA_WORD_LEN 8U | ||
| 47 | + | ||
| 48 | +static mem_id export_mixed_mode_region(void) { | ||
| 49 | + size_t lengths[OBMM_MAX_LOCAL_NUMA_NODES]; | ||
| 50 | + struct obmm_mem_desc desc; | ||
| 51 | + mem_id id; | ||
| 52 | + | ||
| 53 | + memset(&desc, 0, sizeof(desc)); | ||
| 54 | + | ||
| 55 | + ASSERT_ERRNO_MSG(fill_obmm_export_length_array( | ||
| 56 | + lengths, | ||
| 57 | + &(struct obmm_alloc_info){ | ||
| 58 | + .policy = OBMM_ALLOC_INTERLEAVE, | ||
| 59 | + .scope = {.unit = OBMM_ALLOC_UNIT_HOST, .unit_id = 0, .total_size = EXPORT_SIZE}, | ||
| 60 | + }) == 0, | ||
| 61 | + "fill_obmm_export_length_array failed"); | ||
| 62 | + ASSERT_ERRNO_MSG(fill_obmm_mem_desc_by_lengths(&desc, lengths) == 0, "fill_obmm_mem_desc_by_lengths failed"); | ||
| 63 | + errno = 0; | ||
| 64 | + id = obmm_export(lengths, OBMM_EXPORT_FLAG_ALLOW_MMAP, &desc); | ||
| 65 | + ASSERT_ERRNO_MSG(id != OBMM_INVALID_MEMID, "obmm_export failed"); | ||
| 66 | + | ||
| 67 | + return id; | ||
| 68 | +} | ||
| 69 | + | ||
| 70 | +/* | ||
| 71 | + * direction 1: cacheable mmap first — the region is held in NORMAL mode, | ||
| 72 | + * the O_SYNC open passes (pool export allows both caps) and the O_SYNC | ||
| 73 | + * mmap must fail with EPERM. After full teardown a fresh O_SYNC mmap | ||
| 74 | + * proves the mode was released with the last mapping. | ||
| 75 | + */ | ||
| 76 | +static void run_mixed_mode_cacheable_first(mem_id id) { | ||
| 77 | + int fd_cc; | ||
| 78 | + int fd_nc; | ||
| 79 | + void *addr; | ||
| 80 | + void *sync_addr; | ||
| 81 | + | ||
| 82 | + ASSERT_ERRNO_MSG(open_obmm_shmdev(&fd_cc, id, O_RDWR) == 0, "mixed-mode-cc-first: open failed"); | ||
| 83 | + | ||
| 84 | + /* control: the cacheable mapping works and holds the region in | ||
| 85 | + * OBMM_MMAP_NORMAL mode */ | ||
| 86 | + addr = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd_cc, 0); | ||
| 87 | + ASSERT_MSG(addr != MAP_FAILED, "mixed-mode-cc-first: cacheable mmap failed: %s", strerror(errno)); | ||
| 88 | + fill_data(addr, MAP_SIZE, DATA_WORD_LEN, 0x2A); | ||
| 89 | + ASSERT_MSG(check_data(addr, MAP_SIZE, DATA_WORD_LEN, 0x2A) == 0, "mixed-mode-cc-first: pattern verify failed"); | ||
| 90 | + | ||
| 91 | + ASSERT_ERRNO_MSG(open_obmm_shmdev(&fd_nc, id, O_RDWR | O_SYNC) == 0, "mixed-mode-cc-first: O_SYNC open failed"); | ||
| 92 | + | ||
| 93 | + errno = 0; | ||
| 94 | + sync_addr = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd_nc, 0); | ||
| 95 | + ASSERT_MSG(sync_addr == MAP_FAILED, | ||
| 96 | + "mixed-mode-cc-first: O_SYNC mmap on a cacheable-mapped region unexpectedly succeeded"); | ||
| 97 | + ASSERT_MSG(errno == EPERM, "mixed-mode-cc-first: expected EPERM, got errno=%d (%s)", errno, strerror(errno)); | ||
| 98 | + | ||
| 99 | + /* release every mapping so the mode resets with the last VMA close */ | ||
| 100 | + ASSERT_ERRNO_MSG(munmap(addr, MAP_SIZE) == 0, "mixed-mode-cc-first: cacheable munmap failed"); | ||
| 101 | + ASSERT_ERRNO_MSG(close(fd_cc) == 0, "mixed-mode-cc-first: cacheable close failed"); | ||
| 102 | + ASSERT_ERRNO_MSG(close(fd_nc) == 0, "mixed-mode-cc-first: O_SYNC close failed"); | ||
| 103 | + | ||
| 104 | + /* reset control: with no mapping left, the O_SYNC mmap succeeds */ | ||
| 105 | + ASSERT_ERRNO_MSG(open_obmm_shmdev(&fd_nc, id, O_RDWR | O_SYNC) == 0, "mixed-mode-cc-first: reset open failed"); | ||
| 106 | + sync_addr = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd_nc, 0); | ||
| 107 | + ASSERT_MSG(sync_addr != MAP_FAILED, "mixed-mode-cc-first: reset O_SYNC mmap failed: %s", strerror(errno)); | ||
| 108 | + ASSERT_ERRNO_MSG(munmap(sync_addr, MAP_SIZE) == 0, "mixed-mode-cc-first: reset munmap failed"); | ||
| 109 | + ASSERT_ERRNO_MSG(close(fd_nc) == 0, "mixed-mode-cc-first: reset close failed"); | ||
| 110 | + | ||
| 111 | + LOG_DEBUG("mixed-mode-cc-first: O_SYNC mmap rejected with EPERM, mode reset verified"); | ||
| 112 | +} | ||
| 113 | + | ||
| 114 | +/* | ||
| 115 | + * direction 2: O_SYNC mmap first — the region is held in OSYNC mode and a | ||
| 116 | + * cacheable mmap must fail with EPERM. After full teardown a fresh | ||
| 117 | + * cacheable mmap proves the mode was released with the last mapping. | ||
| 118 | + */ | ||
| 119 | +static void run_mixed_mode_osync_first(mem_id id) { | ||
| 120 | + int fd_nc; | ||
| 121 | + int fd_cc; | ||
| 122 | + void *addr; | ||
| 123 | + void *sync_addr; | ||
| 124 | + | ||
| 125 | + ASSERT_ERRNO_MSG(open_obmm_shmdev(&fd_nc, id, O_RDWR | O_SYNC) == 0, "mixed-mode-nc-first: open failed"); | ||
| 126 | + | ||
| 127 | + /* control: the O_SYNC mapping works and holds the region in | ||
| 128 | + * OBMM_MMAP_OSYNC mode */ | ||
| 129 | + sync_addr = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd_nc, 0); | ||
| 130 | + ASSERT_MSG(sync_addr != MAP_FAILED, "mixed-mode-nc-first: O_SYNC mmap failed: %s", strerror(errno)); | ||
| 131 | + fill_data(sync_addr, MAP_SIZE, DATA_WORD_LEN, 0x5B); | ||
| 132 | + ASSERT_MSG(check_data(sync_addr, MAP_SIZE, DATA_WORD_LEN, 0x5B) == 0, "mixed-mode-nc-first: pattern verify failed"); | ||
| 133 | + | ||
| 134 | + ASSERT_ERRNO_MSG(open_obmm_shmdev(&fd_cc, id, O_RDWR) == 0, "mixed-mode-nc-first: cacheable open failed"); | ||
| 135 | + | ||
| 136 | + errno = 0; | ||
| 137 | + addr = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd_cc, 0); | ||
| 138 | + ASSERT_MSG(addr == MAP_FAILED, | ||
| 139 | + "mixed-mode-nc-first: cacheable mmap on an O_SYNC-mapped region unexpectedly succeeded"); | ||
| 140 | + ASSERT_MSG(errno == EPERM, "mixed-mode-nc-first: expected EPERM, got errno=%d (%s)", errno, strerror(errno)); | ||
| 141 | + | ||
| 142 | + /* release every mapping so the mode resets with the last VMA close */ | ||
| 143 | + ASSERT_ERRNO_MSG(munmap(sync_addr, MAP_SIZE) == 0, "mixed-mode-nc-first: O_SYNC munmap failed"); | ||
| 144 | + ASSERT_ERRNO_MSG(close(fd_nc) == 0, "mixed-mode-nc-first: O_SYNC close failed"); | ||
| 145 | + ASSERT_ERRNO_MSG(close(fd_cc) == 0, "mixed-mode-nc-first: cacheable close failed"); | ||
| 146 | + | ||
| 147 | + /* reset control: with no mapping left, the cacheable mmap succeeds */ | ||
| 148 | + ASSERT_ERRNO_MSG(open_obmm_shmdev(&fd_cc, id, O_RDWR) == 0, "mixed-mode-nc-first: reset open failed"); | ||
| 149 | + addr = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd_cc, 0); | ||
| 150 | + ASSERT_MSG(addr != MAP_FAILED, "mixed-mode-nc-first: reset cacheable mmap failed: %s", strerror(errno)); | ||
| 151 | + ASSERT_ERRNO_MSG(munmap(addr, MAP_SIZE) == 0, "mixed-mode-nc-first: reset munmap failed"); | ||
| 152 | + ASSERT_ERRNO_MSG(close(fd_cc) == 0, "mixed-mode-nc-first: reset close failed"); | ||
| 153 | + | ||
| 154 | + LOG_DEBUG("mixed-mode-nc-first: cacheable mmap rejected with EPERM, mode reset verified"); | ||
| 155 | +} | ||
| 156 | + | ||
| 157 | +static int mmap_mixed_cache_mode_test(void) { | ||
| 158 | + mem_id id; | ||
| 159 | + | ||
| 160 | + id = export_mixed_mode_region(); | ||
| 161 | + LOG_DEBUG("exported memid=%lu for cache-mode mix tests", (unsigned long)id); | ||
| 162 | + | ||
| 163 | + run_mixed_mode_cacheable_first(id); | ||
| 164 | + run_mixed_mode_osync_first(id); | ||
| 165 | + | ||
| 166 | + obmm_unexport(id, 0); | ||
| 167 | + return 0; | ||
| 168 | +} | ||
| 169 | + | ||
| 170 | +REGISTER_SINGLE_ACTOR_TEST_FUNC("mmap_and_ownership", "mmap_mixed_cache_mode", mmap_mixed_cache_mode_test, 60); | ||
| @@ -13,6 +13,16 @@ | |||
| 13 | * data preservation in RO half, writable second half | 13 | * data preservation in RO half, writable second half |
| 14 | * - pmd-alignment: alignment validation — misaligned size, misaligned offset, | 14 | * - pmd-alignment: alignment validation — misaligned size, misaligned offset, |
| 15 | * properly aligned offset+size | 15 | * properly aligned offset+size |
| 16 | + * - pmd-flag-smaps: positive OBMM_MMAP_FLAG_HUGETLB_PMD path — mmap with the | ||
| 17 | + * flag (offset 0 and PMD-aligned non-zero offset) must succeed and report | ||
| 18 | + * PMD KernelPageSize in smaps with rd/wr VmFlags | ||
| 19 | + * - pmd-granu-mix: mixing PAGE and PMD granularity on one region is rejected | ||
| 20 | + * with -EPERM, in both directions (each needs its own region because the | ||
| 21 | + * granularity is sticky) | ||
| 22 | + * - pmd-split-alignment: vma may_split alignment validation — a PMD-granu | ||
| 23 | + * mapping rejects munmap at a page-aligned but PMD-unaligned interior | ||
| 24 | + * address with -EINVAL (mapping stays intact), while PMD-aligned interior | ||
| 25 | + * splits succeed; a PAGE-granu control proves normal page-aligned splits | ||
| 16 | * | 26 | * |
| 17 | * All obmm_set_ownership calls use CACHE_INFER internally. | 27 | * All obmm_set_ownership calls use CACHE_INFER internally. |
| 18 | * | 28 | * |
| @@ -21,6 +31,11 @@ | |||
| 21 | * cleans up independently. No export/data-rw test points involved, | 31 | * cleans up independently. No export/data-rw test points involved, |
| 22 | * single-machine mode is sufficient. | 32 | * single-machine mode is sufficient. |
| 23 | * | 33 | * |
| 34 | + * Region mmap granularity is sticky (first mmap decides, mixing granularities | ||
| 35 | + * is rejected with -EPERM), and the shared export is left in PAGE granularity | ||
| 36 | + * by the plain mmaps above. The HUGETLB_PMD flag step therefore uses its own | ||
| 37 | + * dedicated export (2 * pmd_size, at least 8 MB). | ||
| 38 | + * | ||
| 24 | * On 4K pages: pmd_size=2M, export=8M. On 64K pages: pmd_size=512M, export=2G. | 39 | * On 4K pages: pmd_size=2M, export=8M. On 64K pages: pmd_size=512M, export=2G. |
| 25 | */ | 40 | */ |
| 26 | 41 | ||
| @@ -163,6 +178,280 @@ static int verify_pmd_alignment(mem_id id) { | |||
| 163 | return 0; | 178 | return 0; |
| 164 | } | 179 | } |
| 165 | 180 | ||
| 181 | +/* | ||
| 182 | + * pmd-flag-smaps: positive OBMM_MMAP_FLAG_HUGETLB_PMD path — mmap with the | ||
| 183 | + * flag must succeed for offset 0 and a PMD-aligned non-zero offset, support | ||
| 184 | + * data R/W, and report PMD KernelPageSize in smaps with rd/wr VmFlags. | ||
| 185 | + * | ||
| 186 | + * Uses a dedicated export: region mmap granularity is sticky, and the shared | ||
| 187 | + * export above is already in PAGE granularity, so a flag mmap there would be | ||
| 188 | + * rejected as a granularity mix (-EPERM). | ||
| 189 | + */ | ||
| 190 | +static int verify_pmd_flag_smaps(void) { | ||
| 191 | + size_t lengths[OBMM_MAX_LOCAL_NUMA_NODES]; | ||
| 192 | + struct obmm_mem_desc desc; | ||
| 193 | + size_t pmd_size; | ||
| 194 | + size_t export_size; | ||
| 195 | + unsigned long offsets[2]; | ||
| 196 | + unsigned long kernel_page_size; | ||
| 197 | + mem_id id; | ||
| 198 | + int fd; | ||
| 199 | + void *addr; | ||
| 200 | + char vmflags[256]; | ||
| 201 | + unsigned int i; | ||
| 202 | + | ||
| 203 | + ASSERT_ERRNO_MSG(get_pmd_hugepage_size(&pmd_size) == 0, "pmd-flag-smaps: get_pmd_hugepage_size failed"); | ||
| 204 | + ASSERT_MSG(pmd_size > 0, "pmd-flag-smaps: PMD hugepage size is 0"); | ||
| 205 | + | ||
| 206 | + export_size = pmd_size * 2; | ||
| 207 | + if (export_size < MIN_EXPORT_SIZE) | ||
| 208 | + export_size = MIN_EXPORT_SIZE; | ||
| 209 | + | ||
| 210 | + memset(&desc, 0, sizeof(desc)); | ||
| 211 | + ASSERT_ERRNO_MSG(fill_obmm_export_length_array( | ||
| 212 | + lengths, | ||
| 213 | + &(struct obmm_alloc_info){ | ||
| 214 | + .policy = OBMM_ALLOC_INTERLEAVE, | ||
| 215 | + .scope = {.unit = OBMM_ALLOC_UNIT_HOST, .unit_id = 0, .total_size = export_size}, | ||
| 216 | + }) == 0, | ||
| 217 | + "pmd-flag-smaps: fill_obmm_export_length_array failed"); | ||
| 218 | + ASSERT_ERRNO_MSG(fill_obmm_mem_desc_by_lengths(&desc, lengths) == 0, | ||
| 219 | + "pmd-flag-smaps: fill_obmm_mem_desc_by_lengths failed"); | ||
| 220 | + errno = 0; | ||
| 221 | + id = obmm_export(lengths, OBMM_EXPORT_FLAG_ALLOW_MMAP, &desc); | ||
| 222 | + ASSERT_ERRNO_MSG(id != OBMM_INVALID_MEMID, "pmd-flag-smaps: obmm_export failed"); | ||
| 223 | + | ||
| 224 | + offsets[0] = 0; | ||
| 225 | + offsets[1] = pmd_size; | ||
| 226 | + for (i = 0; i < 2; ++i) { | ||
| 227 | + ASSERT_ERRNO_MSG(open_obmm_shmdev(&fd, id, O_RDWR) == 0, "pmd-flag-smaps: open failed"); | ||
| 228 | + | ||
| 229 | + /* flag mmap requires PMD-aligned vma start/end/offset; success itself | ||
| 230 | + * proves the kernel accepted the alignment of the chosen mapping */ | ||
| 231 | + addr = mmap(NULL, pmd_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, offsets[i] | OBMM_MMAP_FLAG_HUGETLB_PMD); | ||
| 232 | + ASSERT_MSG(addr != MAP_FAILED, "pmd-flag-smaps: mmap with HUGETLB_PMD flag (offset=%#lx) failed: %s", | ||
| 233 | + offsets[i], strerror(errno)); | ||
| 234 | + | ||
| 235 | + fill_data(addr, pmd_size, DATA_WORD_LEN, 0x17); | ||
| 236 | + ASSERT_MSG(check_data(addr, pmd_size, DATA_WORD_LEN, 0x17) == 0, | ||
| 237 | + "pmd-flag-smaps: pattern verify failed (offset=%#lx)", offsets[i]); | ||
| 238 | + | ||
| 239 | + /* the flag must select PMD granularity: KernelPageSize must be exactly | ||
| 240 | + * the PMD hugepage size reported by the driver's obmm_pagesize() */ | ||
| 241 | + ASSERT_ERRNO_MSG(check_proc_smaps(0, id, &kernel_page_size) == 0, | ||
| 242 | + "pmd-flag-smaps: failed to get KernelPageSize"); | ||
| 243 | + ASSERT_MSG(kernel_page_size == pmd_size, | ||
| 244 | + "pmd-flag-smaps: KernelPageSize %#lx != PMD size %#lx " | ||
| 245 | + "(HUGETLB_PMD flag did not select PMD mapping, offset=%#lx)", | ||
| 246 | + kernel_page_size, pmd_size, offsets[i]); | ||
| 247 | + | ||
| 248 | + ASSERT_ERRNO_MSG(get_proc_smaps_vmflags(0, id, vmflags, sizeof(vmflags)) == 0, | ||
| 249 | + "pmd-flag-smaps: failed to get VmFlags"); | ||
| 250 | + ASSERT_MSG(strstr(vmflags, "rd") != NULL && strstr(vmflags, "wr") != NULL, | ||
| 251 | + "pmd-flag-smaps: VmFlags missing 'rd'/'wr' for RW PMD mapping: %s", vmflags); | ||
| 252 | + | ||
| 253 | + LOG_DEBUG("pmd-flag-smaps: flag mmap offset=%#lx OK, KernelPageSize=%#lx", offsets[i], kernel_page_size); | ||
| 254 | + | ||
| 255 | + munmap(addr, pmd_size); | ||
| 256 | + close(fd); | ||
| 257 | + } | ||
| 258 | + | ||
| 259 | + obmm_unexport(id, 0); | ||
| 260 | + return 0; | ||
| 261 | +} | ||
| 262 | + | ||
| 263 | +/* | ||
| 264 | + * pmd-granu-mix: mixing PAGE and PMD granularities on one region is | ||
| 265 | + * rejected with -EPERM. Region mmap granularity is sticky — the first | ||
| 266 | + * mmap decides it and it cannot be reset — so each direction needs its | ||
| 267 | + * own region. | ||
| 268 | + */ | ||
| 269 | +static mem_id export_pmd_mix_region(size_t pmd_size) { | ||
| 270 | + size_t lengths[OBMM_MAX_LOCAL_NUMA_NODES]; | ||
| 271 | + struct obmm_mem_desc desc; | ||
| 272 | + size_t export_size; | ||
| 273 | + mem_id id; | ||
| 274 | + | ||
| 275 | + export_size = pmd_size * 2; | ||
| 276 | + if (export_size < MIN_EXPORT_SIZE) | ||
| 277 | + export_size = MIN_EXPORT_SIZE; | ||
| 278 | + | ||
| 279 | + memset(&desc, 0, sizeof(desc)); | ||
| 280 | + ASSERT_ERRNO_MSG(fill_obmm_export_length_array( | ||
| 281 | + lengths, | ||
| 282 | + &(struct obmm_alloc_info){ | ||
| 283 | + .policy = OBMM_ALLOC_INTERLEAVE, | ||
| 284 | + .scope = {.unit = OBMM_ALLOC_UNIT_HOST, .unit_id = 0, .total_size = export_size}, | ||
| 285 | + }) == 0, | ||
| 286 | + "pmd-granu-mix: fill_obmm_export_length_array failed"); | ||
| 287 | + ASSERT_ERRNO_MSG(fill_obmm_mem_desc_by_lengths(&desc, lengths) == 0, | ||
| 288 | + "pmd-granu-mix: fill_obmm_mem_desc_by_lengths failed"); | ||
| 289 | + errno = 0; | ||
| 290 | + id = obmm_export(lengths, OBMM_EXPORT_FLAG_ALLOW_MMAP, &desc); | ||
| 291 | + ASSERT_ERRNO_MSG(id != OBMM_INVALID_MEMID, "pmd-granu-mix: obmm_export failed"); | ||
| 292 | + return id; | ||
| 293 | +} | ||
| 294 | + | ||
| 295 | +/* direction 1: PAGE granularity first, then a fully aligned flag mmap | ||
| 296 | + * passes the alignment checks and must fail on the granularity mix */ | ||
| 297 | +static void run_pmd_granu_mix_page_first(size_t pmd_size) { | ||
| 298 | + mem_id id = export_pmd_mix_region(pmd_size); | ||
| 299 | + int fd; | ||
| 300 | + void *addr; | ||
| 301 | + void *flag_addr; | ||
| 302 | + void *reserve; | ||
| 303 | + uintptr_t hint; | ||
| 304 | + | ||
| 305 | + ASSERT_ERRNO_MSG(open_obmm_shmdev(&fd, id, O_RDWR) == 0, "pmd-granu-mix: page-first open failed"); | ||
| 306 | + | ||
| 307 | + /* plain mmap puts the region in PAGE granularity */ | ||
| 308 | + addr = mmap(NULL, 1024 * 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); | ||
| 309 | + ASSERT_MSG(addr != MAP_FAILED, "pmd-granu-mix: page-first plain mmap failed: %s", strerror(errno)); | ||
| 310 | + | ||
| 311 | + /* reserve a PMD-aligned hole, release it, and reuse the aligned | ||
| 312 | + * address as the hint: the flag mmap then passes the alignment | ||
| 313 | + * checks and reaches the granularity check deterministically */ | ||
| 314 | + reserve = mmap(NULL, 2 * pmd_size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); | ||
| 315 | + ASSERT_MSG(reserve != MAP_FAILED, "pmd-granu-mix: alignment reservation failed: %s", strerror(errno)); | ||
| 316 | + hint = ((uintptr_t)reserve + pmd_size - 1) & ~((uintptr_t)pmd_size - 1); | ||
| 317 | + ASSERT_MSG(munmap(reserve, 2 * pmd_size) == 0, "pmd-granu-mix: reservation release failed"); | ||
| 318 | + | ||
| 319 | + errno = 0; | ||
| 320 | + flag_addr = mmap((void *)hint, pmd_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, OBMM_MMAP_FLAG_HUGETLB_PMD); | ||
| 321 | + ASSERT_MSG(flag_addr == MAP_FAILED, "pmd-granu-mix: page-first flag mmap unexpectedly succeeded"); | ||
| 322 | + ASSERT_MSG(errno == EPERM, "pmd-granu-mix: page-first flag mmap expected EPERM, got errno=%d (%s)", errno, | ||
| 323 | + strerror(errno)); | ||
| 324 | + | ||
| 325 | + ASSERT_ERRNO_MSG(munmap(addr, 1024 * 1024) == 0, "pmd-granu-mix: page-first plain munmap failed"); | ||
| 326 | + close(fd); | ||
| 327 | + obmm_unexport(id, 0); | ||
| 328 | +} | ||
| 329 | + | ||
| 330 | +/* direction 2: PMD granularity first, then a plain mmap must fail on | ||
| 331 | + * the granularity mix */ | ||
| 332 | +static void run_pmd_granu_mix_pmd_first(size_t pmd_size) { | ||
| 333 | + mem_id id = export_pmd_mix_region(pmd_size); | ||
| 334 | + int fd; | ||
| 335 | + void *addr; | ||
| 336 | + void *addr2; | ||
| 337 | + | ||
| 338 | + ASSERT_ERRNO_MSG(open_obmm_shmdev(&fd, id, O_RDWR) == 0, "pmd-granu-mix: pmd-first open failed"); | ||
| 339 | + | ||
| 340 | + /* flag mmap puts the region in PMD granularity */ | ||
| 341 | + addr = mmap(NULL, pmd_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, OBMM_MMAP_FLAG_HUGETLB_PMD); | ||
| 342 | + ASSERT_MSG(addr != MAP_FAILED, "pmd-granu-mix: pmd-first flag mmap failed: %s", strerror(errno)); | ||
| 343 | + | ||
| 344 | + /* sanity: the PMD mapping is real memory */ | ||
| 345 | + fill_data(addr, 1024 * 1024, DATA_WORD_LEN, 0x91); | ||
| 346 | + ASSERT_MSG(check_data(addr, 1024 * 1024, DATA_WORD_LEN, 0x91) == 0, | ||
| 347 | + "pmd-granu-mix: PMD mapping pattern verify failed"); | ||
| 348 | + | ||
| 349 | + errno = 0; | ||
| 350 | + addr2 = mmap(NULL, 1024 * 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); | ||
| 351 | + ASSERT_MSG(addr2 == MAP_FAILED, "pmd-granu-mix: pmd-first plain mmap unexpectedly succeeded"); | ||
| 352 | + ASSERT_MSG(errno == EPERM, "pmd-granu-mix: pmd-first plain mmap expected EPERM, got errno=%d (%s)", errno, | ||
| 353 | + strerror(errno)); | ||
| 354 | + | ||
| 355 | + munmap(addr, pmd_size); | ||
| 356 | + close(fd); | ||
| 357 | + obmm_unexport(id, 0); | ||
| 358 | +} | ||
| 359 | + | ||
| 360 | +static int verify_pmd_granu_mix(void) { | ||
| 361 | + size_t pmd_size; | ||
| 362 | + | ||
| 363 | + ASSERT_ERRNO_MSG(get_pmd_hugepage_size(&pmd_size) == 0, "pmd-granu-mix: get_pmd_hugepage_size failed"); | ||
| 364 | + ASSERT_MSG(pmd_size > 0, "pmd-granu-mix: PMD hugepage size is 0"); | ||
| 365 | + | ||
| 366 | + run_pmd_granu_mix_page_first(pmd_size); | ||
| 367 | + run_pmd_granu_mix_pmd_first(pmd_size); | ||
| 368 | + | ||
| 369 | + LOG_DEBUG("pmd-granu-mix: granularity mix rejected in both directions"); | ||
| 370 | + return 0; | ||
| 371 | +} | ||
| 372 | + | ||
| 373 | +/* | ||
| 374 | + * pmd-split-alignment: vma may_split alignment validation via munmap. | ||
| 375 | + * | ||
| 376 | + * The generic mm rejects sub-page munmap addresses before the driver hook, | ||
| 377 | + * so the driver's alignment check is only reachable on PMD-granularity | ||
| 378 | + * mappings: a page-aligned but PMD-unaligned interior munmap address must | ||
| 379 | + * fail with -EINVAL and leave the mapping intact, while a PMD-aligned | ||
| 380 | + * interior split is allowed. A PAGE-granularity control proves normal | ||
| 381 | + * page-aligned interior splits work. | ||
| 382 | + */ | ||
| 383 | +static int verify_pmd_split_alignment(mem_id id) { | ||
| 384 | + size_t pmd_size; | ||
| 385 | + size_t page_size; | ||
| 386 | + | ||
| 387 | + ASSERT_ERRNO_MSG(get_pmd_hugepage_size(&pmd_size) == 0, "pmd-split-alignment: get_pmd_hugepage_size failed"); | ||
| 388 | + ASSERT_MSG(pmd_size > 0, "pmd-split-alignment: PMD hugepage size is 0"); | ||
| 389 | + ASSERT_ERRNO_MSG(get_page_size(&page_size) == 0, "pmd-split-alignment: get_page_size failed"); | ||
| 390 | + | ||
| 391 | + /* PAGE-granu control: a page-aligned interior munmap splits the VMA */ | ||
| 392 | + { | ||
| 393 | + int fd; | ||
| 394 | + void *addr; | ||
| 395 | + int vma_count_before; | ||
| 396 | + int vma_count_after; | ||
| 397 | + | ||
| 398 | + ASSERT_ERRNO_MSG(open_obmm_shmdev(&fd, id, O_RDWR) == 0, "pmd-split-alignment: page-granu open failed"); | ||
| 399 | + | ||
| 400 | + addr = mmap(NULL, 6 * 1024 * 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); | ||
| 401 | + ASSERT_MSG(addr != MAP_FAILED, "pmd-split-alignment: page-granu mmap failed: %s", strerror(errno)); | ||
| 402 | + | ||
| 403 | + vma_count_before = count_vmas_for_memid(id); | ||
| 404 | + ASSERT_ERRNO_MSG(munmap((char *)addr + 1024 * 1024, 2 * 1024 * 1024) == 0, | ||
| 405 | + "pmd-split-alignment: page-granu interior munmap failed"); | ||
| 406 | + vma_count_after = count_vmas_for_memid(id); | ||
| 407 | + ASSERT_MSG(vma_count_after == vma_count_before + 1, | ||
| 408 | + "pmd-split-alignment: page-granu interior split did not create a new VMA (%d -> %d)", | ||
| 409 | + vma_count_before, vma_count_after); | ||
| 410 | + | ||
| 411 | + ASSERT_ERRNO_MSG(munmap(addr, 6 * 1024 * 1024) == 0, "pmd-split-alignment: page-granu cleanup munmap failed"); | ||
| 412 | + ASSERT_ERRNO_MSG(close(fd) == 0, "pmd-split-alignment: page-granu close failed"); | ||
| 413 | + } | ||
| 414 | + | ||
| 415 | + /* PMD-granu negative + control on a dedicated export: the region | ||
| 416 | + * granularity is sticky and the shared export above is in PAGE mode */ | ||
| 417 | + { | ||
| 418 | + mem_id pmd_id = export_pmd_mix_region(pmd_size); | ||
| 419 | + int fd; | ||
| 420 | + void *addr; | ||
| 421 | + int ret; | ||
| 422 | + | ||
| 423 | + ASSERT_ERRNO_MSG(open_obmm_shmdev(&fd, pmd_id, O_RDWR) == 0, "pmd-split-alignment: pmd-granu open failed"); | ||
| 424 | + | ||
| 425 | + addr = mmap(NULL, 2 * pmd_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, OBMM_MMAP_FLAG_HUGETLB_PMD); | ||
| 426 | + ASSERT_MSG(addr != MAP_FAILED, "pmd-split-alignment: pmd-granu flag mmap failed: %s", strerror(errno)); | ||
| 427 | + | ||
| 428 | + fill_data(addr, 1024 * 1024, DATA_WORD_LEN, 0x3C); | ||
| 429 | + ASSERT_MSG(check_data(addr, 1024 * 1024, DATA_WORD_LEN, 0x3C) == 0, | ||
| 430 | + "pmd-split-alignment: pmd-granu pattern verify failed"); | ||
| 431 | + | ||
| 432 | + /* negative: page-aligned but PMD-unaligned interior split address */ | ||
| 433 | + errno = 0; | ||
| 434 | + ret = munmap((char *)addr + page_size, pmd_size); | ||
| 435 | + ASSERT_MSG(ret == -1, "pmd-split-alignment: PMD-unaligned interior munmap unexpectedly succeeded"); | ||
| 436 | + ASSERT_MSG(errno == EINVAL, "pmd-split-alignment: expected EINVAL, got errno=%d (%s)", errno, strerror(errno)); | ||
| 437 | + | ||
| 438 | + /* the rejected split must not have torn the mapping down */ | ||
| 439 | + ASSERT_MSG(check_data(addr, 1024 * 1024, DATA_WORD_LEN, 0x3C) == 0, | ||
| 440 | + "pmd-split-alignment: mapping damaged after rejected split"); | ||
| 441 | + | ||
| 442 | + /* positive control: a PMD-aligned interior split is allowed */ | ||
| 443 | + ASSERT_ERRNO_MSG(munmap((char *)addr + pmd_size, pmd_size) == 0, | ||
| 444 | + "pmd-split-alignment: PMD-aligned interior munmap failed"); | ||
| 445 | + ASSERT_ERRNO_MSG(munmap(addr, pmd_size) == 0, "pmd-split-alignment: pmd-granu cleanup munmap failed"); | ||
| 446 | + ASSERT_ERRNO_MSG(close(fd) == 0, "pmd-split-alignment: pmd-granu close failed"); | ||
| 447 | + | ||
| 448 | + obmm_unexport(pmd_id, 0); | ||
| 449 | + } | ||
| 450 | + | ||
| 451 | + LOG_DEBUG("pmd-split-alignment: PMD-unaligned split rejected, aligned splits verified"); | ||
| 452 | + return 0; | ||
| 453 | +} | ||
| 454 | + | ||
| 166 | static int pmd_mmap_test(void) { | 455 | static int pmd_mmap_test(void) { |
| 167 | size_t lengths[OBMM_MAX_LOCAL_NUMA_NODES]; | 456 | size_t lengths[OBMM_MAX_LOCAL_NUMA_NODES]; |
| 168 | struct obmm_mem_desc desc; | 457 | struct obmm_mem_desc desc; |
| @@ -200,6 +489,9 @@ static int pmd_mmap_test(void) { | |||
| 200 | ASSERT_MSG(verify_pmd_basic_mmap(id) == 0, "pmd-basic failed"); | 489 | ASSERT_MSG(verify_pmd_basic_mmap(id) == 0, "pmd-basic failed"); |
| 201 | ASSERT_MSG(verify_pmd_ownership(id) == 0, "pmd-ownership failed"); | 490 | ASSERT_MSG(verify_pmd_ownership(id) == 0, "pmd-ownership failed"); |
| 202 | ASSERT_MSG(verify_pmd_alignment(id) == 0, "pmd-alignment failed"); | 491 | ASSERT_MSG(verify_pmd_alignment(id) == 0, "pmd-alignment failed"); |
| 492 | + ASSERT_MSG(verify_pmd_flag_smaps() == 0, "pmd-flag-smaps failed"); | ||
| 493 | + ASSERT_MSG(verify_pmd_granu_mix() == 0, "pmd-granu-mix failed"); | ||
| 494 | + ASSERT_MSG(verify_pmd_split_alignment(id) == 0, "pmd-split-alignment failed"); | ||
| 203 | 495 | ||
| 204 | obmm_unexport(id, 0); | 496 | obmm_unexport(id, 0); |
| 205 | return 0; | 497 | return 0; |
| @@ -0,0 +1,168 @@ | |||
| 1 | +/* SPDX-License-Identifier: MulanPSL-2.0 */ | ||
| 2 | +/** | ||
| 3 | + * @file update_range_invalid_mem_state_matrix.c | ||
| 4 | + * @brief Validates the UPDATE_RANGE mem_state rejection matrix. | ||
| 5 | + * | ||
| 6 | + * Single-actor test verifying that the OBMM shmdev UPDATE_RANGE ioctl | ||
| 7 | + * rejects invalid mem_state values with -EINVAL, per the kernel's | ||
| 8 | + * validate_state() rules: | ||
| 9 | + * | ||
| 10 | + * | Rule | mem_state | Reason | | ||
| 11 | + * |------|------------------------------------------|-------------------------------| | ||
| 12 | + * | 1 | NORMAL\|RW \| 0x10, NORMAL\|RW \| 0x80 | bits outside the cache+access | | ||
| 13 | + * | | | masks are unknown | | ||
| 14 | + * | 2 | CACHE_RESV + {RW, READONLY, NO_ACCESS} | reserved cacheability value | | ||
| 15 | + * | | (RESV|READONLY is the all-zero state) | | | ||
| 16 | + * | 3 | NORMAL_NC\|READEXEC, DEVICE\|READEXEC | NC/device memory cannot be | | ||
| 17 | + * | | | executable | | ||
| 18 | + * | 4 | NORMAL_NC\|READONLY, NORMAL_NC\|READWRITE| NC state with access other | | ||
| 19 | + * | | | than NO_ACCESS | | ||
| 20 | + * | ||
| 21 | + * cache_ops is a valid OBMM_SHM_CACHE_INFER in every case so that | ||
| 22 | + * mem_state is the only invalid input (the kernel checks validate_state | ||
| 23 | + * before the cache_ops validation). | ||
| 24 | + * | ||
| 25 | + * Before the matrix the mapping is proven writable with a data pattern. | ||
| 26 | + * After the matrix the test verifies the state was not corrupted by the | ||
| 27 | + * failed updates: data is still writable, and one valid INFER RW->RO->RW | ||
| 28 | + * round-trip succeeds with data preserved. | ||
| 29 | + * | ||
| 30 | + * The test exports one shmdev with ALLOW_MMAP (8 MB) and mmaps 2 MB RW. | ||
| 31 | + * No export/data-rw test points involved, single-machine mode is | ||
| 32 | + * sufficient. | ||
| 33 | + * | ||
| 34 | + * Preconditions: | ||
| 35 | + * - OBMM driver loaded with obmm_shmdev visible | ||
| 36 | + * - At least one NUMA node with available memory for export (>= 8 MB) | ||
| 37 | + */ | ||
| 38 | + | ||
| 39 | +#include <errno.h> | ||
| 40 | +#include <fcntl.h> | ||
| 41 | +#include <string.h> | ||
| 42 | +#include <sys/mman.h> | ||
| 43 | +#include <unistd.h> | ||
| 44 | + | ||
| 45 | +#include "compat/libobmm-compat.h" | ||
| 46 | + | ||
| 47 | +#include "framework/obmm_test.h" | ||
| 48 | + | ||
| 49 | +#include "utils/data.h" | ||
| 50 | +#include "utils/export.h" | ||
| 51 | +#include "utils/obmm_dev.h" | ||
| 52 | +#include "utils/update_range.h" | ||
| 53 | + | ||
| 54 | +#define EXPORT_SIZE (8 * 1024 * 1024) | ||
| 55 | +#define MMAP_SIZE (2 * 1024 * 1024) | ||
| 56 | +#define DATA_WORD_LEN 8U | ||
| 57 | + | ||
| 58 | +struct invalid_state_case { | ||
| 59 | + uint8_t mem_state; | ||
| 60 | + const char *desc; | ||
| 61 | +}; | ||
| 62 | + | ||
| 63 | +/* | ||
| 64 | + * Run the invalid mem_state matrix: every UPDATE_RANGE must fail with | ||
| 65 | + * EINVAL and the mapping must stay untouched for the next case. | ||
| 66 | + */ | ||
| 67 | +static void run_invalid_state_cases(int fd, void *addr, const struct invalid_state_case *cases, unsigned int count) { | ||
| 68 | + unsigned int i; | ||
| 69 | + | ||
| 70 | + for (i = 0; i < count; ++i) { | ||
| 71 | + errno = 0; | ||
| 72 | + ASSERT_MSG(do_update_range(fd, (uint64_t)(uintptr_t)addr, (uint64_t)(uintptr_t)addr + MMAP_SIZE, | ||
| 73 | + cases[i].mem_state, OBMM_SHM_CACHE_INFER) == -1 && | ||
| 74 | + errno == EINVAL, | ||
| 75 | + "case %u (%s): expected EINVAL, got errno=%d (%s)", i + 1, cases[i].desc, errno, strerror(errno)); | ||
| 76 | + } | ||
| 77 | +} | ||
| 78 | + | ||
| 79 | +static int update_range_invalid_mem_state_matrix_test(void) { | ||
| 80 | + size_t lengths[OBMM_MAX_LOCAL_NUMA_NODES]; | ||
| 81 | + struct obmm_mem_desc desc; | ||
| 82 | + struct invalid_state_case cases[9]; | ||
| 83 | + mem_id id; | ||
| 84 | + int fd; | ||
| 85 | + void *addr; | ||
| 86 | + unsigned char pattern; | ||
| 87 | + | ||
| 88 | + memset(&desc, 0, sizeof(desc)); | ||
| 89 | + ASSERT_ERRNO_MSG(fill_obmm_export_length_array( | ||
| 90 | + lengths, | ||
| 91 | + &(struct obmm_alloc_info){ | ||
| 92 | + .policy = OBMM_ALLOC_INTERLEAVE, | ||
| 93 | + .scope = {.unit = OBMM_ALLOC_UNIT_HOST, .unit_id = 0, .total_size = EXPORT_SIZE}, | ||
| 94 | + }) == 0, | ||
| 95 | + "fill_obmm_export_length_array failed"); | ||
| 96 | + ASSERT_ERRNO_MSG(fill_obmm_mem_desc_by_lengths(&desc, lengths) == 0, "fill_obmm_mem_desc_by_lengths failed"); | ||
| 97 | + errno = 0; | ||
| 98 | + id = obmm_export(lengths, OBMM_EXPORT_FLAG_ALLOW_MMAP, &desc); | ||
| 99 | + ASSERT_ERRNO_MSG(id != OBMM_INVALID_MEMID, "obmm_export failed"); | ||
| 100 | + | ||
| 101 | + ASSERT_ERRNO_MSG(open_obmm_shmdev(&fd, id, O_RDWR) == 0, "open failed"); | ||
| 102 | + | ||
| 103 | + addr = mmap(NULL, MMAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); | ||
| 104 | + ASSERT_MSG(addr != MAP_FAILED, "mmap failed: %s", strerror(errno)); | ||
| 105 | + | ||
| 106 | + /* baseline: the freshly mapped RW region accepts data */ | ||
| 107 | + pattern = 0x3C; | ||
| 108 | + fill_data(addr, MMAP_SIZE, DATA_WORD_LEN, pattern); | ||
| 109 | + ASSERT_MSG(check_data(addr, MMAP_SIZE, DATA_WORD_LEN, pattern) == 0, "baseline pattern verify failed"); | ||
| 110 | + | ||
| 111 | + /* rule 1: bits outside the cache+access masks are unknown */ | ||
| 112 | + cases[0] = | ||
| 113 | + (struct invalid_state_case){.mem_state = (uint8_t)(MEM_STATE(MEM_CACHE_NORMAL, MEM_ACCESS_READWRITE) | 0x10), | ||
| 114 | + .desc = "unknown bit 0x10 set"}; | ||
| 115 | + cases[1] = | ||
| 116 | + (struct invalid_state_case){.mem_state = (uint8_t)(MEM_STATE(MEM_CACHE_NORMAL, MEM_ACCESS_READWRITE) | 0x80), | ||
| 117 | + .desc = "unknown bit 0x80 set"}; | ||
| 118 | + | ||
| 119 | + /* rule 2: reserved cacheability value is rejected regardless of access */ | ||
| 120 | + cases[2] = (struct invalid_state_case){.mem_state = MEM_STATE(OBMM_SHM_MEM_CACHE_RESV, MEM_ACCESS_READWRITE), | ||
| 121 | + .desc = "reserved cache + RW"}; | ||
| 122 | + cases[3] = (struct invalid_state_case){.mem_state = MEM_STATE(OBMM_SHM_MEM_CACHE_RESV, MEM_ACCESS_READONLY), | ||
| 123 | + .desc = "reserved cache + R (all-zero state)"}; | ||
| 124 | + cases[4] = (struct invalid_state_case){.mem_state = MEM_STATE(OBMM_SHM_MEM_CACHE_RESV, MEM_ACCESS_NO_ACCESS), | ||
| 125 | + .desc = "reserved cache + no access"}; | ||
| 126 | + | ||
| 127 | + /* rule 3: NC/device memory cannot be executable */ | ||
| 128 | + cases[5] = (struct invalid_state_case){.mem_state = MEM_STATE(MEM_CACHE_NORMAL_NC, MEM_ACCESS_READEXEC), | ||
| 129 | + .desc = "NC + READEXEC"}; | ||
| 130 | + cases[6] = (struct invalid_state_case){.mem_state = MEM_STATE(MEM_CACHE_DEVICE, MEM_ACCESS_READEXEC), | ||
| 131 | + .desc = "device + READEXEC"}; | ||
| 132 | + | ||
| 133 | + /* rule 4: NC state with access other than NO_ACCESS */ | ||
| 134 | + cases[7] = (struct invalid_state_case){.mem_state = MEM_STATE(MEM_CACHE_NORMAL_NC, MEM_ACCESS_READONLY), | ||
| 135 | + .desc = "NC + READONLY"}; | ||
| 136 | + cases[8] = (struct invalid_state_case){.mem_state = MEM_STATE(MEM_CACHE_NORMAL_NC, MEM_ACCESS_READWRITE), | ||
| 137 | + .desc = "NC + RW"}; | ||
| 138 | + | ||
| 139 | + run_invalid_state_cases(fd, addr, cases, sizeof(cases) / sizeof(cases[0])); | ||
| 140 | + | ||
| 141 | + /* the failed updates must not have corrupted the mapping state */ | ||
| 142 | + pattern = 0x5F; | ||
| 143 | + fill_data(addr, MMAP_SIZE, DATA_WORD_LEN, pattern); | ||
| 144 | + ASSERT_MSG(check_data(addr, MMAP_SIZE, DATA_WORD_LEN, pattern) == 0, | ||
| 145 | + "mapping corrupted after invalid mem_state matrix"); | ||
| 146 | + | ||
| 147 | + /* a valid INFER round-trip must still succeed after the matrix */ | ||
| 148 | + ASSERT_ERRNO_MSG(do_update_range(fd, (uint64_t)(uintptr_t)addr, (uint64_t)(uintptr_t)addr + MMAP_SIZE, | ||
| 149 | + MEM_STATE(MEM_CACHE_NORMAL, MEM_ACCESS_READONLY), OBMM_SHM_CACHE_INFER) == 0, | ||
| 150 | + "valid RW->RO INFER update failed after matrix"); | ||
| 151 | + ASSERT_MSG(check_data(addr, MMAP_SIZE, DATA_WORD_LEN, pattern) == 0, "data not preserved after valid RW->RO"); | ||
| 152 | + | ||
| 153 | + ASSERT_ERRNO_MSG(do_update_range(fd, (uint64_t)(uintptr_t)addr, (uint64_t)(uintptr_t)addr + MMAP_SIZE, | ||
| 154 | + MEM_STATE(MEM_CACHE_NORMAL, MEM_ACCESS_READWRITE), OBMM_SHM_CACHE_INFER) == 0, | ||
| 155 | + "valid RO->RW INFER update failed after matrix"); | ||
| 156 | + pattern = 0x7D; | ||
| 157 | + fill_data(addr, MMAP_SIZE, DATA_WORD_LEN, pattern); | ||
| 158 | + ASSERT_MSG(check_data(addr, MMAP_SIZE, DATA_WORD_LEN, pattern) == 0, "write after RO->RW restore failed"); | ||
| 159 | + | ||
| 160 | + munmap(addr, MMAP_SIZE); | ||
| 161 | + close(fd); | ||
| 162 | + obmm_unexport(id, 0); | ||
| 163 | + | ||
| 164 | + return 0; | ||
| 165 | +} | ||
| 166 | + | ||
| 167 | +REGISTER_SINGLE_ACTOR_TEST_FUNC("mmap_and_ownership", "update_range_invalid_mem_state_matrix", | ||
| 168 | + update_range_invalid_mem_state_matrix_test, 60); | ||
| @@ -0,0 +1,140 @@ | |||
| 1 | +/* SPDX-License-Identifier: MulanPSL-2.0 */ | ||
| 2 | +/** | ||
| 3 | + * @file export_useraddr_basic.c | ||
| 4 | + * @brief Validates the positive export path of obmm_export_useraddr(). | ||
| 5 | + * | ||
| 6 | + * Single-actor API test covering gap-analysis item E1: the current process | ||
| 7 | + * exports one PMD-size hugetlb anonymous mapping through | ||
| 8 | + * obmm_export_useraddr(). The upstream contract (obmm_export.md) requires the | ||
| 9 | + * target range to be PMD_SIZE-aligned and backed by PMD-granularity mappings; | ||
| 10 | + * the kernel pins the pages and marks the resulting region as memory coming | ||
| 11 | + * from a user process (obmm_export_from_user.c). | ||
| 12 | + * | ||
| 13 | + * Test flow: | ||
| 14 | + * 1. Resolve the PMD hugepage size of the running kernel; no hardcoded 2 MB, | ||
| 15 | + * so the case also applies to 64 KB-page systems where PMD_SIZE is 512 MB. | ||
| 16 | + * 2. Find a present local NUMA node with at least one free PMD hugepage and | ||
| 17 | + * fill the descriptor deid from that node's UB controller. | ||
| 18 | + * 3. mmap() one PMD-size hugetlb page (MAP_HUGETLB plus the matching | ||
| 19 | + * MAP_HUGE_<size> encoding) and bind the range to the selected NUMA node | ||
| 20 | + * with mbind() before the first fault. Hugetlb pages are only allocated | ||
| 21 | + * when faulted, and the first fault happens inside the export call when | ||
| 22 | + * the kernel pins the range; without an explicit policy the page follows | ||
| 23 | + * the node of whatever CPU the thread runs on. The upstream contract | ||
| 24 | + * requires an exported user range to stay within one socket, so pages on | ||
| 25 | + * a node not covered by the descriptor deid make the export fail with | ||
| 26 | + * EOPNOTSUPP. | ||
| 27 | + * 4. obmm_export_useraddr(0, va, pmd_size, 0, &desc) must succeed; pid=0 | ||
| 28 | + * selects the calling process. | ||
| 29 | + * 5. Assert the returned mem_id is valid, the exported device size matches | ||
| 30 | + * the request, and the sysfs export_info/memory_from_user attribute | ||
| 31 | + * reports 1 (exported from user memory, not from the OBMM pool). | ||
| 32 | + * 6. Cleanup: obmm_unexport() first, then munmap() — the upstream contract | ||
| 33 | + * requires releasing the export before destroying the user mapping. The | ||
| 34 | + * test never accesses the mapping between export and unexport because the | ||
| 35 | + * kernel rewrites the page-table protections of the exported range. | ||
| 36 | + * | ||
| 37 | + * Environment assumptions: | ||
| 38 | + * - The OBMM driver is loaded with the obmm device visible. | ||
| 39 | + * - PMD-level hugepages are reserved on at least one local NUMA node (via | ||
| 40 | + * /proc/sys/vm/nr_hugepages or the per-size sysfs nr_hugepages files). The | ||
| 41 | + * test fails with guidance instead of reserving hugepages itself to avoid | ||
| 42 | + * system-state side effects. | ||
| 43 | + * - The hugetlb range is explicitly bound to the descriptor node before | ||
| 44 | + * export, so the pages and the descriptor deid land on the same node | ||
| 45 | + * regardless of scheduler placement or the host package count. | ||
| 46 | + */ | ||
| 47 | + | ||
| 48 | +#include <errno.h> | ||
| 49 | +#include <stdint.h> | ||
| 50 | +#include <string.h> | ||
| 51 | +#include <sys/mman.h> | ||
| 52 | + | ||
| 53 | +#include "compat/libobmm-compat.h" | ||
| 54 | + | ||
| 55 | +#include "framework/obmm_test.h" | ||
| 56 | + | ||
| 57 | +#include "utils/numa_setup.h" | ||
| 58 | +#include "utils/obmm_dev.h" | ||
| 59 | +#include "utils/os_info.h" | ||
| 60 | + | ||
| 61 | +#include "export_useraddr_common.h" | ||
| 62 | + | ||
| 63 | +#ifndef MAP_HUGE_SHIFT | ||
| 64 | +#define MAP_HUGE_SHIFT 26 | ||
| 65 | +#endif | ||
| 66 | + | ||
| 67 | +static int test_export_useraddr_basic(void) { | ||
| 68 | + size_t pmd_size; | ||
| 69 | + struct obmm_mem_desc desc; | ||
| 70 | + unsigned int numa_id; | ||
| 71 | + unsigned long memory_from_user; | ||
| 72 | + size_t dev_size; | ||
| 73 | + void *va; | ||
| 74 | + int mmap_huge_flag; | ||
| 75 | + mem_id id; | ||
| 76 | + int ret; | ||
| 77 | + | ||
| 78 | + ret = get_pmd_hugepage_size(&pmd_size); | ||
| 79 | + ASSERT_ERRNO_MSG(ret == 0, "%s: get_pmd_hugepage_size failed", __func__); | ||
| 80 | + /* MAP_HUGE_<size> encodes log2(size), so the size must be a power of two */ | ||
| 81 | + ASSERT_MSG(pmd_size != 0 && (pmd_size & (pmd_size - 1)) == 0, "%s: PMD hugepage size %#zx is not a power of two", | ||
| 82 | + __func__, pmd_size); | ||
| 83 | + | ||
| 84 | + ret = export_useraddr_find_free_hugepage_numa(&numa_id, pmd_size); | ||
| 85 | + ASSERT_ERRNO_MSG(ret == 0, | ||
| 86 | + "%s: no present local NUMA node with free PMD hugepages; reserve PMD-level hugepages first " | ||
| 87 | + "(e.g. /proc/sys/vm/nr_hugepages or /sys/kernel/mm/hugepages/hugepages-<size>kB/nr_hugepages)", | ||
| 88 | + __func__); | ||
| 89 | + LOG_DEBUG("%s: using NUMA node %u with free PMD hugepages", __func__, numa_id); | ||
| 90 | + | ||
| 91 | + ret = export_useraddr_fill_desc_for_numa(&desc, numa_id, pmd_size); | ||
| 92 | + ASSERT_ERRNO_MSG(ret == 0, "%s: export_useraddr_fill_desc_for_numa failed for node %u", __func__, numa_id); | ||
| 93 | + | ||
| 94 | + mmap_huge_flag = (int)(__builtin_ctzll((unsigned long long)pmd_size) << MAP_HUGE_SHIFT); | ||
| 95 | + errno = 0; | ||
| 96 | + va = | ||
| 97 | + mmap(NULL, pmd_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB | mmap_huge_flag, -1, 0); | ||
| 98 | + ASSERT_MSG(va != MAP_FAILED, "%s: mmap of one PMD-size hugetlb page failed: errno=%d (%s)", __func__, errno, | ||
| 99 | + strerror(errno)); | ||
| 100 | + | ||
| 101 | + /* The hugetlb page is not allocated at mmap time: the first fault happens | ||
| 102 | + * when obmm_export_useraddr() pins the range. Bind the mapping to the | ||
| 103 | + * descriptor node first so the allocation node matches desc.deid; with | ||
| 104 | + * the default policy the page would follow the node of the CPU the thread | ||
| 105 | + * runs on, and the spec requires an exported user range to stay within | ||
| 106 | + * one socket, so cross-socket placement is rejected with EOPNOTSUPP. */ | ||
| 107 | + ret = mbind_numa_memory(va, pmd_size, numa_id); | ||
| 108 | + ASSERT_ERRNO_MSG(ret == 0, "%s: failed to bind hugetlb range to NUMA node %u", __func__, numa_id); | ||
| 109 | + | ||
| 110 | + LOG_DEBUG("%s: exporting current process va=%p length=%zu", __func__, va, pmd_size); | ||
| 111 | + errno = 0; | ||
| 112 | + id = obmm_export_useraddr(0, va, pmd_size, 0, &desc); | ||
| 113 | + ASSERT_MSG(id != OBMM_INVALID_MEMID, "%s: obmm_export_useraddr failed: errno=%d (%s)", __func__, errno, | ||
| 114 | + strerror(errno)); | ||
| 115 | + ASSERT_MSG(desc.length == pmd_size, "%s: desc.length=%llu, expected %zu", __func__, (unsigned long long)desc.length, | ||
| 116 | + pmd_size); | ||
| 117 | + | ||
| 118 | + ret = get_obmm_dev_size(&dev_size, id); | ||
| 119 | + ASSERT_ERRNO_MSG(ret == 0, "%s: get_obmm_dev_size failed for mem_id %llu", __func__, (unsigned long long)id); | ||
| 120 | + ASSERT_MSG(dev_size == pmd_size, "%s: device size=%zu, expected %zu", __func__, dev_size, pmd_size); | ||
| 121 | + | ||
| 122 | + ret = get_obmm_sysfs_attr_ul(&memory_from_user, id, "export_info/memory_from_user"); | ||
| 123 | + ASSERT_ERRNO_MSG(ret == 0, "%s: failed to read export_info/memory_from_user for mem_id %llu", __func__, | ||
| 124 | + (unsigned long long)id); | ||
| 125 | + ASSERT_MSG(memory_from_user == 1, "%s: export_info/memory_from_user=%lu, expected 1", __func__, memory_from_user); | ||
| 126 | + | ||
| 127 | + LOG_DEBUG("%s: export succeeded with mem_id=%llu, releasing via obmm_unexport before munmap", __func__, | ||
| 128 | + (unsigned long long)id); | ||
| 129 | + errno = 0; | ||
| 130 | + ret = obmm_unexport(id, 0); | ||
| 131 | + ASSERT_MSG(ret == 0, "%s: obmm_unexport failed: errno=%d (%s)", __func__, errno, strerror(errno)); | ||
| 132 | + | ||
| 133 | + errno = 0; | ||
| 134 | + ret = munmap(va, pmd_size); | ||
| 135 | + ASSERT_MSG(ret == 0, "%s: munmap failed: errno=%d (%s)", __func__, errno, strerror(errno)); | ||
| 136 | + | ||
| 137 | + return 0; | ||
| 138 | +} | ||
| 139 | + | ||
| 140 | +REGISTER_SINGLE_ACTOR_TEST_FUNC("obmm_export_api", "export_useraddr_basic", test_export_useraddr_basic, 120); | ||
| @@ -0,0 +1,106 @@ | |||
| 1 | +/* SPDX-License-Identifier: MulanPSL-2.0 */ | ||
| 2 | +/** | ||
| 3 | + * @file export_useraddr_common.h | ||
| 4 | + * @brief Shared helpers for the obmm_export_useraddr() test cases | ||
| 5 | + * | ||
| 6 | + * Provides small static-inline helpers shared by the export_useraddr_* | ||
| 7 | + * test cases in this directory: | ||
| 8 | + * - selecting a present local NUMA node as descriptor source | ||
| 9 | + * - finding a present local NUMA node with free PMD-level hugepages | ||
| 10 | + * - filling one OBMM memory descriptor for obmm_export_useraddr() | ||
| 11 | + * | ||
| 12 | + * All helpers are test-local building blocks; they abort through the caller's | ||
| 13 | + * assertion macros, never on their own. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | +#ifndef OBMM_TEST_EXPORT_USERADDR_COMMON_H | ||
| 17 | +#define OBMM_TEST_EXPORT_USERADDR_COMMON_H | ||
| 18 | + | ||
| 19 | +#include <errno.h> | ||
| 20 | +#include <stddef.h> | ||
| 21 | +#include <string.h> | ||
| 22 | + | ||
| 23 | +#include "compat/libobmm-compat.h" | ||
| 24 | + | ||
| 25 | +#include "utils/export.h" | ||
| 26 | +#include "utils/os_info.h" | ||
| 27 | + | ||
| 28 | +/** | ||
| 29 | + * @brief Find the lowest present local NUMA node ID | ||
| 30 | + * @param numa_id Output pointer for the NUMA node ID | ||
| 31 | + * @return 0 on success, -1 with errno set when no local NUMA node is present | ||
| 32 | + * @pre numa_id != NULL | ||
| 33 | + */ | ||
| 34 | +static inline int export_useraddr_first_present_numa(unsigned int *numa_id) { | ||
| 35 | + bool present; | ||
| 36 | + unsigned int candidate; | ||
| 37 | + | ||
| 38 | + for (candidate = 0; candidate < OBMM_MAX_LOCAL_NUMA_NODES; ++candidate) { | ||
| 39 | + if (is_local_numa_present(&present, candidate) != 0) { | ||
| 40 | + continue; | ||
| 41 | + } | ||
| 42 | + if (present) { | ||
| 43 | + *numa_id = candidate; | ||
| 44 | + return 0; | ||
| 45 | + } | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + errno = ENOENT; | ||
| 49 | + return -1; | ||
| 50 | +} | ||
| 51 | + | ||
| 52 | +/** | ||
| 53 | + * @brief Find the lowest present local NUMA node with free hugepages | ||
| 54 | + * @param numa_id Output pointer for the NUMA node ID | ||
| 55 | + * @param hugepage_size Hugepage size in bytes (PMD- or PUD-level) | ||
| 56 | + * @return 0 on success, -1 with errno set when no present local NUMA node | ||
| 57 | + * reports at least one free hugepage of the requested size | ||
| 58 | + * @pre numa_id != NULL | ||
| 59 | + * | ||
| 60 | + * The free count is reread from sysfs on every call, so the result reflects | ||
| 61 | + * the current hugepage pool state. | ||
| 62 | + */ | ||
| 63 | +static inline int export_useraddr_find_free_hugepage_numa(unsigned int *numa_id, size_t hugepage_size) { | ||
| 64 | + bool present; | ||
| 65 | + unsigned int free_pages; | ||
| 66 | + unsigned int candidate; | ||
| 67 | + | ||
| 68 | + for (candidate = 0; candidate < OBMM_MAX_LOCAL_NUMA_NODES; ++candidate) { | ||
| 69 | + if (is_local_numa_present(&present, candidate) != 0 || !present) { | ||
| 70 | + continue; | ||
| 71 | + } | ||
| 72 | + if (get_numa_free_hugepage_count(&free_pages, candidate, hugepage_size) != 0) { | ||
| 73 | + continue; | ||
| 74 | + } | ||
| 75 | + if (free_pages > 0) { | ||
| 76 | + *numa_id = candidate; | ||
| 77 | + return 0; | ||
| 78 | + } | ||
| 79 | + } | ||
| 80 | + | ||
| 81 | + errno = ENOENT; | ||
| 82 | + return -1; | ||
| 83 | +} | ||
| 84 | + | ||
| 85 | +/** | ||
| 86 | + * @brief Fill one OBMM memory descriptor for obmm_export_useraddr() | ||
| 87 | + * @param desc Descriptor to initialize | ||
| 88 | + * @param numa_id NUMA node whose UB controller provides the descriptor deid | ||
| 89 | + * @param length Export length used to select and validate the source package | ||
| 90 | + * @return 0 on success, -1 on error (errno set by the underlying helper) | ||
| 91 | + * @pre desc != NULL | ||
| 92 | + * | ||
| 93 | + * obmm_export_useraddr() takes a single length instead of the per-NUMA array | ||
| 94 | + * used by obmm_export(), so the descriptor is filled through a one-node | ||
| 95 | + * length array. The deid must be a valid UB controller EID, otherwise the | ||
| 96 | + * userspace vendor adaptation fails before the kernel sees the call. | ||
| 97 | + */ | ||
| 98 | +static inline int export_useraddr_fill_desc_for_numa(struct obmm_mem_desc *desc, unsigned int numa_id, size_t length) { | ||
| 99 | + size_t lengths[OBMM_MAX_LOCAL_NUMA_NODES] = {0}; | ||
| 100 | + | ||
| 101 | + memset(desc, 0, sizeof(*desc)); | ||
| 102 | + lengths[numa_id] = length; | ||
| 103 | + return fill_obmm_mem_desc_by_lengths(desc, lengths); | ||
| 104 | +} | ||
| 105 | + | ||
| 106 | +#endif /* OBMM_TEST_EXPORT_USERADDR_COMMON_H */ | ||
| @@ -0,0 +1,102 @@ | |||
| 1 | +/* SPDX-License-Identifier: MulanPSL-2.0 */ | ||
| 2 | +/** | ||
| 3 | + * @file export_useraddr_length_pmd_align.c | ||
| 4 | + * @brief Validates PMD_SIZE length alignment rejection of obmm_export_useraddr(). | ||
| 5 | + * | ||
| 6 | + * Single-actor API test covering gap-analysis item E2: obmm_export_useraddr() | ||
| 7 | + * requires the export length to be aligned to PMD_SIZE (unlike obmm_export(), | ||
| 8 | + * which aligns to the basic export granularity). The kernel rejects zero and | ||
| 9 | + * PMD-misaligned lengths in the parameter-check stage of obmm_export_pid() | ||
| 10 | + * with -EINVAL, before any page is pinned or any VA is dereferenced. | ||
| 11 | + * | ||
| 12 | + * Test flow: | ||
| 13 | + * 1. Resolve the PMD hugepage size of the running kernel (dynamic, so the | ||
| 14 | + * case also applies to 64 KB-page systems where PMD_SIZE is 512 MB). | ||
| 15 | + * 2. Fill a valid descriptor (valid deid) so a rejection can only come from | ||
| 16 | + * the length check — an invalid deid would fail earlier in the userspace | ||
| 17 | + * vendor adaptation with the same EINVAL and hide the real cause. | ||
| 18 | + * 3. mmap() a normal anonymous region of 2 * PMD_SIZE as the VA argument; | ||
| 19 | + * misaligned lengths never reach the pinning stage, so hugetlb backing | ||
| 20 | + * is not required and the case stays independent of hugepage availability. | ||
| 21 | + * 4. Call obmm_export_useraddr() with length values that straddle the | ||
| 22 | + * alignment boundaries and expect OBMM_INVALID_MEMID with errno EINVAL: | ||
| 23 | + * - 0 (zero-length export) | ||
| 24 | + * - PMD_SIZE - 1 (largest misaligned below the boundary) | ||
| 25 | + * - PMD_SIZE + 1 (smallest misaligned above the boundary) | ||
| 26 | + * - PMD_SIZE * 3 / 2 (mid misaligned, 1.5x) | ||
| 27 | + * - PMD_SIZE * 2 - 1 (largest misaligned within two units) | ||
| 28 | + * 5. Cleanup: munmap() the anonymous region. | ||
| 29 | + * | ||
| 30 | + * Environment assumptions: | ||
| 31 | + * - The OBMM driver is loaded with the obmm device visible. | ||
| 32 | + * - At least one present local NUMA node for the descriptor deid. | ||
| 33 | + */ | ||
| 34 | + | ||
| 35 | +#include <errno.h> | ||
| 36 | +#include <stdint.h> | ||
| 37 | +#include <string.h> | ||
| 38 | +#include <sys/mman.h> | ||
| 39 | + | ||
| 40 | +#include "compat/libobmm-compat.h" | ||
| 41 | + | ||
| 42 | +#include "framework/obmm_test.h" | ||
| 43 | + | ||
| 44 | +#include "utils/obmm_dev.h" | ||
| 45 | +#include "utils/os_info.h" | ||
| 46 | + | ||
| 47 | +#include "export_useraddr_common.h" | ||
| 48 | + | ||
| 49 | +static int test_export_useraddr_length_pmd_align(void) { | ||
| 50 | + size_t pmd_size; | ||
| 51 | + size_t misaligned_lengths[5]; | ||
| 52 | + struct obmm_mem_desc desc; | ||
| 53 | + unsigned int numa_id; | ||
| 54 | + void *va; | ||
| 55 | + mem_id id; | ||
| 56 | + unsigned int i; | ||
| 57 | + int ret; | ||
| 58 | + | ||
| 59 | + ret = get_pmd_hugepage_size(&pmd_size); | ||
| 60 | + ASSERT_ERRNO_MSG(ret == 0, "%s: get_pmd_hugepage_size failed", __func__); | ||
| 61 | + ASSERT_MSG(pmd_size != 0, "%s: PMD hugepage size is zero", __func__); | ||
| 62 | + | ||
| 63 | + ret = export_useraddr_first_present_numa(&numa_id); | ||
| 64 | + ASSERT_ERRNO_MSG(ret == 0, "%s: no present local NUMA node found", __func__); | ||
| 65 | + ret = export_useraddr_fill_desc_for_numa(&desc, numa_id, pmd_size); | ||
| 66 | + ASSERT_ERRNO_MSG(ret == 0, "%s: export_useraddr_fill_desc_for_numa failed for node %u", __func__, numa_id); | ||
| 67 | + | ||
| 68 | + /* The VA is never dereferenced for misaligned lengths: the kernel rejects | ||
| 69 | + * them in the parameter-check stage before pinning pages. A normal | ||
| 70 | + * anonymous mapping keeps this negative case independent of hugetlb | ||
| 71 | + * availability. */ | ||
| 72 | + errno = 0; | ||
| 73 | + va = mmap(NULL, 2 * pmd_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); | ||
| 74 | + ASSERT_MSG(va != MAP_FAILED, "%s: mmap of %zu anonymous bytes failed: errno=%d (%s)", __func__, 2 * pmd_size, errno, | ||
| 75 | + strerror(errno)); | ||
| 76 | + | ||
| 77 | + misaligned_lengths[0] = 0; | ||
| 78 | + misaligned_lengths[1] = pmd_size - 1; | ||
| 79 | + misaligned_lengths[2] = pmd_size + 1; | ||
| 80 | + misaligned_lengths[3] = pmd_size + pmd_size / 2; | ||
| 81 | + misaligned_lengths[4] = 2 * pmd_size - 1; | ||
| 82 | + | ||
| 83 | + for (i = 0; i < sizeof(misaligned_lengths) / sizeof(misaligned_lengths[0]); ++i) { | ||
| 84 | + LOG_DEBUG("%s: testing length=%zu", __func__, misaligned_lengths[i]); | ||
| 85 | + errno = 0; | ||
| 86 | + id = obmm_export_useraddr(0, va, misaligned_lengths[i], 0, &desc); | ||
| 87 | + ASSERT_MSG(id == OBMM_INVALID_MEMID, | ||
| 88 | + "%s: obmm_export_useraddr succeeded unexpectedly with misaligned length=%zu", __func__, | ||
| 89 | + misaligned_lengths[i]); | ||
| 90 | + ASSERT_MSG(errno == EINVAL, "%s: length=%zu rejected with unexpected errno: got %d (%s), expected EINVAL (%d)", | ||
| 91 | + __func__, misaligned_lengths[i], errno, strerror(errno), EINVAL); | ||
| 92 | + } | ||
| 93 | + | ||
| 94 | + errno = 0; | ||
| 95 | + ret = munmap(va, 2 * pmd_size); | ||
| 96 | + ASSERT_MSG(ret == 0, "%s: munmap failed: errno=%d (%s)", __func__, errno, strerror(errno)); | ||
| 97 | + | ||
| 98 | + return 0; | ||
| 99 | +} | ||
| 100 | + | ||
| 101 | +REGISTER_SINGLE_ACTOR_TEST_FUNC("obmm_export_api", "export_useraddr_length_pmd_align", | ||
| 102 | + test_export_useraddr_length_pmd_align, 60); | ||
| @@ -0,0 +1,87 @@ | |||
| 1 | +/* SPDX-License-Identifier: MulanPSL-2.0 */ | ||
| 2 | +/** | ||
| 3 | + * @file export_useraddr_non_hugetlb_rejected.c | ||
| 4 | + * @brief Validates that obmm_export_useraddr() rejects non-hugetlb memory. | ||
| 5 | + * | ||
| 6 | + * Single-actor API test covering gap-analysis item E3: obmm_export_useraddr() | ||
| 7 | + * only accepts PMD-granularity hugetlb mappings. The kernel validates every | ||
| 8 | + * pinned page with PageHuge() (obmm_export_from_user.c, the page-list check) | ||
| 9 | + * and fails the export with -EOPNOTSUPP when a page is not a hugetlb page. | ||
| 10 | + * Transparent huge pages do not satisfy PageHuge() either, so a plain | ||
| 11 | + * anonymous mapping is rejected deterministically regardless of THP settings. | ||
| 12 | + * | ||
| 13 | + * Test flow: | ||
| 14 | + * 1. Resolve the PMD hugepage size of the running kernel and use it as the | ||
| 15 | + * export length, so the length passes the PMD-alignment check and cannot | ||
| 16 | + * be the rejection reason. | ||
| 17 | + * 2. Fill a valid descriptor (valid deid) for the same reason. | ||
| 18 | + * 3. mmap() a normal anonymous region of PMD_SIZE (no MAP_HUGETLB) — the | ||
| 19 | + * pinned pages are ordinary base pages (or THP), which fail the kernel's | ||
| 20 | + * hugetlb page check. | ||
| 21 | + * 4. obmm_export_useraddr(0, va, pmd_size, 0, &desc) must fail with | ||
| 22 | + * OBMM_INVALID_MEMID and errno EOPNOTSUPP. | ||
| 23 | + * 5. Cleanup: munmap() the anonymous region; the kernel frees the pinned | ||
| 24 | + * pages on the export failure path, so no OBMM device is left behind. | ||
| 25 | + * | ||
| 26 | + * Environment assumptions: | ||
| 27 | + * - The OBMM driver is loaded with the obmm device visible. | ||
| 28 | + * - At least one present local NUMA node for the descriptor deid. | ||
| 29 | + * - Enough free memory to pin the anonymous region (the kernel pins every | ||
| 30 | + * page before the hugetlb check rejects the export). | ||
| 31 | + */ | ||
| 32 | + | ||
| 33 | +#include <errno.h> | ||
| 34 | +#include <stdint.h> | ||
| 35 | +#include <string.h> | ||
| 36 | +#include <sys/mman.h> | ||
| 37 | + | ||
| 38 | +#include "compat/libobmm-compat.h" | ||
| 39 | + | ||
| 40 | +#include "framework/obmm_test.h" | ||
| 41 | + | ||
| 42 | +#include "utils/obmm_dev.h" | ||
| 43 | +#include "utils/os_info.h" | ||
| 44 | + | ||
| 45 | +#include "export_useraddr_common.h" | ||
| 46 | + | ||
| 47 | +static int test_export_useraddr_non_hugetlb_rejected(void) { | ||
| 48 | + size_t pmd_size; | ||
| 49 | + struct obmm_mem_desc desc; | ||
| 50 | + unsigned int numa_id; | ||
| 51 | + void *va; | ||
| 52 | + mem_id id; | ||
| 53 | + int ret; | ||
| 54 | + | ||
| 55 | + ret = get_pmd_hugepage_size(&pmd_size); | ||
| 56 | + ASSERT_ERRNO_MSG(ret == 0, "%s: get_pmd_hugepage_size failed", __func__); | ||
| 57 | + ASSERT_MSG(pmd_size != 0, "%s: PMD hugepage size is zero", __func__); | ||
| 58 | + | ||
| 59 | + ret = export_useraddr_first_present_numa(&numa_id); | ||
| 60 | + ASSERT_ERRNO_MSG(ret == 0, "%s: no present local NUMA node found", __func__); | ||
| 61 | + ret = export_useraddr_fill_desc_for_numa(&desc, numa_id, pmd_size); | ||
| 62 | + ASSERT_ERRNO_MSG(ret == 0, "%s: export_useraddr_fill_desc_for_numa failed for node %u", __func__, numa_id); | ||
| 63 | + | ||
| 64 | + errno = 0; | ||
| 65 | + va = mmap(NULL, pmd_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); | ||
| 66 | + ASSERT_MSG(va != MAP_FAILED, "%s: mmap of %zu anonymous bytes failed: errno=%d (%s)", __func__, pmd_size, errno, | ||
| 67 | + strerror(errno)); | ||
| 68 | + | ||
| 69 | + LOG_DEBUG("%s: exporting non-hugetlb va=%p length=%zu, expecting EOPNOTSUPP", __func__, va, pmd_size); | ||
| 70 | + errno = 0; | ||
| 71 | + id = obmm_export_useraddr(0, va, pmd_size, 0, &desc); | ||
| 72 | + ASSERT_MSG(id == OBMM_INVALID_MEMID, "%s: obmm_export_useraddr succeeded unexpectedly on non-hugetlb memory", | ||
| 73 | + __func__); | ||
| 74 | + ASSERT_MSG(errno == EOPNOTSUPP, | ||
| 75 | + "%s: non-hugetlb export rejected with unexpected errno: got %d (%s), " | ||
| 76 | + "expected EOPNOTSUPP (%d)", | ||
| 77 | + __func__, errno, strerror(errno), EOPNOTSUPP); | ||
| 78 | + | ||
| 79 | + errno = 0; | ||
| 80 | + ret = munmap(va, pmd_size); | ||
| 81 | + ASSERT_MSG(ret == 0, "%s: munmap failed: errno=%d (%s)", __func__, errno, strerror(errno)); | ||
| 82 | + | ||
| 83 | + return 0; | ||
| 84 | +} | ||
| 85 | + | ||
| 86 | +REGISTER_SINGLE_ACTOR_TEST_FUNC("obmm_export_api", "export_useraddr_non_hugetlb_rejected", | ||
| 87 | + test_export_useraddr_non_hugetlb_rejected, 60); | ||
| @@ -0,0 +1,150 @@ | |||
| 1 | +/* SPDX-License-Identifier: MulanPSL-2.0 */ | ||
| 2 | +/** | ||
| 3 | + * @file unimport_busy.c | ||
| 4 | + * @brief Validates that obmm_unimport fails with -EBUSY while the imported | ||
| 5 | + * shmdev is held open and mapped. | ||
| 6 | + * | ||
| 7 | + * Single-actor test verifying the unimport busy path: an imported OBMM | ||
| 8 | + * region whose shmdev is still open (and mapped) holds a region | ||
| 9 | + * reference, so obmm_unimport() must fail with -EBUSY instead of tearing | ||
| 10 | + * the region down under the mapping. | ||
| 11 | + * | ||
| 12 | + * Steps: | ||
| 13 | + * 1. Resolve the local UB controller and decoder; map a decoder window | ||
| 14 | + * of 3*granu (granu = preimport granularity) via mami4ubmem. | ||
| 15 | + * 2. obmm_import() [pa, pa+2*granu) with ALLOW_MMAP — success. | ||
| 16 | + * 3. Open the imported shmdev O_RDWR and mmap it RW; touch every page | ||
| 17 | + * (one write and one read) to prove the mapping is usable. | ||
| 18 | + * 4. obmm_unimport() while the fd and mapping are held — expect ret=-1 | ||
| 19 | + * with errno=EBUSY. | ||
| 20 | + * 5. Re-touch the mapping — a rejected unimport must not tear down or | ||
| 21 | + * disturb the still-mapped region. | ||
| 22 | + * 6. Cleanup in strict reverse order: munmap, close, then | ||
| 23 | + * obmm_unimport() — success — then release the decoder window. | ||
| 24 | + * | ||
| 25 | + * The case is intentionally independent of OBMM read/write data | ||
| 26 | + * consistency, which virtual environments do not provide: accessibility | ||
| 27 | + * is proven by touch-based access (one write and one read per page | ||
| 28 | + * without comparing contents) instead of full-region pattern compares, | ||
| 29 | + * which would also be prohibitively slow over 2*granu of emulated | ||
| 30 | + * device memory. | ||
| 31 | + * | ||
| 32 | + * Spec reference: obmm_import(3) / obmm_unimport(3) | ||
| 33 | + * | ||
| 34 | + * Preconditions: | ||
| 35 | + * - OBMM driver loaded with numa_remote=preonline | ||
| 36 | + * - mami4ubmem plugin available on the local host | ||
| 37 | + * - At least one UB controller on the local host | ||
| 38 | + */ | ||
| 39 | + | ||
| 40 | +#include <errno.h> | ||
| 41 | +#include <fcntl.h> | ||
| 42 | +#include <stdint.h> | ||
| 43 | +#include <stdio.h> | ||
| 44 | +#include <string.h> | ||
| 45 | +#include <sys/mman.h> | ||
| 46 | +#include <unistd.h> | ||
| 47 | + | ||
| 48 | +#include "compat/libobmm-compat.h" | ||
| 49 | +#include "compat/mami4ubmem-compat.h" | ||
| 50 | + | ||
| 51 | +#include "framework/obmm_test.h" | ||
| 52 | + | ||
| 53 | +#include "utils/import.h" | ||
| 54 | +#include "utils/obmm_dev.h" | ||
| 55 | +#include "utils/os_info.h" | ||
| 56 | +#include "utils/ubmem_vendor.h" | ||
| 57 | + | ||
| 58 | +#include "tests/import_test_common.h" | ||
| 59 | + | ||
| 60 | +/* | ||
| 61 | + * touch every page with one write and one read: proves the range is | ||
| 62 | + * writable and readable without comparing data contents, so the case | ||
| 63 | + * stays independent of OBMM read/write data consistency | ||
| 64 | + */ | ||
| 65 | +static void touch_range(void *addr, size_t size) { | ||
| 66 | + size_t page_size; | ||
| 67 | + size_t i; | ||
| 68 | + unsigned char value = 0; | ||
| 69 | + | ||
| 70 | + ASSERT_ERRNO_MSG(get_page_size(&page_size) == 0, "touch_range: get_page_size failed"); | ||
| 71 | + for (i = 0; i < size; i += page_size) { | ||
| 72 | + volatile unsigned char *p = (volatile unsigned char *)addr + i; | ||
| 73 | + | ||
| 74 | + *p = (unsigned char)(i / page_size); | ||
| 75 | + value = *p; | ||
| 76 | + } | ||
| 77 | + (void)value; | ||
| 78 | +} | ||
| 79 | + | ||
| 80 | +static int unimport_busy_test(void) { | ||
| 81 | + unsigned int controller_id; | ||
| 82 | + uint32_t scna; | ||
| 83 | + uint8_t seid[16]; | ||
| 84 | + struct ubmem_decoder_id decoder_id; | ||
| 85 | + struct ubmem_decoder_config config; | ||
| 86 | + unsigned long pa_start; | ||
| 87 | + size_t granu; | ||
| 88 | + size_t window_size; | ||
| 89 | + size_t import_size; | ||
| 90 | + struct obmm_mem_desc desc; | ||
| 91 | + mem_id id; | ||
| 92 | + int fd; | ||
| 93 | + void *addr; | ||
| 94 | + int ret; | ||
| 95 | + | ||
| 96 | + ASSERT_ERRNO_MSG(resolve_local_controller(&controller_id, &scna, seid) == 0, "resolve_local_controller failed"); | ||
| 97 | + ASSERT_ERRNO_MSG(resolve_local_decoder(&decoder_id) == 0, "resolve_local_decoder failed"); | ||
| 98 | + ASSERT_ERRNO_MSG(fill_ubmem_decoder_config(&config, UBMEM_DECODER_CONFIG_PROFILE_CC_DEFAULT, 0) == 0, | ||
| 99 | + "fill_ubmem_decoder_config failed"); | ||
| 100 | + ASSERT_ERRNO_MSG(get_preimport_granu(&granu) == 0, "get_preimport_granu failed"); | ||
| 101 | + ASSERT_MSG(granu > 0, "preimport granularity is zero"); | ||
| 102 | + | ||
| 103 | + import_size = 2 * granu; | ||
| 104 | + window_size = 3 * granu; | ||
| 105 | + | ||
| 106 | + /* Map one decoder window providing contiguous PA space for the import */ | ||
| 107 | + ASSERT_ERRNO_MSG( | ||
| 108 | + setup_import_map(&pa_start, &decoder_id, &config, scna, seid, window_size, 0, MAMI4UBMEM_MAP_CC_SPACE) == 0, | ||
| 109 | + "setup_import_map for decoder window failed"); | ||
| 110 | + | ||
| 111 | + memset(&desc, 0, sizeof(desc)); | ||
| 112 | + fill_import_desc_default(&desc, pa_start, import_size, scna, seid); | ||
| 113 | + | ||
| 114 | + errno = 0; | ||
| 115 | + id = obmm_import(&desc, OBMM_IMPORT_FLAG_ALLOW_MMAP, 0, NULL); | ||
| 116 | + ASSERT_ERRNO_OBMM_IMPORT_PARAMS(id != OBMM_INVALID_MEMID, &desc, OBMM_IMPORT_FLAG_ALLOW_MMAP, 0, NULL, | ||
| 117 | + "obmm_import ALLOW_MMAP failed"); | ||
| 118 | + | ||
| 119 | + ASSERT_ERRNO_MSG(open_obmm_shmdev(&fd, id, O_RDWR) == 0, "open imported shmdev failed"); | ||
| 120 | + | ||
| 121 | + addr = mmap(NULL, desc.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); | ||
| 122 | + ASSERT_MSG(addr != MAP_FAILED, "mmap imported shmdev failed: %s", strerror(errno)); | ||
| 123 | + | ||
| 124 | + /* sanity: the imported mapping is writable and readable before the | ||
| 125 | + * busy check, proven by touch without content compares */ | ||
| 126 | + touch_range(addr, desc.length); | ||
| 127 | + | ||
| 128 | + /* core assertion: the held fd + mapping keep the region busy */ | ||
| 129 | + errno = 0; | ||
| 130 | + ret = obmm_unimport(id, 0); | ||
| 131 | + ASSERT_MSG(ret == -1 && errno == EBUSY, "unimport with open mmap: expected EBUSY, got ret=%d errno=%d (%s)", ret, | ||
| 132 | + errno, strerror(errno)); | ||
| 133 | + | ||
| 134 | + /* a rejected unimport must not disturb the still-mapped region */ | ||
| 135 | + touch_range(addr, desc.length); | ||
| 136 | + | ||
| 137 | + /* cleanup in reverse order: munmap, close, then unimport succeeds */ | ||
| 138 | + ASSERT_ERRNO_MSG(munmap(addr, desc.length) == 0, "munmap failed"); | ||
| 139 | + ASSERT_ERRNO_MSG(close(fd) == 0, "close shmdev failed"); | ||
| 140 | + | ||
| 141 | + errno = 0; | ||
| 142 | + ret = obmm_unimport(id, 0); | ||
| 143 | + ASSERT_ERRNO_MSG(ret == 0, "unimport after release failed, errno=%d (%s)", errno, strerror(errno)); | ||
| 144 | + | ||
| 145 | + ASSERT_ERRNO_MSG(teardown_import_map(pa_start) == 0, "teardown_import_map failed"); | ||
| 146 | + | ||
| 147 | + return 0; | ||
| 148 | +} | ||
| 149 | + | ||
| 150 | +REGISTER_SINGLE_ACTOR_TEST_FUNC("obmm_import_api", "unimport_busy", unimport_busy_test, 300); | ||
| @@ -9,8 +9,10 @@ | |||
| 9 | * cacheable mmap mode display. | 9 | * cacheable mmap mode display. |
| 10 | * | 10 | * |
| 11 | * Steps: | 11 | * Steps: |
| 12 | - * - proc-vmflags: VmFlags in smaps contains "rd" for RW and RO mappings | 12 | + * - proc-vmflags: VmFlags in smaps contains "rd" for RW and RO mappings, and |
| 13 | - * - proc-kernel-page-size: KernelPageSize is 4 KB or 2 MB | 13 | + * "wr" for RW mappings |
| 14 | + * - proc-kernel-page-size: KernelPageSize is exactly the page size for a | ||
| 15 | + * plain (PAGE-granularity) mapping | ||
| 14 | * - proc-perm-display: /proc/pid/maps shows "rw-s" for RW mmap | 16 | * - proc-perm-display: /proc/pid/maps shows "rw-s" for RW mmap |
| 15 | * - proc-update-range: UPDATE_RANGE RO transition preserves data, restore to RW | 17 | * - proc-update-range: UPDATE_RANGE RO transition preserves data, restore to RW |
| 16 | * - proc-cacheable-display: cacheable mmap VMA display in /proc/pid/maps | 18 | * - proc-cacheable-display: cacheable mmap VMA display in /proc/pid/maps |
| @@ -68,6 +70,8 @@ static int verify_proc_vmflags(mem_id id) { | |||
| 68 | 70 | ||
| 69 | ASSERT_MSG(strstr(vmflags, "rd") != NULL, "proc-vmflags: VmFlags missing 'rd' for RW mapping"); | 71 | ASSERT_MSG(strstr(vmflags, "rd") != NULL, "proc-vmflags: VmFlags missing 'rd' for RW mapping"); |
| 70 | 72 | ||
| 73 | + ASSERT_MSG(strstr(vmflags, "wr") != NULL, "proc-vmflags: VmFlags missing 'wr' for RW mapping"); | ||
| 74 | + | ||
| 71 | munmap(addr, test_size); | 75 | munmap(addr, test_size); |
| 72 | close(fd); | 76 | close(fd); |
| 73 | 77 | ||
| @@ -92,18 +96,18 @@ static int verify_proc_vmflags(mem_id id) { | |||
| 92 | } | 96 | } |
| 93 | 97 | ||
| 94 | /* | 98 | /* |
| 95 | - * proc-kernel-page-size: KernelPageSize display — must be page size or PMD hugepage size. | 99 | + * proc-kernel-page-size: KernelPageSize display — a plain mmap (no |
| 100 | + * HUGETLB_PMD flag) keeps the region in PAGE granularity, and the driver | ||
| 101 | + * reports exactly the page size for such a region. | ||
| 96 | */ | 102 | */ |
| 97 | static int verify_proc_kernel_page_size(mem_id id) { | 103 | static int verify_proc_kernel_page_size(mem_id id) { |
| 98 | int fd; | 104 | int fd; |
| 99 | void *addr; | 105 | void *addr; |
| 100 | size_t test_size = SZ_2M; | 106 | size_t test_size = SZ_2M; |
| 101 | size_t page_size; | 107 | size_t page_size; |
| 102 | - size_t pmd_size; | ||
| 103 | unsigned long kernel_page_size; | 108 | unsigned long kernel_page_size; |
| 104 | 109 | ||
| 105 | ASSERT_ERRNO_MSG(get_page_size(&page_size) == 0, "proc-kernel-page-size: get_page_size failed"); | 110 | ASSERT_ERRNO_MSG(get_page_size(&page_size) == 0, "proc-kernel-page-size: get_page_size failed"); |
| 106 | - ASSERT_ERRNO_MSG(get_pmd_hugepage_size(&pmd_size) == 0, "proc-kernel-page-size: get_pmd_hugepage_size failed"); | ||
| 107 | 111 | ||
| 108 | ASSERT_ERRNO_MSG(open_obmm_shmdev(&fd, id, O_RDWR) == 0, "proc-kernel-page-size: open failed"); | 112 | ASSERT_ERRNO_MSG(open_obmm_shmdev(&fd, id, O_RDWR) == 0, "proc-kernel-page-size: open failed"); |
| 109 | 113 | ||
| @@ -115,15 +119,10 @@ static int verify_proc_kernel_page_size(mem_id id) { | |||
| 115 | 119 | ||
| 116 | LOG_DEBUG("proc-kernel-page-size: KernelPageSize=%#lx (%lu KB)", kernel_page_size, kernel_page_size / 1024); | 120 | LOG_DEBUG("proc-kernel-page-size: KernelPageSize=%#lx (%lu KB)", kernel_page_size, kernel_page_size / 1024); |
| 117 | 121 | ||
| 118 | - ASSERT_MSG(kernel_page_size == page_size || kernel_page_size == pmd_size, | 122 | + ASSERT_MSG(kernel_page_size == page_size, "proc-kernel-page-size: unexpected KernelPageSize %#lx (expected %#lx)", |
| 119 | - "proc-kernel-page-size: unexpected KernelPageSize %#lx " | 123 | + kernel_page_size, (unsigned long)page_size); |
| 120 | - "(expected %#lx or %#lx)", | ||
| 121 | - kernel_page_size, (unsigned long)page_size, (unsigned long)pmd_size); | ||
| 122 | 124 | ||
| 123 | - if (kernel_page_size == page_size) | 125 | + LOG_DEBUG("proc-kernel-page-size: PAGE granularity (%#lx)", (unsigned long)page_size); |
| 124 | - LOG_DEBUG("proc-kernel-page-size: PAGE granularity (%#lx)", (unsigned long)page_size); | ||
| 125 | - else | ||
| 126 | - LOG_DEBUG("proc-kernel-page-size: PMD granularity (%#lx)", (unsigned long)pmd_size); | ||
| 127 | 126 | ||
| 128 | munmap(addr, test_size); | 127 | munmap(addr, test_size); |
| 129 | close(fd); | 128 | close(fd); |
| @@ -33,8 +33,7 @@ static int verify_sysfs_attributes(mem_id id) { | |||
| 33 | size_t size_val; | 33 | size_t size_val; |
| 34 | char buf[128]; | 34 | char buf[128]; |
| 35 | 35 | ||
| 36 | - ASSERT_ERRNO_MSG(get_obmm_dev_size(&size_val, id) == 0, | 36 | + ASSERT_ERRNO_MSG(get_obmm_dev_size(&size_val, id) == 0, "sysfs-attr-read: failed to read size attribute"); |
| 37 | - "sysfs-attr-read: failed to read size attribute"); | ||
| 38 | ASSERT_MSG(size_val > 0, "sysfs-attr-read: size is 0"); | 37 | ASSERT_MSG(size_val > 0, "sysfs-attr-read: size is 0"); |
| 39 | 38 | ||
| 40 | LOG_DEBUG("sysfs-attr-read: size=%#zx (%zu MB)", size_val, size_val / (1024 * 1024)); | 39 | LOG_DEBUG("sysfs-attr-read: size=%#zx (%zu MB)", size_val, size_val / (1024 * 1024)); |
| @@ -69,8 +68,7 @@ static int verify_sysfs_size(mem_id id) { | |||
| 69 | size_t size_val; | 68 | size_t size_val; |
| 70 | 69 | ||
| 71 | ASSERT_ERRNO_MSG(get_obmm_dev_size(&size_val, id) == 0, "sysfs-size: failed to read size attribute"); | 70 | ASSERT_ERRNO_MSG(get_obmm_dev_size(&size_val, id) == 0, "sysfs-size: failed to read size attribute"); |
| 72 | - ASSERT_MSG(size_val >= EXPORT_SIZE, "sysfs-size: size %#zx < export size %#zx", size_val, | 71 | + ASSERT_MSG(size_val >= EXPORT_SIZE, "sysfs-size: size %#zx < export size %#zx", size_val, (size_t)EXPORT_SIZE); |
| 73 | - (size_t)EXPORT_SIZE); | ||
| 74 | 72 | ||
| 75 | LOG_DEBUG("sysfs-size: size=%#zx >= export_size=%#zx OK", size_val, (size_t)EXPORT_SIZE); | 73 | LOG_DEBUG("sysfs-size: size=%#zx >= export_size=%#zx OK", size_val, (size_t)EXPORT_SIZE); |
| 76 | return 0; | 74 | return 0; |
| @@ -0,0 +1,326 @@ | |||
| 1 | +/* SPDX-License-Identifier: MulanPSL-2.0 */ | ||
| 2 | +/** | ||
| 3 | + * @file ptrace_ownership_consistency.c | ||
| 4 | + * @brief Verifies ptrace access immediately follows ownership transitions. | ||
| 5 | + * | ||
| 6 | + * Single-actor test validating the consistency between OBMM ownership | ||
| 7 | + * updates (UPDATE_RANGE) and cross-process ptrace access. The shmdev | ||
| 8 | + * access handler (obmm_vma_access) re-checks the VMA's vm_flags on every | ||
| 9 | + * access, and an ownership transition swaps those flags under the mmap | ||
| 10 | + * lock, so the next ptrace access must reflect the new permission | ||
| 11 | + * immediately — no delay, no window. | ||
| 12 | + * | ||
| 13 | + * Matrix: ownership states {READONLY, NO_ACCESS, READWRITE} x ptrace | ||
| 14 | + * {read, write}, anchored by the initial RW mapping before any | ||
| 15 | + * transition. Expected outcomes follow the access handler's vm_flags | ||
| 16 | + * check: | ||
| 17 | + * | ||
| 18 | + * | ownership | vm_flags | ptrace read | ptrace write | | ||
| 19 | + * |------------|-------------------|-------------|--------------| | ||
| 20 | + * | READWRITE | VM_READ|VM_WRITE | OK | OK | | ||
| 21 | + * | READONLY | VM_READ | OK | EIO | | ||
| 22 | + * | NO_ACCESS | none | EIO | EIO | | ||
| 23 | + * | ||
| 24 | + * Note: the gap-analysis matrix lists PROT_WRITE and R|W separately, but | ||
| 25 | + * in the kernel access model VM_WRITE maps to the READWRITE state and | ||
| 26 | + * access_to_vm_flags(READWRITE) restores VM_READ|VM_WRITE together, so | ||
| 27 | + * both rows collapse into one kernel state (READWRITE here). | ||
| 28 | + * | ||
| 29 | + * Failure errno note: the access handler returns -EINVAL, which the | ||
| 30 | + * ptrace layer (generic_ptrace_peekdata/pokedata) surfaces to userspace | ||
| 31 | + * as EIO. | ||
| 32 | + * | ||
| 33 | + * Role split: UPDATE_RANGE operates on current->mm, so the child — the | ||
| 34 | + * mapping owner — applies every ownership transition itself, while the | ||
| 35 | + * parent (tracer) drives the matrix via pipes and checks ptrace access | ||
| 36 | + * at each PTRACE_INTERRUPT stop (PTRACE_SEIZE / PTRACE_INTERRUPT / | ||
| 37 | + * PTRACE_CONT; INTERRUPT-stops always resume cleanly). | ||
| 38 | + * | ||
| 39 | + * Per round the parent peeks word[0] (must stay the child-written | ||
| 40 | + * pattern on every readable state) and pokes word[1] with a per-round | ||
| 41 | + * marker, reading it back after each successful poke. | ||
| 42 | + * | ||
| 43 | + * Preconditions: | ||
| 44 | + * - OBMM driver loaded with obmm_shmdev visible | ||
| 45 | + * - At least one NUMA node with available memory for export | ||
| 46 | + */ | ||
| 47 | + | ||
| 48 | +#include <errno.h> | ||
| 49 | +#include <fcntl.h> | ||
| 50 | +#include <signal.h> | ||
| 51 | +#include <stdint.h> | ||
| 52 | +#include <stdio.h> | ||
| 53 | +#include <string.h> | ||
| 54 | +#include <sys/mman.h> | ||
| 55 | +#include <sys/ptrace.h> | ||
| 56 | +#include <sys/types.h> | ||
| 57 | +#include <sys/wait.h> | ||
| 58 | +#include <unistd.h> | ||
| 59 | + | ||
| 60 | +#include "compat/libobmm-compat.h" | ||
| 61 | + | ||
| 62 | +#include "framework/obmm_test.h" | ||
| 63 | + | ||
| 64 | +#include "utils/export.h" | ||
| 65 | +#include "utils/obmm_dev.h" | ||
| 66 | +#include "utils/update_range.h" | ||
| 67 | + | ||
| 68 | +/* request byte sent from parent to child; mem_state values otherwise */ | ||
| 69 | +#define POC_REQ_EXIT 0xFF | ||
| 70 | + | ||
| 71 | +/* child exit codes for early-failure diagnosis */ | ||
| 72 | +#define CHILD_EXIT_DEV_SIZE 10 | ||
| 73 | +#define CHILD_EXIT_OPEN 11 | ||
| 74 | +#define CHILD_EXIT_MMAP 12 | ||
| 75 | +#define CHILD_EXIT_PIPE 13 | ||
| 76 | +#define CHILD_EXIT_UPDATE 14 | ||
| 77 | + | ||
| 78 | +static const uint8_t g_pattern[8] = {0x5A, 0xA5, 0x5A, 0xA5, 0x5A, 0xA5, 0x5A, 0xA5}; | ||
| 79 | +static const uint8_t g_done_byte = 1; | ||
| 80 | + | ||
| 81 | +/* shared with the fatal cleanup hook; only the parent process uses them */ | ||
| 82 | +static pid_t g_child_pid = -1; | ||
| 83 | +static mem_id g_dev_id = OBMM_INVALID_MEMID; | ||
| 84 | + | ||
| 85 | +/* best-effort cleanup for the assertion path: must not use ASSERT macros */ | ||
| 86 | +static void kill_child_and_release_device(void) { | ||
| 87 | + if (g_child_pid > 0) { | ||
| 88 | + kill(g_child_pid, SIGKILL); | ||
| 89 | + waitpid(g_child_pid, NULL, 0); | ||
| 90 | + g_child_pid = -1; | ||
| 91 | + } | ||
| 92 | + if (g_dev_id != OBMM_INVALID_MEMID) { | ||
| 93 | + obmm_unexport(g_dev_id, 0); | ||
| 94 | + g_dev_id = OBMM_INVALID_MEMID; | ||
| 95 | + } | ||
| 96 | +} | ||
| 97 | + | ||
| 98 | +/* child role: mmap the exported region RW, write the pattern, report the | ||
| 99 | + * mapping address, then apply ownership transitions on request until the | ||
| 100 | + * exit request arrives */ | ||
| 101 | +static void run_ownership_child(int req_fd, int done_fd) { | ||
| 102 | + size_t region_size; | ||
| 103 | + int fd; | ||
| 104 | + void *base; | ||
| 105 | + uint8_t req; | ||
| 106 | + | ||
| 107 | + if (get_obmm_dev_size(®ion_size, g_dev_id) != 0) | ||
| 108 | + _exit(CHILD_EXIT_DEV_SIZE); | ||
| 109 | + | ||
| 110 | + if (open_obmm_shmdev(&fd, g_dev_id, O_RDWR) != 0) | ||
| 111 | + _exit(CHILD_EXIT_OPEN); | ||
| 112 | + | ||
| 113 | + base = mmap(NULL, region_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); | ||
| 114 | + if (base == MAP_FAILED) | ||
| 115 | + _exit(CHILD_EXIT_MMAP); | ||
| 116 | + | ||
| 117 | + memcpy(base, g_pattern, sizeof(g_pattern)); | ||
| 118 | + | ||
| 119 | + if (write(done_fd, &base, sizeof(base)) != (ssize_t)sizeof(base)) | ||
| 120 | + _exit(CHILD_EXIT_PIPE); | ||
| 121 | + if (write(done_fd, &g_done_byte, sizeof(g_done_byte)) != (ssize_t)sizeof(g_done_byte)) | ||
| 122 | + _exit(CHILD_EXIT_PIPE); | ||
| 123 | + | ||
| 124 | + for (;;) { | ||
| 125 | + if (read(req_fd, &req, sizeof(req)) != (ssize_t)sizeof(req)) | ||
| 126 | + _exit(CHILD_EXIT_PIPE); | ||
| 127 | + if (req == POC_REQ_EXIT) | ||
| 128 | + break; | ||
| 129 | + | ||
| 130 | + /* exit with the pipe closed so the parent sees EOF on the done | ||
| 131 | + * pipe and can reap + diagnose this failure */ | ||
| 132 | + if (do_update_range(fd, (uint64_t)(uintptr_t)base, (uint64_t)(uintptr_t)base + region_size, req, | ||
| 133 | + OBMM_SHM_CACHE_INFER) != 0) | ||
| 134 | + _exit(CHILD_EXIT_UPDATE); | ||
| 135 | + | ||
| 136 | + if (write(done_fd, &g_done_byte, sizeof(g_done_byte)) != (ssize_t)sizeof(g_done_byte)) | ||
| 137 | + _exit(CHILD_EXIT_PIPE); | ||
| 138 | + } | ||
| 139 | + | ||
| 140 | + munmap(base, region_size); | ||
| 141 | + close(fd); | ||
| 142 | + _exit(0); | ||
| 143 | +} | ||
| 144 | + | ||
| 145 | +static void send_request(int req_fd, uint8_t req) { | ||
| 146 | + ASSERT_ERRNO_MSG(write(req_fd, &req, sizeof(req)) == (ssize_t)sizeof(req), "write request failed"); | ||
| 147 | +} | ||
| 148 | + | ||
| 149 | +/* wait for the child's done marker; a pipe EOF means the child died, in | ||
| 150 | + * which case it is reaped here and diagnosed through the assertion */ | ||
| 151 | +static void wait_done(int done_fd, const char *phase) { | ||
| 152 | + uint8_t byte; | ||
| 153 | + ssize_t n; | ||
| 154 | + | ||
| 155 | + n = read(done_fd, &byte, sizeof(byte)); | ||
| 156 | + if (n == 0) { | ||
| 157 | + int wstatus = 0; | ||
| 158 | + | ||
| 159 | + waitpid(g_child_pid, &wstatus, 0); | ||
| 160 | + g_child_pid = -1; | ||
| 161 | + ASSERT_MSG(0, "child died during %s (exited=%d code=%d signalled=%d sig=%d)", phase, WIFEXITED(wstatus), | ||
| 162 | + WIFEXITED(wstatus) ? WEXITSTATUS(wstatus) : -1, WIFSIGNALED(wstatus), | ||
| 163 | + WIFSIGNALED(wstatus) ? WTERMSIG(wstatus) : -1); | ||
| 164 | + } | ||
| 165 | + ASSERT_MSG(n == (ssize_t)sizeof(byte), "reading done marker failed during %s", phase); | ||
| 166 | +} | ||
| 167 | + | ||
| 168 | +static void interrupt_and_wait(pid_t child) { | ||
| 169 | + int wstatus; | ||
| 170 | + | ||
| 171 | + ASSERT_ERRNO_MSG(ptrace(PTRACE_INTERRUPT, child, 0, 0) == 0, "PTRACE_INTERRUPT failed"); | ||
| 172 | + errno = 0; | ||
| 173 | + ASSERT_ERRNO_MSG(waitpid(child, &wstatus, 0) == child, "waitpid after PTRACE_INTERRUPT failed"); | ||
| 174 | + ASSERT_MSG(WIFSTOPPED(wstatus) && WSTOPSIG(wstatus) == SIGTRAP, | ||
| 175 | + "child not in ptrace-stop after INTERRUPT (status=%#x)", (unsigned int)wstatus); | ||
| 176 | +} | ||
| 177 | + | ||
| 178 | +static void resume_child(pid_t child) { | ||
| 179 | + ASSERT_ERRNO_MSG(ptrace(PTRACE_CONT, child, 0, 0) == 0, "PTRACE_CONT failed"); | ||
| 180 | +} | ||
| 181 | + | ||
| 182 | +/* | ||
| 183 | + * One matrix cell pair: peek word[0] and poke word[1] at the child's | ||
| 184 | + * mapping, comparing against the expected outcomes for the current | ||
| 185 | + * ownership state. | ||
| 186 | + */ | ||
| 187 | +static void verify_ptrace_access(pid_t child, void *base, int expect_read, int expect_write, unsigned int round_seq, | ||
| 188 | + const char *desc) { | ||
| 189 | + long expected_word; | ||
| 190 | + long marker; | ||
| 191 | + long word; | ||
| 192 | + | ||
| 193 | + memcpy(&expected_word, g_pattern, sizeof(expected_word)); | ||
| 194 | + | ||
| 195 | + if (expect_read) { | ||
| 196 | + errno = 0; | ||
| 197 | + word = ptrace(PTRACE_PEEKDATA, child, base, NULL); | ||
| 198 | + ASSERT_ERRNO_MSG(errno == 0, "%s: PTRACE_PEEKDATA unexpectedly failed", desc); | ||
| 199 | + ASSERT_MSG(word == expected_word, "%s: peeked word %#lx != pattern %#lx", desc, (unsigned long)word, | ||
| 200 | + (unsigned long)expected_word); | ||
| 201 | + } else { | ||
| 202 | + errno = 0; | ||
| 203 | + ptrace(PTRACE_PEEKDATA, child, base, NULL); | ||
| 204 | + ASSERT_MSG(errno == EIO, "%s: PTRACE_PEEKDATA expected EIO, got errno=%d (%s)", desc, errno, strerror(errno)); | ||
| 205 | + } | ||
| 206 | + | ||
| 207 | + marker = (long)(0xA5000000UL | round_seq); | ||
| 208 | + if (expect_write) { | ||
| 209 | + errno = 0; | ||
| 210 | + ptrace(PTRACE_POKEDATA, child, (char *)base + sizeof(long), (void *)marker); | ||
| 211 | + ASSERT_ERRNO_MSG(errno == 0, "%s: PTRACE_POKEDATA unexpectedly failed", desc); | ||
| 212 | + | ||
| 213 | + errno = 0; | ||
| 214 | + word = ptrace(PTRACE_PEEKDATA, child, (char *)base + sizeof(long), NULL); | ||
| 215 | + ASSERT_ERRNO_MSG(errno == 0, "%s: read-back after PTRACE_POKEDATA failed", desc); | ||
| 216 | + ASSERT_MSG(word == marker, "%s: poked word %#lx != marker %#lx", desc, (unsigned long)word, | ||
| 217 | + (unsigned long)marker); | ||
| 218 | + } else { | ||
| 219 | + errno = 0; | ||
| 220 | + ptrace(PTRACE_POKEDATA, child, (char *)base + sizeof(long), (void *)marker); | ||
| 221 | + ASSERT_MSG(errno == EIO, "%s: PTRACE_POKEDATA expected EIO, got errno=%d (%s)", desc, errno, strerror(errno)); | ||
| 222 | + } | ||
| 223 | +} | ||
| 224 | + | ||
| 225 | +static int ptrace_ownership_consistency_test(void) { | ||
| 226 | + static const struct { | ||
| 227 | + uint8_t mem_state; | ||
| 228 | + int expect_read; | ||
| 229 | + int expect_write; | ||
| 230 | + const char *desc; | ||
| 231 | + } rounds[] = { | ||
| 232 | + {MEM_STATE(MEM_CACHE_NORMAL, MEM_ACCESS_READONLY), 1, 0, "READONLY"}, | ||
| 233 | + {MEM_STATE(MEM_CACHE_NORMAL, MEM_ACCESS_NO_ACCESS), 0, 0, "NO_ACCESS"}, | ||
| 234 | + {MEM_STATE(MEM_CACHE_NORMAL, MEM_ACCESS_READWRITE), 1, 1, "READWRITE"}, | ||
| 235 | + }; | ||
| 236 | + | ||
| 237 | + size_t lengths[OBMM_MAX_LOCAL_NUMA_NODES]; | ||
| 238 | + struct obmm_alloc_info alloc_info = {0}; | ||
| 239 | + struct obmm_mem_desc desc; | ||
| 240 | + int req_pipe[2]; | ||
| 241 | + int done_pipe[2]; | ||
| 242 | + pid_t child; | ||
| 243 | + int wstatus; | ||
| 244 | + void *base; | ||
| 245 | + unsigned int i; | ||
| 246 | + | ||
| 247 | + memset(&desc, 0, sizeof(desc)); | ||
| 248 | + alloc_info.policy = OBMM_ALLOC_MIN_MEM; | ||
| 249 | + alloc_info.scope.unit = OBMM_ALLOC_UNIT_HOST; | ||
| 250 | + alloc_info.scope.unit_id = 0; | ||
| 251 | + | ||
| 252 | + ASSERT_ERRNO_MSG(fill_obmm_export_length_array(lengths, &alloc_info) == 0, "fill_obmm_export_length_array failed"); | ||
| 253 | + ASSERT_ERRNO_MSG(fill_obmm_mem_desc_by_lengths(&desc, lengths) == 0, "fill_obmm_mem_desc_by_lengths failed"); | ||
| 254 | + errno = 0; | ||
| 255 | + g_dev_id = obmm_export(lengths, OBMM_EXPORT_FLAG_ALLOW_MMAP, &desc); | ||
| 256 | + ASSERT_ERRNO_MSG(g_dev_id != OBMM_INVALID_MEMID, "obmm_export failed"); | ||
| 257 | + | ||
| 258 | + ASSERT_ERRNO_MSG(pipe(req_pipe) == 0, "request pipe failed"); | ||
| 259 | + ASSERT_ERRNO_MSG(pipe(done_pipe) == 0, "done pipe failed"); | ||
| 260 | + | ||
| 261 | + child = fork(); | ||
| 262 | + ASSERT_MSG(child >= 0, "fork failed: %s", strerror(errno)); | ||
| 263 | + | ||
| 264 | + if (child == 0) { | ||
| 265 | + /* child reads requests from req_pipe[0] and reports on done_pipe[1] */ | ||
| 266 | + close(req_pipe[1]); | ||
| 267 | + close(done_pipe[0]); | ||
| 268 | + run_ownership_child(req_pipe[0], done_pipe[1]); | ||
| 269 | + _exit(0); | ||
| 270 | + } | ||
| 271 | + g_child_pid = child; | ||
| 272 | + obmm_test_set_fatal_cleanup(kill_child_and_release_device); | ||
| 273 | + | ||
| 274 | + /* parent sends requests on req_pipe[1] and reads reports on done_pipe[0] */ | ||
| 275 | + close(req_pipe[0]); | ||
| 276 | + close(done_pipe[1]); | ||
| 277 | + | ||
| 278 | + /* child reports its mapping address, then readiness */ | ||
| 279 | + { | ||
| 280 | + ssize_t n = read(done_pipe[0], &base, sizeof(base)); | ||
| 281 | + | ||
| 282 | + if (n == 0) { | ||
| 283 | + int dead_status = 0; | ||
| 284 | + | ||
| 285 | + waitpid(child, &dead_status, 0); | ||
| 286 | + g_child_pid = -1; | ||
| 287 | + ASSERT_MSG(0, "child died before reporting mapping (exited=%d code=%d)", WIFEXITED(dead_status), | ||
| 288 | + WIFEXITED(dead_status) ? WEXITSTATUS(dead_status) : -1); | ||
| 289 | + } | ||
| 290 | + ASSERT_MSG(n == (ssize_t)sizeof(base), "reading child mapping address failed"); | ||
| 291 | + } | ||
| 292 | + wait_done(done_pipe[0], "readiness handshake"); | ||
| 293 | + | ||
| 294 | + ASSERT_ERRNO_MSG(ptrace(PTRACE_SEIZE, child, 0, 0) == 0, "PTRACE_SEIZE failed"); | ||
| 295 | + | ||
| 296 | + /* baseline: the initial RW mapping allows ptrace read and write */ | ||
| 297 | + interrupt_and_wait(child); | ||
| 298 | + verify_ptrace_access(child, base, 1, 1, 1, "baseline RW"); | ||
| 299 | + resume_child(child); | ||
| 300 | + | ||
| 301 | + /* matrix: transition, then immediately re-check ptrace access */ | ||
| 302 | + for (i = 0; i < sizeof(rounds) / sizeof(rounds[0]); ++i) { | ||
| 303 | + send_request(req_pipe[1], rounds[i].mem_state); | ||
| 304 | + wait_done(done_pipe[0], rounds[i].desc); | ||
| 305 | + | ||
| 306 | + interrupt_and_wait(child); | ||
| 307 | + verify_ptrace_access(child, base, rounds[i].expect_read, rounds[i].expect_write, i + 2, rounds[i].desc); | ||
| 308 | + resume_child(child); | ||
| 309 | + } | ||
| 310 | + | ||
| 311 | + send_request(req_pipe[1], POC_REQ_EXIT); | ||
| 312 | + errno = 0; | ||
| 313 | + ASSERT_ERRNO_MSG(waitpid(child, &wstatus, 0) == child, "waitpid after exit request failed"); | ||
| 314 | + ASSERT_MSG(WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == 0, "child did not exit cleanly (status=%#x)", | ||
| 315 | + (unsigned int)wstatus); | ||
| 316 | + | ||
| 317 | + ASSERT_ERRNO_MSG(obmm_unexport(g_dev_id, 0) == 0, "obmm_unexport failed"); | ||
| 318 | + g_dev_id = OBMM_INVALID_MEMID; | ||
| 319 | + g_child_pid = -1; | ||
| 320 | + obmm_test_set_fatal_cleanup(NULL); | ||
| 321 | + | ||
| 322 | + return 0; | ||
| 323 | +} | ||
| 324 | + | ||
| 325 | +REGISTER_SINGLE_ACTOR_TEST_FUNC("ptrace_vma_access", "ptrace_ownership_consistency", ptrace_ownership_consistency_test, | ||
| 326 | + 60); | ||
| @@ -0,0 +1,201 @@ | |||
| 1 | +/* SPDX-License-Identifier: MulanPSL-2.0 */ | ||
| 2 | +/** | ||
| 3 | + * @file ptrace_read_write_only.c | ||
| 4 | + * @brief Verifies ptrace read on a PROT_WRITE-only OBMM shmdev VMA succeeds. | ||
| 5 | + * | ||
| 6 | + * Single-actor test validating the obmm_shmdev custom access handler | ||
| 7 | + * (vm_ops->access, obmm_vma_access) for cross-process ptrace access on | ||
| 8 | + * PFNMAP VMAs. The access handler grants a ptrace read when the target | ||
| 9 | + * VMA has VM_READ or VM_WRITE, so a PROT_WRITE-only mapping satisfies the | ||
| 10 | + * read permission check even though it carries no VM_READ flag. This | ||
| 11 | + * mirrors the ARM64 quirk that write-only user PTEs do not exist (a | ||
| 12 | + * write-only mapping is effectively readable) and keeps ptrace/GDB-based | ||
| 13 | + * inspection of OBMM mappings working. | ||
| 14 | + * | ||
| 15 | + * Mechanism note: process_vm_readv() cannot reach the access handler — | ||
| 16 | + * GUP rejects VM_IO|VM_PFNMAP VMAs outright — so this test uses | ||
| 17 | + * PTRACE_PEEKDATA, which routes through access_remote_vm() into | ||
| 18 | + * vm_ops->access, as suggested by the gap-analysis dependency table. | ||
| 19 | + * | ||
| 20 | + * Scenario (single machine, fork-based): | ||
| 21 | + * - Parent: exports one shmdev with ALLOW_MMAP (minimal size), forks, | ||
| 22 | + * registers a fatal cleanup that kills the child and unexports, then | ||
| 23 | + * waits for the child to report the mapped address over a pipe. | ||
| 24 | + * - Child: PTRACE_TRACEME, opens the shmdev O_RDWR, mmaps one page with | ||
| 25 | + * PROT_WRITE only, writes a known 8-byte pattern, reports the mapping | ||
| 26 | + * address over the pipe, and raises SIGSTOP. | ||
| 27 | + * - Parent: PTRACE_PEEKDATA reads the pattern word from the write-only | ||
| 28 | + * mapping and expects success with identical bytes; then SIGKILLs the | ||
| 29 | + * stopped child (the kernel auto-releases its fd and mapping) and | ||
| 30 | + * unexports the device. | ||
| 31 | + * | ||
| 32 | + * Success criteria: | ||
| 33 | + * - PROT_WRITE-only mmap on an O_RDWR shmdev fd succeeds | ||
| 34 | + * - PTRACE_PEEKDATA on the write-only VMA succeeds (errno 0) | ||
| 35 | + * - the peeked word matches the child-written pattern | ||
| 36 | + * | ||
| 37 | + * Preconditions: | ||
| 38 | + * - OBMM driver loaded with obmm_shmdev visible | ||
| 39 | + * - At least one NUMA node with available memory for export | ||
| 40 | + * - System page size obtainable via get_page_size() | ||
| 41 | + */ | ||
| 42 | + | ||
| 43 | +#include <errno.h> | ||
| 44 | +#include <fcntl.h> | ||
| 45 | +#include <signal.h> | ||
| 46 | +#include <stdint.h> | ||
| 47 | +#include <stdio.h> | ||
| 48 | +#include <string.h> | ||
| 49 | +#include <sys/mman.h> | ||
| 50 | +#include <sys/ptrace.h> | ||
| 51 | +#include <sys/types.h> | ||
| 52 | +#include <sys/wait.h> | ||
| 53 | +#include <unistd.h> | ||
| 54 | + | ||
| 55 | +#include "compat/libobmm-compat.h" | ||
| 56 | + | ||
| 57 | +#include "framework/obmm_test.h" | ||
| 58 | + | ||
| 59 | +#include "utils/export.h" | ||
| 60 | +#include "utils/obmm_dev.h" | ||
| 61 | +#include "utils/os_info.h" | ||
| 62 | + | ||
| 63 | +/* 8-byte pattern the child writes into its write-only mapping and the | ||
| 64 | + * parent expects to read back via PTRACE_PEEKDATA (memory image, endian | ||
| 65 | + * agnostic) */ | ||
| 66 | +static const uint8_t g_pattern[8] = {0x5A, 0xA5, 0x5A, 0xA5, 0x5A, 0xA5, 0x5A, 0xA5}; | ||
| 67 | + | ||
| 68 | +/* child exit codes for early-failure diagnosis */ | ||
| 69 | +#define CHILD_EXIT_TRACEME 10 | ||
| 70 | +#define CHILD_EXIT_PAGE_SIZE 11 | ||
| 71 | +#define CHILD_EXIT_OPEN 12 | ||
| 72 | +#define CHILD_EXIT_MMAP 13 | ||
| 73 | +#define CHILD_EXIT_PIPE 14 | ||
| 74 | + | ||
| 75 | +/* shared with the fatal cleanup hook; only the parent process uses them */ | ||
| 76 | +static pid_t g_child_pid = -1; | ||
| 77 | +static mem_id g_dev_id = OBMM_INVALID_MEMID; | ||
| 78 | + | ||
| 79 | +/* best-effort cleanup for the assertion path: must not use ASSERT macros */ | ||
| 80 | +static void kill_child_and_release_device(void) { | ||
| 81 | + if (g_child_pid > 0) { | ||
| 82 | + kill(g_child_pid, SIGKILL); | ||
| 83 | + waitpid(g_child_pid, NULL, 0); | ||
| 84 | + g_child_pid = -1; | ||
| 85 | + } | ||
| 86 | + if (g_dev_id != OBMM_INVALID_MEMID) { | ||
| 87 | + obmm_unexport(g_dev_id, 0); | ||
| 88 | + g_dev_id = OBMM_INVALID_MEMID; | ||
| 89 | + } | ||
| 90 | +} | ||
| 91 | + | ||
| 92 | +/* child role: TRACEME, mmap one page write-only, write the pattern, report | ||
| 93 | + * the mapping address, then stop for the parent's ptrace inspection. The | ||
| 94 | + * parent terminates this process after verification. */ | ||
| 95 | +static void run_write_only_child(int pipe_fd) { | ||
| 96 | + size_t page_size; | ||
| 97 | + int fd; | ||
| 98 | + void *addr; | ||
| 99 | + | ||
| 100 | + if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) != 0) | ||
| 101 | + _exit(CHILD_EXIT_TRACEME); | ||
| 102 | + | ||
| 103 | + if (get_page_size(&page_size) != 0) | ||
| 104 | + _exit(CHILD_EXIT_PAGE_SIZE); | ||
| 105 | + | ||
| 106 | + if (open_obmm_shmdev(&fd, g_dev_id, O_RDWR) != 0) | ||
| 107 | + _exit(CHILD_EXIT_OPEN); | ||
| 108 | + | ||
| 109 | + /* write-only mapping is accepted by the driver: validate_perm only | ||
| 110 | + * requires FMODE_WRITE for VM_WRITE, which O_RDWR provides */ | ||
| 111 | + addr = mmap(NULL, page_size, PROT_WRITE, MAP_SHARED, fd, 0); | ||
| 112 | + if (addr == MAP_FAILED) | ||
| 113 | + _exit(CHILD_EXIT_MMAP); | ||
| 114 | + | ||
| 115 | + memcpy(addr, g_pattern, sizeof(g_pattern)); | ||
| 116 | + | ||
| 117 | + if (write(pipe_fd, &addr, sizeof(addr)) != (ssize_t)sizeof(addr)) | ||
| 118 | + _exit(CHILD_EXIT_PIPE); | ||
| 119 | + | ||
| 120 | + raise(SIGSTOP); | ||
| 121 | + | ||
| 122 | + /* reached only if the parent resumed instead of killing this process */ | ||
| 123 | + munmap(addr, page_size); | ||
| 124 | + close(fd); | ||
| 125 | + _exit(0); | ||
| 126 | +} | ||
| 127 | + | ||
| 128 | +static int ptrace_read_write_only_test(void) { | ||
| 129 | + size_t lengths[OBMM_MAX_LOCAL_NUMA_NODES]; | ||
| 130 | + struct obmm_alloc_info alloc_info = {0}; | ||
| 131 | + struct obmm_mem_desc desc; | ||
| 132 | + int pipefd[2]; | ||
| 133 | + pid_t child; | ||
| 134 | + int wstatus; | ||
| 135 | + void *map_addr; | ||
| 136 | + long peeked; | ||
| 137 | + long expected; | ||
| 138 | + | ||
| 139 | + memset(&desc, 0, sizeof(desc)); | ||
| 140 | + alloc_info.policy = OBMM_ALLOC_MIN_MEM; | ||
| 141 | + alloc_info.scope.unit = OBMM_ALLOC_UNIT_HOST; | ||
| 142 | + alloc_info.scope.unit_id = 0; | ||
| 143 | + | ||
| 144 | + ASSERT_ERRNO_MSG(fill_obmm_export_length_array(lengths, &alloc_info) == 0, "fill_obmm_export_length_array failed"); | ||
| 145 | + ASSERT_ERRNO_MSG(fill_obmm_mem_desc_by_lengths(&desc, lengths) == 0, "fill_obmm_mem_desc_by_lengths failed"); | ||
| 146 | + errno = 0; | ||
| 147 | + g_dev_id = obmm_export(lengths, OBMM_EXPORT_FLAG_ALLOW_MMAP, &desc); | ||
| 148 | + ASSERT_ERRNO_MSG(g_dev_id != OBMM_INVALID_MEMID, "obmm_export failed"); | ||
| 149 | + | ||
| 150 | + ASSERT_ERRNO_MSG(pipe(pipefd) == 0, "pipe failed"); | ||
| 151 | + | ||
| 152 | + child = fork(); | ||
| 153 | + ASSERT_MSG(child >= 0, "fork failed: %s", strerror(errno)); | ||
| 154 | + | ||
| 155 | + if (child == 0) { | ||
| 156 | + close(pipefd[0]); | ||
| 157 | + run_write_only_child(pipefd[1]); | ||
| 158 | + _exit(0); | ||
| 159 | + } | ||
| 160 | + g_child_pid = child; | ||
| 161 | + obmm_test_set_fatal_cleanup(kill_child_and_release_device); | ||
| 162 | + | ||
| 163 | + close(pipefd[1]); | ||
| 164 | + ASSERT_ERRNO_MSG(read(pipefd[0], &map_addr, sizeof(map_addr)) == (ssize_t)sizeof(map_addr), | ||
| 165 | + "reading mapped address from child failed"); | ||
| 166 | + close(pipefd[0]); | ||
| 167 | + | ||
| 168 | + /* child must arrive in a SIGSTOP ptrace-stop with the pattern written */ | ||
| 169 | + errno = 0; | ||
| 170 | + ASSERT_ERRNO_MSG(waitpid(child, &wstatus, 0) == child, "waitpid failed"); | ||
| 171 | + ASSERT_MSG(WIFSTOPPED(wstatus) && WSTOPSIG(wstatus) == SIGSTOP, | ||
| 172 | + "child did not stop with SIGSTOP (exited=%d code=%d status=%#x)", WIFEXITED(wstatus), | ||
| 173 | + WIFEXITED(wstatus) ? WEXITSTATUS(wstatus) : -1, (unsigned int)wstatus); | ||
| 174 | + | ||
| 175 | + /* core assertion: ptrace read on a PROT_WRITE-only VMA must succeed — | ||
| 176 | + * obmm_vma_access grants a read when VM_READ or VM_WRITE is set */ | ||
| 177 | + errno = 0; | ||
| 178 | + peeked = ptrace(PTRACE_PEEKDATA, child, map_addr, NULL); | ||
| 179 | + ASSERT_ERRNO_MSG(errno == 0, "PTRACE_PEEKDATA on write-only VMA failed"); | ||
| 180 | + | ||
| 181 | + memcpy(&expected, g_pattern, sizeof(expected)); | ||
| 182 | + ASSERT_MSG(peeked == expected, "peeked word %#lx != expected pattern %#lx", (unsigned long)peeked, | ||
| 183 | + (unsigned long)expected); | ||
| 184 | + | ||
| 185 | + /* verification done — kill the stopped child; the kernel releases its | ||
| 186 | + * fd and mapping on process death */ | ||
| 187 | + ASSERT_ERRNO_MSG(kill(child, SIGKILL) == 0, "kill child failed"); | ||
| 188 | + errno = 0; | ||
| 189 | + ASSERT_ERRNO_MSG(waitpid(child, &wstatus, 0) == child, "waitpid after kill failed"); | ||
| 190 | + ASSERT_MSG(WIFSIGNALED(wstatus) && WTERMSIG(wstatus) == SIGKILL, "child not terminated by SIGKILL (status=%#x)", | ||
| 191 | + (unsigned int)wstatus); | ||
| 192 | + | ||
| 193 | + ASSERT_ERRNO_MSG(obmm_unexport(g_dev_id, 0) == 0, "obmm_unexport failed"); | ||
| 194 | + g_dev_id = OBMM_INVALID_MEMID; | ||
| 195 | + g_child_pid = -1; | ||
| 196 | + obmm_test_set_fatal_cleanup(NULL); | ||
| 197 | + | ||
| 198 | + return 0; | ||
| 199 | +} | ||
| 200 | + | ||
| 201 | +REGISTER_SINGLE_ACTOR_TEST_FUNC("ptrace_vma_access", "ptrace_read_write_only", ptrace_read_write_only_test, 60); | ||
这里是只有kp 950的需要过滤,其他的不用过滤。 同时新增一个issue,整理哪些用例需要单路径/多路径都覆盖