已开启
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
已开启
gao-tianxiong创建于 26 天前
共 33 个文件变更+3581-127
A.agents/skills/obmm-utility-helper/SKILL.md+225-0文件内容审核中,请稍后刷新重试
Mdoc/README.md+4-0文件内容审核中,请稍后刷新重试
@@ -556,7 +556,9 @@ Current utility headers:
556- `utils/numa_setup.h`: NUMA-backed user-memory mapping and migration556- `utils/numa_setup.h`: NUMA-backed user-memory mapping and migration
557 helpers (libnuma allocation, mmap+move_pages, mmap+mbind, with optional557 helpers (libnuma allocation, mmap+move_pages, mmap+mbind, with optional
558 read/write faulting before migration); pair `map_numa_memory()` with558 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 complex562- `utils/param_fmt.h`: parameter formatting helpers for formatting complex
561 types and function-call parameters in error output; provides sprintf-style563 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
120procfs, address query API round-trip, `/proc/pid/maps` display, and121procfs, address query API round-trip, `/proc/pid/maps` display, and
121sysfs attribute reads.122sysfs 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 Tests135### `bugfix` — Regression Tests
124 136 
125Regression tests guarding against previously-fixed bugs: open/mmap137Regression tests guarding against previously-fixed bugs: open/mmap
Minclude/tests/import_test_common.h+7-6文件内容审核中,请稍后刷新重试
@@ -97,6 +97,23 @@ int map_numa_memory(void **ptr, size_t size, unsigned int numa_id, enum numa_map
97 */97 */
98int unmap_numa_memory(void *ptr, size_t size, enum numa_map_method method);98int 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 node118 * @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 loading28 * @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-specific50 * 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 and59 * Bit-alignment invariant: the generic ubmem_decoder_config_flag bits and
54 * the KP950 UBMEM_DECODER_CONFIG_KP950_FLAG_* bits share identical60 * the KP950 UBMEM_DECODER_CONFIG_KP950_FLAG_* bits share identical
55 * positions (1<<0..1<<5), so a direct OR of the validated generic bits61 * 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}
Mtests/obmm/consistency/atomic.c+3-18文件内容审核中,请稍后刷新重试
Mtests/obmm/consistency/lock.c+0-15文件内容审核中,请稍后刷新重试
@@ -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
Yyuhao_zhang9 天前
已过期

这里是只有kp 950的需要过滤,其他的不用过滤。 同时新增一个issue,整理哪些用例需要单路径/多路径都覆盖

likedislike
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- 
753static int nrt_test(void) {740static 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 = &param_set->params[i];757 const struct nrt_test_param *p = &param_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;
Mtests/obmm/consistency/producer_consumer.c+0-15文件内容审核中,请稍后刷新重试
Mtests/obmm/consistency/producer_consumer_wo.c+0-15文件内容审核中,请稍后刷新重试
@@ -301,8 +301,8 @@ static int rr_run_exporter(const struct rr_test_param *param, size_t export_size
301 LOG_DEBUG("rr_run_exporter: round %u/%u worker=%u turn=%u", round + 1, param->num_rounds, worker_id,301 LOG_DEBUG("rr_run_exporter: round %u/%u worker=%u turn=%u", round + 1, param->num_rounds, worker_id,
302 turn_index);302 turn_index);
303 303 
304- ret = rr_do_turn(memid, sysfs_size, access_offset, access_size, param->exporter_cacheable, pmd_size,304+ ret =
305- turn_index);305+ rr_do_turn(memid, sysfs_size, access_offset, access_size, param->exporter_cacheable, pmd_size, turn_index);
306 ASSERT_MSG(ret == 0, "rr_run_exporter: rr_do_turn failed for %s round %u", param->name, round + 1);306 ASSERT_MSG(ret == 0, "rr_run_exporter: rr_do_turn failed for %s round %u", param->name, round + 1);
307 307 
308 /* Signal importer that exporter turn is done */308 /* Signal importer that exporter turn is done */
@@ -402,8 +402,8 @@ static int rr_run_importer(const struct rr_test_param *param, size_t access_size
402 LOG_DEBUG("rr_run_importer: round %u/%u worker=%u turn=%u", round + 1, param->num_rounds, worker_id,402 LOG_DEBUG("rr_run_importer: round %u/%u worker=%u turn=%u", round + 1, param->num_rounds, worker_id,
403 turn_index);403 turn_index);
404 404 
405- ret = rr_do_turn(memid, sysfs_size, access_offset, access_size, param->importer_cacheable, pmd_size,405+ ret =
406- turn_index);406+ rr_do_turn(memid, sysfs_size, access_offset, access_size, param->importer_cacheable, pmd_size, turn_index);
407 ASSERT_MSG(ret == 0, "rr_run_importer: rr_do_turn failed for %s round %u", param->name, round + 1);407 ASSERT_MSG(ret == 0, "rr_run_importer: rr_do_turn failed for %s round %u", param->name, round + 1);
408 408 
409 /* Signal exporter that importer turn is done */409 /* Signal exporter that importer turn is done */
@@ -12,6 +12,8 @@
12 * - mmap-perm-combos: open-flags x mmap-prot permission combinations12 * - mmap-perm-combos: open-flags x mmap-prot permission combinations
13 * - mmap-partial-offset: partial mmap with non-zero offset and VMA size check13 * - 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 step19 * 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+ */
207static int verify_rdonly_write_rejected(mem_id id) {255static 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);
Mtests/obmm/mmap_and_ownership/cache_infer.c+168-16文件内容审核中,请稍后刷新重试
@@ -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 half13 * 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+size15 * 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+ 
166static int pmd_mmap_test(void) {455static 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);
Atests/obmm/obmm_import_api/mmap_o_sync_mem_cap_check.c+166-0文件内容审核中,请稍后刷新重试
@@ -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 mappings12+ * - proc-vmflags: VmFlags in smaps contains "rd" for RW and RO mappings, and
13- * - proc-kernel-page-size: KernelPageSize is 4 KB or 2 MB13+ * "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 mmap16 * - proc-perm-display: /proc/pid/maps shows "rw-s" for RW mmap
15 * - proc-update-range: UPDATE_RANGE RO transition preserves data, restore to RW17 * - proc-update-range: UPDATE_RANGE RO transition preserves data, restore to RW
16 * - proc-cacheable-display: cacheable mmap VMA display in /proc/pid/maps18 * - 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 */
97static int verify_proc_kernel_page_size(mem_id id) {103static 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(&region_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);