NetBSD-Bugs archive

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

lib/59124: arc4random(3): first call in process races with concurrent fork



>Number:         59124
>Category:       lib
>Synopsis:       arc4random(3): first call in process races with concurrent fork
>Confidential:   no
>Severity:       serious
>Priority:       medium
>Responsible:    lib-bug-people
>State:          open
>Class:          sw-bug
>Submitter-Id:   net
>Arrival-Date:   Mon Mar 03 21:05:00 +0000 2025
>Originator:     Taylor R Campbell
>Release:        current, 10, 9, ...
>Organization:
The NetBSD Fork4randamnation
>Environment:
>Description:
Suppose thread A calls arc4random for the first time, and thread B calls fork at the same time.

arc4random will (a) take the global arc4random lock and (b) call pthread_atfork to make itself fork-safe.

But if the fork happens between (a) and (b), the child will see the lock held with no threads running to release it.
>How-To-Repeat:
#include <sys/wait.h>

#include <err.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

static void *
forkit(void *cookie)
{
	pthread_barrier_t *bar = cookie;
	pid_t pid;
	int status;

	(void)pthread_barrier_wait(bar);
	if ((pid = fork()) == -1)
		err(1, "fork");
	if (pid == 0) {
		(void)alarm(1);
		(void)arc4random();
		_exit(0);
	}
	if (waitpid(pid, &status, 0) == -1)
		err(1, "waitpid");
	if (WIFSIGNALED(status)) {
		errx(1, "child exited on signal %d (%s)", WTERMSIG(status),
		    strsignal(WTERMSIG(status)));
	}
	if (!WIFEXITED(status) || WEXITSTATUS(status) != 0)
		errx(1, "child exited 0x%x", status);
	return NULL;
}

static void
test(void)
{
	pthread_barrier_t bar;
	pthread_t t;
	int error;

	error = pthread_barrier_init(&bar, NULL, 2);
	if (error)
		errc(1, error, "pthread_barrier_init");
	error = pthread_create(&t, NULL, &forkit, &bar);
	if (error)
		errc(1, error, "pthread_create");
	(void)pthread_barrier_wait(&bar);
	(void)arc4random();
	error = pthread_join(t, NULL);
	if (error)
		errc(1, error, "pthread_join");
}

int
main(void)
{

	for (unsigned long long n = 1;; n++) {
		pid_t pid;
		int status;

		if ((pid = fork()) == -1)
			err(1, "fork");
		if (pid == 0) {
			test();
			_exit(0);
		}
		if (waitpid(pid, &status, 0) == -1)
			err(1, "waitpid");
		if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
			errx(1, "test exited 0x%x after %zu trial%s", status,
			    n, n == 1 ? "" : "s");
		}
	}
	return 0;
}
>Fix:
Yes, please!

This is an instance of a more general problem.  One way we could address this is by creating a variant of pthread_atfork with caller-supplied storage, and using it in an ELF constructor routine which runs before any threads can be started.  Applications, of course, can avoid the problem if they make a single call to arc4random before fork.  But this has never been part of the contract of arc4random which is generally to be usable in any contexts without having to think about anything.



Home | Main Index | Thread Index | Old Index