| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Copyright 2022 Google LLC |
| */ |
| #define _GNU_SOURCE |
| #include <errno.h> |
| #include <stdbool.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <sys/syscall.h> |
| #include <sys/wait.h> |
| #include <unistd.h> |
| #include <asm-generic/unistd.h> |
| #include "vm_util.h" |
| #include "../kselftest.h" |
| |
| #define MB(x) (x << 20) |
| #define MAX_SIZE_MB 1024 |
| |
| static int alloc_noexit(unsigned long nr_pages, int pipefd) |
| { |
| int ppid = getppid(); |
| int timeout = 10; /* 10sec timeout to get killed */ |
| unsigned long i; |
| char *buf; |
| |
| buf = (char *)mmap(NULL, nr_pages * psize(), PROT_READ | PROT_WRITE, |
| MAP_PRIVATE | MAP_ANON, 0, 0); |
| if (buf == MAP_FAILED) { |
| perror("mmap failed, halting the test"); |
| return KSFT_FAIL; |
| } |
| |
| for (i = 0; i < nr_pages; i++) |
| *((unsigned long *)(buf + (i * psize()))) = i; |
| |
| /* Signal the parent that the child is ready */ |
| if (write(pipefd, "", 1) < 0) { |
| perror("write"); |
| return KSFT_FAIL; |
| } |
| |
| /* Wait to be killed (when reparenting happens) */ |
| while (getppid() == ppid && timeout > 0) { |
| sleep(1); |
| timeout--; |
| } |
| |
| munmap(buf, nr_pages * psize()); |
| |
| return (timeout > 0) ? KSFT_PASS : KSFT_FAIL; |
| } |
| |
| /* The process_mrelease calls in this test are expected to fail */ |
| static void run_negative_tests(int pidfd) |
| { |
| int res; |
| /* Test invalid flags. Expect to fail with EINVAL error code. */ |
| if (!syscall(__NR_process_mrelease, pidfd, (unsigned int)-1) || |
| errno != EINVAL) { |
| res = (errno == ENOSYS ? KSFT_SKIP : KSFT_FAIL); |
| perror("process_mrelease with wrong flags"); |
| exit(res); |
| } |
| /* |
| * Test reaping while process is alive with no pending SIGKILL. |
| * Expect to fail with EINVAL error code. |
| */ |
| if (!syscall(__NR_process_mrelease, pidfd, 0) || errno != EINVAL) { |
| res = (errno == ENOSYS ? KSFT_SKIP : KSFT_FAIL); |
| perror("process_mrelease on a live process"); |
| exit(res); |
| } |
| } |
| |
| static int child_main(int pipefd[], size_t size) |
| { |
| int res; |
| |
| /* Allocate and fault-in memory and wait to be killed */ |
| close(pipefd[0]); |
| res = alloc_noexit(MB(size) / psize(), pipefd[1]); |
| close(pipefd[1]); |
| return res; |
| } |
| |
| int main(void) |
| { |
| int pipefd[2], pidfd; |
| bool success, retry; |
| size_t size; |
| pid_t pid; |
| char byte; |
| int res; |
| |
| /* Test a wrong pidfd */ |
| if (!syscall(__NR_process_mrelease, -1, 0) || errno != EBADF) { |
| res = (errno == ENOSYS ? KSFT_SKIP : KSFT_FAIL); |
| perror("process_mrelease with wrong pidfd"); |
| exit(res); |
| } |
| |
| /* Start the test with 1MB child memory allocation */ |
| size = 1; |
| retry: |
| /* |
| * Pipe for the child to signal when it's done allocating |
| * memory |
| */ |
| if (pipe(pipefd)) { |
| perror("pipe"); |
| exit(KSFT_FAIL); |
| } |
| pid = fork(); |
| if (pid < 0) { |
| perror("fork"); |
| close(pipefd[0]); |
| close(pipefd[1]); |
| exit(KSFT_FAIL); |
| } |
| |
| if (pid == 0) { |
| /* Child main routine */ |
| res = child_main(pipefd, size); |
| exit(res); |
| } |
| |
| /* |
| * Parent main routine: |
| * Wait for the child to finish allocations, then kill and reap |
| */ |
| close(pipefd[1]); |
| /* Block until the child is ready */ |
| res = read(pipefd[0], &byte, 1); |
| close(pipefd[0]); |
| if (res < 0) { |
| perror("read"); |
| if (!kill(pid, SIGKILL)) |
| waitpid(pid, NULL, 0); |
| exit(KSFT_FAIL); |
| } |
| |
| pidfd = syscall(__NR_pidfd_open, pid, 0); |
| if (pidfd < 0) { |
| perror("pidfd_open"); |
| if (!kill(pid, SIGKILL)) |
| waitpid(pid, NULL, 0); |
| exit(KSFT_FAIL); |
| } |
| |
| /* Run negative tests which require a live child */ |
| run_negative_tests(pidfd); |
| |
| if (kill(pid, SIGKILL)) { |
| res = (errno == ENOSYS ? KSFT_SKIP : KSFT_FAIL); |
| perror("kill"); |
| exit(res); |
| } |
| |
| success = (syscall(__NR_process_mrelease, pidfd, 0) == 0); |
| if (!success) { |
| /* |
| * If we failed to reap because the child exited too soon, |
| * before we could call process_mrelease. Double child's memory |
| * which causes it to spend more time on cleanup and increases |
| * our chances of reaping its memory before it exits. |
| * Retry until we succeed or reach MAX_SIZE_MB. |
| */ |
| if (errno == ESRCH) { |
| retry = (size <= MAX_SIZE_MB); |
| } else { |
| res = (errno == ENOSYS ? KSFT_SKIP : KSFT_FAIL); |
| perror("process_mrelease"); |
| waitpid(pid, NULL, 0); |
| exit(res); |
| } |
| } |
| |
| /* Cleanup to prevent zombies */ |
| if (waitpid(pid, NULL, 0) < 0) { |
| perror("waitpid"); |
| exit(KSFT_FAIL); |
| } |
| close(pidfd); |
| |
| if (!success) { |
| if (retry) { |
| size *= 2; |
| goto retry; |
| } |
| printf("All process_mrelease attempts failed!\n"); |
| exit(KSFT_FAIL); |
| } |
| |
| printf("Success reaping a child with %zuMB of memory allocations\n", |
| size); |
| return KSFT_PASS; |
| } |