Re: RFC: spurious UFFD_EVENT_FORK with pending signals

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Sat, Oct 07, 2017 at 05:16:09PM +0200, Andrea Arcangeli wrote:
> Hello,
> 
> I noticed that the addition of a SIGLARM to the selftest broke the
> selftest in how it handles UFFD_EVENT_FORK because of an undocumented
> UFFD_EVENT_FORK behavior. The testcase doesn't expect "spurious" fork
> events to be received.
> 
> As result two or more child uffds may be received by the monitor
> during a single fork() invocation of the parent (only the last uffd
> will be for the real child, the previous are all spurious associated
> with an mm with mm_users = 0). The monitor thread because it's a
> selftest, it expects deterministic behavior so when it receives a
> spurious uffd, it thinks it received the real uffd of the child and so
> it closes the parent uffd and just listens to such spurious uffd. But
> such spurious uffd is not the child uffd and when the parent uffd is
> closed the real child runs without userfaultfd monitoring (resulting
> in immediate corruption being detected in the child).
> 
> The source of the spurious child uffds is this check in kernel/fork.c:
> 
> 	recalc_sigpending();
> 	if (signal_pending(current)) {
> 		retval = -ERESTARTNOINTR;
> 		goto bad_fork_cancel_cgroup;
> 
> In practice this isn't a problem for CRIU or any other real non
> cooperative use case, because the parent uffd could never be closed
> (the only possible concern about this detail, would be if CRIU could
> run out of file descriptors in presence of signal flood, but even that
> would be a graceful failure with no memory corruption possible).

Indeed in CRIU we don't close the parent uffd and a couple of spurious
UFFD_EVENT_FORK won't cause a real problem. Yet, if we'll run out of file
descriptors because of signal flood during migration, even with graceful
failure we'd loose the migrated process entirely.
 
> We don't have a userfaultfd_exit hook to send a POLLHUP when the mm is
> destroyed. If we had that, the spurious uffd could be collected and
> release the file handle in the userfaultfd monitor fd space. To send
> such POLLHUP we'd need to queue all userfaultfd_ctx in a list in the
> mm_struct.
> 
> The other possible solution to the possible concern of running out of
> file descriptors in the CRIU userfaultfd monitor, is to simply prevent
> the generation of the spurious uffds and in turn removing this
> detail. That's not hard but that would move uffd structures way up
> into the callers so the UFFD_EVENT_FORK is only delivered after the
> above signal_pending check.

Currently userfault related code in fork.c neatly fits into dup_mmap() and
moving the uffd structures up into the callers would be ugly :(

However, calling dup_userfaultfd_complete() in dup_mmap() will cause
spurious UFFD_EVENT_FORK if fork() fails at any point after copy_mm().

Maybe we do need to queue all duplicated userfaultfd_ctx in the mm_struct
of the child process and call dup_userfaultfd_complete() closer to the end
of copy_process().
 
I'm going to experiment with the list of userfaultfd_ctx in mm_struct, it
seems to me that it may have additional value, e.g. to simplify
userfaultfd_event_wait_completion(). I'll need a bit of time to see if I'm
not talking complete nonsense :)

> For now I added an easy reproducer to the testcase and sigprocmask
> SIG_BLOCK/UNBLOCK around fork() to verify that such a change restores
> all "cooperative" expectations of the "non-cooperative" part of the
> testcase.
> 
> This survived fine a 12+ hour load with 4 selftests in parallel
> (shmem, hugetlb, hugetlb_shared, anon).
> 
> I found out about this detail the first time with heavy host CPU over
> commit that enlarged the guest fork runtime long enough for a SIGALARM
> to hit it and it wasn't easy to reproduce until I added the signal
> flooder to the selftest.
> 
> I'll cleanup and submit the below selftest fix shortly with a separate
> submit, but here the question is if we want to make any change to
> UFFD_EVENT_FORK for this signal issue.
> 
> If we change anything in the UFFD_EVENT_FORK processing for this, then
> the sigprocmask calls and the two #defines should be dropped from the
> selftest so the signal flooder will then validate any kernel change
> done for it.
> 
> If we change nothing then this detail should probably be documented
> just in case somebody has deterministic expectations for the
> non-cooperative features (like the selftest has).
> 
> Thanks,
> Andrea
> 
> diff --git a/tools/testing/selftests/vm/userfaultfd.c b/tools/testing/selftests/vm/userfaultfd.c
> index de2f9ec8a87f..3f7b6e91050c 100644
> --- a/tools/testing/selftests/vm/userfaultfd.c
> +++ b/tools/testing/selftests/vm/userfaultfd.c
> @@ -69,6 +69,9 @@
>  #include <setjmp.h>
>  #include <stdbool.h>
> 
> +//#define REPRODUCE_SPURIOUS_UFFD_EVENT_FORK_IN_SIG_TEST
> +//#define REPRODUCE_SPURIOUS_UFFD_EVENT_FORK_IN_EVENTS_TEST
> +
>  #ifdef __NR_userfaultfd
> 
>  static unsigned long nr_cpus, nr_pages, nr_pages_per_cpu, page_size;
> @@ -159,8 +162,7 @@ static void hugetlb_allocate_area(void **alloc_area)
>  	void *area_alias = NULL;
>  	char **alloc_area_alias;
>  	*alloc_area = mmap(NULL, nr_pages * page_size, PROT_READ | PROT_WRITE,
> -			   (map_shared ? MAP_SHARED : MAP_PRIVATE) |
> -			   MAP_HUGETLB,
> +			   (map_shared ? MAP_SHARED : MAP_PRIVATE),
>  			   huge_fd, *alloc_area == area_src ? 0 :
>  			   nr_pages * page_size);
>  	if (*alloc_area == MAP_FAILED) {
> @@ -170,7 +172,7 @@ static void hugetlb_allocate_area(void **alloc_area)
> 
>  	if (map_shared) {
>  		area_alias = mmap(NULL, nr_pages * page_size, PROT_READ | PROT_WRITE,
> -				  MAP_SHARED | MAP_HUGETLB,
> +				  MAP_SHARED,
>  				  huge_fd, *alloc_area == area_src ? 0 :
>  				  nr_pages * page_size);
>  		if (area_alias == MAP_FAILED) {
> @@ -457,8 +459,11 @@ static void *uffd_poll_thread(void *arg)
>  		ret = poll(pollfd, 2, -1);
>  		if (!ret)
>  			fprintf(stderr, "poll error %d\n", ret), exit(1);
> -		if (ret < 0)
> +		if (ret < 0) {
> +			if (errno == EINTR)
> +				continue;
>  			perror("poll"), exit(1);
> +		}
>  		if (pollfd[1].revents & POLLIN) {
>  			if (read(pollfd[1].fd, &tmp_chr, 1) != 1)
>  				fprintf(stderr, "read pipefd error\n"),
> @@ -470,8 +475,13 @@ static void *uffd_poll_thread(void *arg)
>  				pollfd[0].revents), exit(1);
>  		ret = read(uffd, &msg, sizeof(msg));
>  		if (ret < 0) {
> -			if (errno == EAGAIN)
> +			if (errno == EINTR) {
> +				fprintf(stderr, "nonblocking read -EINTR\n");
>  				continue;
> +			}
> +			if (errno == EAGAIN) {
> +				continue;
> +			}
>  			perror("nonblocking read error"), exit(1);
>  		}
>  		switch (msg.event) {
> @@ -734,14 +744,23 @@ static int faulting_process(int signal_test)
>  		count = *area_count(area_dst, nr);
>  		if (count != count_verify[nr]) {
>  			fprintf(stderr,
> -				"nr %lu memory corruption %Lu %Lu\n",
> +				"nr %lu memory corruption %Lu %Lu %d\n",
>  				nr, count,
> -				count_verify[nr]), exit(1);
> +				count_verify[nr], signal_test), exit(1);
>  		}
>  	}
> 
> -	if (signal_test)
> +	if (signal_test) {
> +		/* restore SIGBUS defaults after signal_test completed */
> +		sigbuf = NULL;
> +		memset(&act, 0, sizeof(act));
> +		act.sa_handler = SIG_DFL;
> +		if (sigaction(SIGBUS, &act, 0)) {
> +			perror("sigaction SIGBUS SIG_DFL");
> +			return 1;
> +		}
>  		return signalled != split_nr_pages;
> +	}
> 
>  	if (test_type == TEST_HUGETLB)
>  		return 0;
> @@ -920,14 +939,25 @@ static int userfaultfd_events_test(void)
>  	if (pthread_create(&uffd_mon, &attr, uffd_poll_thread, NULL))
>  		perror("uffd_poll_thread create"), exit(1);
> 
> +	/* See the comment in background_signal_flood */
> +	sigset_t set;
> +	sigemptyset(&set);
> +	sigaddset(&set, SIGUSR1);
> +	sigaddset(&set, SIGALRM);
> +#ifndef REPRODUCE_SPURIOUS_UFFD_EVENT_FORK_IN_EVENTS_TEST
> +	/* FIXME, should be pthread_sigmask */
> +	sigprocmask(SIG_BLOCK, &set, NULL);
> +#endif
>  	pid = fork();
> +	sigprocmask(SIG_UNBLOCK, &set, NULL);
>  	if (pid < 0)
>  		perror("fork"), exit(1);
> 
>  	if (!pid)
> -		return faulting_process(0);
> +		exit(faulting_process(0));
> 
> -	waitpid(pid, &err, 0);
> +	if (waitpid(pid, &err, 0) != pid)
> +		perror("waitpid"), exit(1);
>  	if (err)
>  		fprintf(stderr, "faulting process failed\n"), exit(1);
> 
> @@ -985,14 +1015,25 @@ static int userfaultfd_sig_test(void)
>  	if (pthread_create(&uffd_mon, &attr, uffd_poll_thread, NULL))
>  		perror("uffd_poll_thread create"), exit(1);
> 
> +	/* See the comment in background_signal_flood */
> +	sigset_t set;
> +	sigemptyset(&set);
> +	sigaddset(&set, SIGUSR1);
> +	sigaddset(&set, SIGALRM);
> +#ifndef REPRODUCE_SPURIOUS_UFFD_EVENT_FORK_IN_SIG_TEST
> +	/* FIXME, should be pthread_sigmask */
> +	sigprocmask(SIG_BLOCK, &set, NULL);
> +#endif
>  	pid = fork();
> +	sigprocmask(SIG_UNBLOCK, &set, NULL);
>  	if (pid < 0)
>  		perror("fork"), exit(1);
> 
>  	if (!pid)
>  		exit(faulting_process(2));
> 
> -	waitpid(pid, &err, 0);
> +	if (waitpid(pid, &err, 0) != pid)
> +		perror("waitpid"), exit(1);
>  	if (err)
>  		fprintf(stderr, "faulting process failed\n"), exit(1);
> 
> @@ -1267,14 +1308,66 @@ static void sigalrm(int sig)
>  	alarm(ALARM_INTERVAL_SECS);
>  }
> 
> +static void sigusr1(int sig)
> +{
> +	if (sig != SIGUSR1)
> +		abort();
> +}
> +
> +/*
> + * This helps reproduce the UFFD_EVENT_FORK behavior where multiple
> + * child uffds are created despite there's only one parent. That is ok
> + * as long as they're all tracked. Non cooperative users will work
> + * fine even if the mm belonging to the additional uffd are already
> + * destroyed, simply no userfault or event will happen there. To
> + * optimize those away we'd need to send the UFFD_EVENT_FORK after the
> + * signal_pending check (way up into the callers of
> + * dup_mmap()). Alternatively we'd need a reliable way to be notified
> + * with POLLHUP when the mm exits so even if spurious uffd are
> + * initially received by the monitor thread, they can be garbage
> + * collected, but that would require to queue up userfaultfd_ctx in
> + * the mm_struct. This signal flood makes sure signals are working
> + * fine at all times and the only tricky part is the UFFD_EVENT_FORK
> + * handling. If you make cooperative assumptions on non cooperative
> + * UFFD_EVENT_FORK, masking signals around fork() is necessary with
> + * the currrent API (and it will remain backwards compatible even if
> + * we lift this requirement).
> + */
> +static pid_t background_signal_flood(void)
> +{
> +	pid_t parent_pid = getpid(), pid;
> +	int n;
> +	pid = fork();
> +	if (pid < 0)
> +		perror("fork"), exit(1);
> +	if (!pid) {
> +		for (;;) {
> +			for (n = 0; n < 1000; n++) {
> +				if (kill(parent_pid, SIGUSR1))
> +					exit(0);
> +				usleep(1000);
> +			}
> +			sleep(1);
> +		}
> +	}
> +	return pid;
> +}
> +
>  int main(int argc, char **argv)
>  {
> +	pid_t signal_flooder;
> +	int ret;
>  	if (argc < 4)
>  		fprintf(stderr, "Usage: <test type> <MiB> <bounces> [hugetlbfs_file]\n"),
>  				exit(1);
> 
> +	/* FIXME: signal not to be used in multithreaded... */
>  	if (signal(SIGALRM, sigalrm) == SIG_ERR)
>  		fprintf(stderr, "failed to arm SIGALRM"), exit(1);
> +	if (signal(SIGUSR1, sigusr1) == SIG_ERR)
> +		fprintf(stderr, "failed to arm SIGUSR1"), exit(1);
> +
> +	signal_flooder = background_signal_flood();
>  	alarm(ALARM_INTERVAL_SECS);
> 
>  	set_test_type(argv[1]);
> @@ -1312,7 +1405,11 @@ int main(int argc, char **argv)
>  	}
>  	printf("nr_pages: %lu, nr_pages_per_cpu: %lu\n",
>  	       nr_pages, nr_pages_per_cpu);
> -	return userfaultfd_stress();
> +	ret = userfaultfd_stress();
> +	kill(signal_flooder, SIGTERM);
> +	if (waitpid(signal_flooder, NULL, 0) != signal_flooder)
> +		perror("waitpid"), exit(1);
> +	return ret;
>  }
> 
>  #else /* __NR_userfaultfd */
> 

-- 
Sincerely yours,
Mike.

--
To unsubscribe, send a message with 'unsubscribe linux-mm' in
the body to majordomo@xxxxxxxxx.  For more info on Linux MM,
see: http://www.linux-mm.org/ .
Don't email: <a href=mailto:"dont@xxxxxxxxx";> email@xxxxxxxxx </a>



[Index of Archives]     [Linux ARM Kernel]     [Linux ARM]     [Linux Omap]     [Fedora ARM]     [IETF Annouce]     [Bugtraq]     [Linux OMAP]     [Linux MIPS]     [eCos]     [Asterisk Internet PBX]     [Linux API]
  Powered by Linux