| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * It tests the mlock/mlock2() when they are invoked |
| * on randomly memory region. |
| */ |
| #include <unistd.h> |
| #include <sys/resource.h> |
| #include <sys/capability.h> |
| #include <sys/mman.h> |
| #include <fcntl.h> |
| #include <string.h> |
| #include <sys/ipc.h> |
| #include <sys/shm.h> |
| #include <time.h> |
| #include "mlock2.h" |
| |
| #define CHUNK_UNIT (128 * 1024) |
| #define MLOCK_RLIMIT_SIZE (CHUNK_UNIT * 2) |
| #define MLOCK_WITHIN_LIMIT_SIZE CHUNK_UNIT |
| #define MLOCK_OUTOF_LIMIT_SIZE (CHUNK_UNIT * 3) |
| |
| #define TEST_LOOP 100 |
| #define PAGE_ALIGN(size, ps) (((size) + ((ps) - 1)) & ~((ps) - 1)) |
| |
| int set_cap_limits(rlim_t max) |
| { |
| struct rlimit new; |
| cap_t cap = cap_init(); |
| |
| new.rlim_cur = max; |
| new.rlim_max = max; |
| if (setrlimit(RLIMIT_MEMLOCK, &new)) { |
| perror("setrlimit() returns error\n"); |
| return -1; |
| } |
| |
| /* drop capabilities including CAP_IPC_LOCK */ |
| if (cap_set_proc(cap)) { |
| perror("cap_set_proc() returns error\n"); |
| return -2; |
| } |
| |
| return 0; |
| } |
| |
| int get_proc_locked_vm_size(void) |
| { |
| FILE *f; |
| int ret = -1; |
| char line[1024] = {0}; |
| unsigned long lock_size = 0; |
| |
| f = fopen("/proc/self/status", "r"); |
| if (!f) { |
| perror("fopen"); |
| return -1; |
| } |
| |
| while (fgets(line, 1024, f)) { |
| if (strstr(line, "VmLck")) { |
| ret = sscanf(line, "VmLck:\t%8lu kB", &lock_size); |
| if (ret <= 0) { |
| printf("sscanf() on VmLck error: %s: %d\n", |
| line, ret); |
| fclose(f); |
| return -1; |
| } |
| fclose(f); |
| return (int)(lock_size << 10); |
| } |
| } |
| |
| perror("cann't parse VmLck in /proc/self/status\n"); |
| fclose(f); |
| return -1; |
| } |
| |
| /* |
| * Get the MMUPageSize of the memory region including input |
| * address from proc file. |
| * |
| * return value: on error case, 0 will be returned. |
| * Otherwise the page size(in bytes) is returned. |
| */ |
| int get_proc_page_size(unsigned long addr) |
| { |
| FILE *smaps; |
| char *line; |
| unsigned long mmupage_size = 0; |
| size_t size; |
| |
| smaps = seek_to_smaps_entry(addr); |
| if (!smaps) { |
| printf("Unable to parse /proc/self/smaps\n"); |
| return 0; |
| } |
| |
| while (getline(&line, &size, smaps) > 0) { |
| if (!strstr(line, "MMUPageSize")) { |
| free(line); |
| line = NULL; |
| size = 0; |
| continue; |
| } |
| |
| /* found the MMUPageSize of this section */ |
| if (sscanf(line, "MMUPageSize: %8lu kB", |
| &mmupage_size) < 1) { |
| printf("Unable to parse smaps entry for Size:%s\n", |
| line); |
| break; |
| } |
| |
| } |
| free(line); |
| if (smaps) |
| fclose(smaps); |
| return mmupage_size << 10; |
| } |
| |
| /* |
| * Test mlock/mlock2() on provided memory chunk. |
| * It expects the mlock/mlock2() to be successful (within rlimit) |
| * |
| * With allocated memory chunk [p, p + alloc_size), this |
| * test will choose start/len randomly to perform mlock/mlock2 |
| * [start, start + len] memory range. The range is within range |
| * of the allocated chunk. |
| * |
| * The memory region size alloc_size is within the rlimit. |
| * So we always expect a success of mlock/mlock2. |
| * |
| * VmLck is assumed to be 0 before this test. |
| * |
| * return value: 0 - success |
| * else: failure |
| */ |
| int test_mlock_within_limit(char *p, int alloc_size) |
| { |
| int i; |
| int ret = 0; |
| int locked_vm_size = 0; |
| struct rlimit cur; |
| int page_size = 0; |
| |
| getrlimit(RLIMIT_MEMLOCK, &cur); |
| if (cur.rlim_cur < alloc_size) { |
| printf("alloc_size[%d] < %u rlimit,lead to mlock failure\n", |
| alloc_size, (unsigned int)cur.rlim_cur); |
| return -1; |
| } |
| |
| srand(time(NULL)); |
| for (i = 0; i < TEST_LOOP; i++) { |
| /* |
| * - choose mlock/mlock2 randomly |
| * - choose lock_size randomly but lock_size < alloc_size |
| * - choose start_offset randomly but p+start_offset+lock_size |
| * < p+alloc_size |
| */ |
| int is_mlock = !!(rand() % 2); |
| int lock_size = rand() % alloc_size; |
| int start_offset = rand() % (alloc_size - lock_size); |
| |
| if (is_mlock) |
| ret = mlock(p + start_offset, lock_size); |
| else |
| ret = mlock2_(p + start_offset, lock_size, |
| MLOCK_ONFAULT); |
| |
| if (ret) { |
| printf("%s() failure at |%p(%d)| mlock:|%p(%d)|\n", |
| is_mlock ? "mlock" : "mlock2", |
| p, alloc_size, |
| p + start_offset, lock_size); |
| return ret; |
| } |
| } |
| |
| /* |
| * Check VmLck left by the tests. |
| */ |
| locked_vm_size = get_proc_locked_vm_size(); |
| page_size = get_proc_page_size((unsigned long)p); |
| if (page_size == 0) { |
| printf("cannot get proc MMUPageSize\n"); |
| return -1; |
| } |
| |
| if (locked_vm_size > PAGE_ALIGN(alloc_size, page_size) + page_size) { |
| printf("test_mlock_within_limit() left VmLck:%d on %d chunk\n", |
| locked_vm_size, alloc_size); |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| |
| /* |
| * We expect the mlock/mlock2() to be fail (outof limitation) |
| * |
| * With allocated memory chunk [p, p + alloc_size), this |
| * test will randomly choose start/len and perform mlock/mlock2 |
| * on [start, start+len] range. |
| * |
| * The memory region size alloc_size is above the rlimit. |
| * And the len to be locked is higher than rlimit. |
| * So we always expect a failure of mlock/mlock2. |
| * No locked page number should be increased as a side effect. |
| * |
| * return value: 0 - success |
| * else: failure |
| */ |
| int test_mlock_outof_limit(char *p, int alloc_size) |
| { |
| int i; |
| int ret = 0; |
| int locked_vm_size = 0, old_locked_vm_size = 0; |
| struct rlimit cur; |
| |
| getrlimit(RLIMIT_MEMLOCK, &cur); |
| if (cur.rlim_cur >= alloc_size) { |
| printf("alloc_size[%d] >%u rlimit, violates test condition\n", |
| alloc_size, (unsigned int)cur.rlim_cur); |
| return -1; |
| } |
| |
| old_locked_vm_size = get_proc_locked_vm_size(); |
| srand(time(NULL)); |
| for (i = 0; i < TEST_LOOP; i++) { |
| int is_mlock = !!(rand() % 2); |
| int lock_size = (rand() % (alloc_size - cur.rlim_cur)) |
| + cur.rlim_cur; |
| int start_offset = rand() % (alloc_size - lock_size); |
| |
| if (is_mlock) |
| ret = mlock(p + start_offset, lock_size); |
| else |
| ret = mlock2_(p + start_offset, lock_size, |
| MLOCK_ONFAULT); |
| if (ret == 0) { |
| printf("%s() succeeds? on %p(%d) mlock%p(%d)\n", |
| is_mlock ? "mlock" : "mlock2", |
| p, alloc_size, |
| p + start_offset, lock_size); |
| return -1; |
| } |
| } |
| |
| locked_vm_size = get_proc_locked_vm_size(); |
| if (locked_vm_size != old_locked_vm_size) { |
| printf("tests leads to new mlocked page: old[%d], new[%d]\n", |
| old_locked_vm_size, |
| locked_vm_size); |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| int main(int argc, char **argv) |
| { |
| char *p = NULL; |
| int ret = 0; |
| |
| if (set_cap_limits(MLOCK_RLIMIT_SIZE)) |
| return -1; |
| |
| p = malloc(MLOCK_WITHIN_LIMIT_SIZE); |
| if (p == NULL) { |
| perror("malloc() failure\n"); |
| return -1; |
| } |
| ret = test_mlock_within_limit(p, MLOCK_WITHIN_LIMIT_SIZE); |
| if (ret) |
| return ret; |
| munlock(p, MLOCK_WITHIN_LIMIT_SIZE); |
| free(p); |
| |
| |
| p = malloc(MLOCK_OUTOF_LIMIT_SIZE); |
| if (p == NULL) { |
| perror("malloc() failure\n"); |
| return -1; |
| } |
| ret = test_mlock_outof_limit(p, MLOCK_OUTOF_LIMIT_SIZE); |
| if (ret) |
| return ret; |
| munlock(p, MLOCK_OUTOF_LIMIT_SIZE); |
| free(p); |
| |
| return 0; |
| } |