On 5/9/22 9:00 PM, Suren Baghdasaryan wrote:
Introduce process_mrelease syscall sanity tests. They include tests of invalid pidfd and flags inputs, attempting to call process_mrelease with a live process and a valid usage of process_mrelease. Because process_mrelease has to be used against a process with a pending SIGKILL, it's possible that the process exits before process_mrelease gets called. In such cases we retry the test with a victim that allocates twice more memory up to 1GB. This would require the victim process to spend more time during exit and process_mrelease has a better chance of catching the process before it exits.
+1 on Mike's comments on improving the change log. List what is getting tested as opposed to describing the test code.
Signed-off-by: Suren Baghdasaryan surenb@google.com
tools/testing/selftests/vm/Makefile | 1 + tools/testing/selftests/vm/mrelease_test.c | 176 +++++++++++++++++++++ tools/testing/selftests/vm/run_vmtests.sh | 16 ++ 3 files changed, 193 insertions(+) create mode 100644 tools/testing/selftests/vm/mrelease_test.c
Please update .gitignore with the new executable.
diff --git a/tools/testing/selftests/vm/Makefile b/tools/testing/selftests/vm/Makefile index 04a49e876a46..733fccbff0ef 100644 --- a/tools/testing/selftests/vm/Makefile +++ b/tools/testing/selftests/vm/Makefile @@ -43,6 +43,7 @@ TEST_GEN_FILES += map_populate TEST_GEN_FILES += memfd_secret TEST_GEN_FILES += mlock-random-test TEST_GEN_FILES += mlock2-tests +TEST_GEN_FILES += mrelease_test TEST_GEN_FILES += mremap_dontunmap TEST_GEN_FILES += mremap_test TEST_GEN_FILES += on-fault-limit diff --git a/tools/testing/selftests/vm/mrelease_test.c b/tools/testing/selftests/vm/mrelease_test.c new file mode 100644 index 000000000000..a61061bf8433 --- /dev/null +++ b/tools/testing/selftests/vm/mrelease_test.c @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: GPL-2.0 +/*
- Copyright 2022 Google LLC
- */
+#define _GNU_SOURCE +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/wait.h> +#include <unistd.h>
+#include "util.h"
+static inline int pidfd_open(pid_t pid, unsigned int flags) +{ +#ifdef __NR_pidfd_open
- return syscall(__NR_pidfd_open, pid, flags);
+#else
- errno = ENOSYS;
This isn't an error - this would be skip because this syscall isn't supported.
- return -1;
+#endif
Key off of syscall return instead of these ifdefs - same comment on all of the ifdefs
+}
I am not seeing any reason for breaking this code up have a separate routine for pidfd_open().
+static inline int process_mrelease(int pidfd, unsigned int flags) +{ +#ifdef __NR_process_mrelease
- return syscall(__NR_process_mrelease, pidfd, flags);
+#else
- errno = ENOSYS;
- return -1;
+#endif> +}
Same comments on ifdefs and skips here as well.
+static void write_fault_pages(char *addr, unsigned long nr_pages) +{
- unsigned long i;
- for (i = 0; i < nr_pages; i++)
*((unsigned long *)(addr + (i * PAGE_SIZE))) = i;
+}
+static int alloc_noexit(unsigned long nr_pages, int pipefd) +{
- int ppid = getppid();
- void *buf;
- buf = mmap(NULL, nr_pages * PAGE_SIZE, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANON, 0, 0);
- if (buf == MAP_FAILED) {
perror("mmap");
A bit more descriptive message what the test would do will be helpful. Also consider if this should be a skip or fail for the test.
return 1;
- }
- write_fault_pages((char *)buf, nr_pages);
- /* Signal the parent that the child is ready */
- if (write(pipefd, "", 1) < 0) {
perror("write");
return 1;
- }
- /* Wait to be killed (when reparenting happens) */
- while (getppid() == ppid)
sleep(1);
What happens if reparenting doesn't happen? Will this loop for ever? This test could hang?
- munmap(buf, nr_pages * PAGE_SIZE);
- return 0;
+}
+#define MB(x) (x << 20) +#define MAX_SIZE_MB 1024
+int main(void) +{
- int res;
- int pipefd[2], pidfd;
- pid_t pid;
- char byte;
- size_t size;
- int negative_tests_done = 0;
- /* Test a wrong pidfd */
- if (!process_mrelease(-1, 0) || errno != EBADF) {
perror("process_mrelease with wring pidfd");
Incorrect spelling "wring/wrong"
exit(1);
- }
- /*
* Start the test with 1MB allocation and double every time
* process_mrelease fails
*/
- for (size = 1; size <= MAX_SIZE_MB; size *= 2) {
/*
* Pipe for the child to signal when it's done allocating
* memory
*/
if (pipe(pipefd)) {
perror("pipe");
exit(1);
}
pid = fork();
if (pid < 0) {
perror("fork");
Close the pipe?
exit(1);
}
if (pid == 0) {
close(pipefd[0]);
res = alloc_noexit(MB(size) / PAGE_SIZE, pipefd[1]);
close(pipefd[1]);
exit(res);
}
close(pipefd[1]);
/* Block until the child is ready */
res = read(pipefd[0], &byte, 1);
close(pipefd[0]);
if (res < 0) {
perror("read");
exit(1);
}
pidfd = pidfd_open(pid, 0);
if (pidfd < 0) {
perror("pidfd_open");
exit(1);
}
The code is very hard to read. Add comments to indicate parent and child paths clearly so reviewers can follow the logic and be able to do effective review.
/* Run negative tests which require a valid child only once */
if (!negative_tests_done) {
/* Test invalid flags */
if (!process_mrelease(pidfd, (unsigned int)-1) ||
errno != EINVAL) {
perror("process_mrelease with wrong flags");
exit(1);
So is this an expected fail or a test fail?
}
/* Test reapling while process is still alive */
if (!process_mrelease(pidfd, 0) ||
errno != EINVAL) {
perror("process_mrelease on a live process");
So is this an expected fail or a test fail?
exit(1);
}
negative_tests_done = 1;
}
Now the above negative_tests_done block could be in a separate function --- All the others aren't really needed. It will be good for abstraction and readability.
if (kill(pid, SIGKILL)) {
perror("kill");
Include test results in the change log - so we can see the test report.
exit(1);
}
if (!process_mrelease(pidfd, 0)) {
/* Terminate the test once process_mrelease succeeds */
return 0;
}
/*
* Ignore the failure if the child exited before mrelease got
* called, increase allocation size and retry the test
*/
Add more info. on why allocating more memory helps.
if (errno != ESRCH) {
perror("process_mrelease");
exit(1);
}
if (waitpid(pid, NULL, 0) < 0) {
perror("waitpid");
exit(1);
}
close(pidfd);
- }
- printf("All process_mrelease attempts failed!\n");
- exit(1);
+} diff --git a/tools/testing/selftests/vm/run_vmtests.sh b/tools/testing/selftests/vm/run_vmtests.sh index 352ba00cf26b..1986162fea39 100755 --- a/tools/testing/selftests/vm/run_vmtests.sh +++ b/tools/testing/selftests/vm/run_vmtests.sh @@ -287,6 +287,22 @@ else echo "[PASS]" fi +echo "---------------------" +echo "running mrelease_test" +echo "---------------------" +./mrelease_test +ret_val=$?
+if [ $ret_val -eq 0 ]; then
- echo "[PASS]"
+elif [ $ret_val -eq $ksft_skip ]; then
echo "[SKIP]"
exitcode=$ksft_skip
+else
- echo "[FAIL]"
- exitcode=1
+fi
- echo "-------------------" echo "running mremap_test" echo "-------------------"
In general, the code flow is hard to read to make sure resources are released e.g: pipefd in all the error paths. The code is broken up into smaller chunks where it isn't needed in some cases and left as a large block when it could benefit from abstraction e.g: negative test block.
Please make changes and send v2.
thanks, -- Shuah