#define _REENTRANT
#include <sys/cdefs.h>
__RCSID("$NetBSD: t_arc4random.c,v 1.5 2025/03/09 18:11:55 riastradh Exp $");
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/sysctl.h>
#include <sys/wait.h>
#include <atf-c.h>
#include <err.h>
#include <fcntl.h>
#include <paths.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "arc4random.h"
#include "reentrant.h"
#include "h_macros.h"
static bool
iszero(const void *buf, size_t len)
{
const unsigned char *p = buf;
size_t i;
for (i = 0; i < len; i++) {
if (p[i] != 0)
return false;
}
return true;
}
static struct arc4random_prng *
arc4random_prng(void)
{
struct arc4random_prng *prng = NULL;
if (arc4random_global.per_thread)
prng = thr_getspecific(arc4random_global.thread_key);
if (prng == NULL)
prng = &arc4random_global.prng;
return prng;
}
static void
arc4random_global_buf(void *buf, size_t len)
{
struct rlimit rlim, orlim;
struct arc4random_prng *prng;
RL(getrlimit(RLIMIT_AS, &orlim));
memcpy(&rlim, &orlim, sizeof(rlim));
rlim.rlim_cur = 0;
RL(setrlimit(RLIMIT_AS, &rlim));
arc4random_buf(buf, len);
RL(setrlimit(RLIMIT_AS, &orlim));
RL(setrlimit(RLIMIT_AS, &orlim));
prng = arc4random_prng();
ATF_CHECK_EQ(prng, &arc4random_global.prng);
ATF_CHECK(!iszero(&prng->arc4_prng, sizeof(prng->arc4_prng)));
ATF_CHECK(prng->arc4_epoch != 0);
}
static void *
arc4random_global_thread(void *cookie)
{
unsigned char buf[32];
arc4random_global_buf(buf, sizeof(buf));
return NULL;
}
ATF_TC(addrandom);
ATF_TC_HEAD(addrandom, tc)
{
atf_tc_set_md_var(tc, "descr",
"Test arc4random_addrandom updates the state");
}
ATF_TC_BODY(addrandom, tc)
{
unsigned char buf[32], zero32[32] = {0};
struct arc4random_prng *prng, copy;
arc4random_buf(buf, sizeof(buf));
ATF_CHECK(!iszero(buf, sizeof(buf)));
ATF_CHECK(arc4random_global.initialized);
prng = arc4random_prng();
ATF_CHECK(!iszero(&prng->arc4_prng, sizeof(prng->arc4_prng)));
ATF_CHECK(prng->arc4_epoch != 0);
copy = *prng;
arc4random_addrandom(zero32, sizeof(zero32));
ATF_CHECK(memcmp(&prng->arc4_prng, ©.arc4_prng,
sizeof(copy.arc4_prng)) != 0);
copy = *prng;
arc4random_stir();
ATF_CHECK(memcmp(&prng->arc4_prng, ©.arc4_prng,
sizeof(copy.arc4_prng)) != 0);
}
ATF_TC(consolidate);
ATF_TC_HEAD(consolidate, tc)
{
atf_tc_set_md_var(tc, "descr",
"Test consolidating entropy resets the epoch");
}
ATF_TC_BODY(consolidate, tc)
{
unsigned char buf[32];
struct arc4random_prng *local, *global = &arc4random_global.prng;
unsigned localepoch, globalepoch;
const int consolidate = 1;
pthread_t thread;
arc4random_global_buf(buf, sizeof(buf));
ATF_CHECK(!iszero(buf, sizeof(buf)));
ATF_CHECK(!iszero(&global->arc4_prng, sizeof(global->arc4_prng)));
ATF_CHECK((globalepoch = global->arc4_epoch) != 0);
arc4random_buf(buf, sizeof(buf));
ATF_CHECK(!iszero(buf, sizeof(buf)));
local = arc4random_prng();
ATF_CHECK(!iszero(&local->arc4_prng, sizeof(local->arc4_prng)));
ATF_CHECK((localepoch = local->arc4_epoch) != 0);
RL(sysctlbyname("kern.entropy.consolidate", NULL, 0,
&consolidate, sizeof(consolidate)));
ATF_CHECK_EQ_MSG(globalepoch, global->arc4_epoch,
"global epoch was %u, now %u", globalepoch, global->arc4_epoch);
ATF_CHECK_EQ_MSG(localepoch, local->arc4_epoch,
"local epoch was %u, now %u", localepoch, local->arc4_epoch);
arc4random_buf(buf, sizeof(buf));
ATF_CHECK(!iszero(buf, sizeof(buf)));
ATF_CHECK_MSG(localepoch != local->arc4_epoch,
"local epoch unchanged from %u", localepoch);
RZ(pthread_create(&thread, NULL, &arc4random_global_thread, NULL));
RZ(pthread_join(thread, NULL));
ATF_CHECK_MSG(globalepoch != global->arc4_epoch,
"global epoch unchanged from %u", globalepoch);
}
ATF_TC(chroot);
ATF_TC_HEAD(chroot, tc)
{
atf_tc_set_md_var(tc, "descr",
"Test arc4random in an empty chroot");
atf_tc_set_md_var(tc, "require.user", "root");
}
ATF_TC_BODY(chroot, tc)
{
pid_t pid;
int status;
RL(mkdir("root", 0500));
RL(pid = fork());
if (pid == 0) {
unsigned char buf[32] = {0};
if (chroot("root") == -1)
err(1, "chroot");
if (open(_PATH_URANDOM, O_RDONLY) != -1)
errx(1, "open /dev/urandom must fail in empty chroot");
if (errno != ENOENT) {
err(1, "expected open to fail with %d=ENOENT, not %d",
ENOENT, errno);
}
arc4random_buf(buf, sizeof(buf));
if (iszero(buf, sizeof(buf)))
errx(1, "arc4random returned all-zero");
if (arc4random_prng()->arc4_epoch == 0)
errx(1, "arc4random failed to observe entropy epoch");
_exit(0);
}
RL(waitpid(pid, &status, 0));
ATF_CHECK_MSG(WIFEXITED(status) && WEXITSTATUS(status) == 0,
"child exited status 0x%x", status);
}
ATF_TC(fdlimit);
ATF_TC_HEAD(fdlimit, tc)
{
atf_tc_set_md_var(tc, "descr",
"Test arc4random works even if we have hit the fd limit");
}
ATF_TC_BODY(fdlimit, tc)
{
pid_t pid;
int status;
RL(pid = fork());
if (pid == 0) {
struct rlimit rlim = {.rlim_cur = 0, .rlim_max = 0};
unsigned char buf[32] = {0};
if (setrlimit(RLIMIT_NOFILE, &rlim) == -1)
err(1, "setrlimit(RLIMIT_NOFILE)");
if (open(_PATH_URANDOM, O_RDONLY) != -1)
errx(1, "open must fail with zero RLIMIT_NOFILE");
if (errno != EMFILE) {
err(1, "expected open to fail with %d=EMFILE, not %d",
EMFILE, errno);
}
arc4random_buf(buf, sizeof(buf));
if (iszero(buf, sizeof(buf)))
errx(1, "arc4random returned all-zero");
if (arc4random_prng()->arc4_epoch == 0)
errx(1, "arc4random failed to observe entropy epoch");
_exit(0);
}
RL(waitpid(pid, &status, 0));
ATF_CHECK_MSG(WIFEXITED(status) && WEXITSTATUS(status) == 0,
"child exited status 0x%x", status);
}
ATF_TC(fork);
ATF_TC_HEAD(fork, tc)
{
atf_tc_set_md_var(tc, "descr",
"Test fork zeros the state and gets independent state");
}
ATF_TC_BODY(fork, tc)
{
unsigned char buf[32];
struct arc4random_prng *local, *global = &arc4random_global.prng;
struct arc4random_prng childstate;
int fd[2];
pid_t child, pid;
ssize_t nread;
int status;
arc4random_global_buf(buf, sizeof(buf));
ATF_CHECK(!iszero(buf, sizeof(buf)));
ATF_CHECK(!iszero(&global->arc4_prng, sizeof(global->arc4_prng)));
ATF_CHECK(global->arc4_epoch != 0);
arc4random_buf(buf, sizeof(buf));
ATF_CHECK(!iszero(buf, sizeof(buf)));
local = arc4random_prng();
ATF_CHECK(!iszero(&local->arc4_prng, sizeof(local->arc4_prng)));
ATF_CHECK(local->arc4_epoch != 0);
RL(pipe(fd));
RL(child = fork());
if (child == 0) {
status = 0;
if (!iszero(local, sizeof(*local))) {
fprintf(stderr, "failed to zero local state\n");
status = 1;
}
if (!iszero(global, sizeof(*global))) {
fprintf(stderr, "failed to zero global state\n");
status = 1;
}
arc4random_buf(buf, sizeof(buf));
if (iszero(buf, sizeof(buf))) {
fprintf(stderr, "failed to generate nonzero output\n");
status = 1;
}
if ((size_t)write(fd[1], local, sizeof(*local)) !=
sizeof(*local)) {
fprintf(stderr, "failed to share local state\n");
status = 1;
}
_exit(status);
}
ATF_CHECK(iszero(global, sizeof(*global)));
RL(nread = read(fd[0], &childstate, sizeof(childstate)));
ATF_CHECK_EQ_MSG(nread, sizeof(childstate),
"nread=%zu sizeof(childstate)=%zu", nread, sizeof(childstate));
ATF_CHECK(memcmp(local, &childstate, sizeof(*local)) != 0);
arc4random_buf(buf, sizeof(buf));
ATF_CHECK(!iszero(buf, sizeof(buf)));
ATF_CHECK(memcmp(local, &childstate, sizeof(*local)) != 0);
RL(pid = waitpid(child, &status, 0));
ATF_CHECK_EQ_MSG(status, 0, "child exited with nonzero status=%d",
status);
}
ATF_TC(global_aslimit);
ATF_TC_HEAD(global_aslimit, tc)
{
atf_tc_set_md_var(tc, "descr",
"Test the global state is used when address space limit is hit");
}
ATF_TC_BODY(global_aslimit, tc)
{
unsigned char buf[32], buf1[32];
arc4random_global_buf(buf, sizeof(buf));
ATF_CHECK(!iszero(buf, sizeof(buf)));
arc4random_buf(buf1, sizeof(buf1));
ATF_CHECK(!iszero(buf1, sizeof(buf1)));
ATF_CHECK(memcmp(buf, buf1, sizeof(buf)) != 0);
}
ATF_TC(global_threadkeylimit);
ATF_TC_HEAD(global_threadkeylimit, tc)
{
atf_tc_set_md_var(tc, "descr",
"Test the global state is used we run out of thread keys");
}
ATF_TC_BODY(global_threadkeylimit, tc)
{
unsigned char buf[32], buf1[32];
arc4random_global_buf(buf, sizeof(buf));
ATF_CHECK(!iszero(buf, sizeof(buf)));
arc4random_global.per_thread = false;
memset(&arc4random_global.thread_key, 0x5a,
sizeof(arc4random_global.thread_key));
arc4random_global.prng.arc4_epoch = 0;
ATF_CHECK(arc4random_prng() == &arc4random_global.prng);
arc4random_buf(buf1, sizeof(buf1));
ATF_CHECK(!iszero(buf1, sizeof(buf1)));
ATF_CHECK(memcmp(buf, buf1, sizeof(buf)) != 0);
ATF_CHECK(arc4random_global.prng.arc4_epoch != 0);
}
ATF_TC(local);
ATF_TC_HEAD(local, tc)
{
atf_tc_set_md_var(tc, "descr",
"Test arc4random uses thread-local state");
}
ATF_TC_BODY(local, tc)
{
unsigned char buf[32], buf1[32];
struct arc4random_prng *prng;
arc4random_buf(buf, sizeof(buf));
ATF_CHECK(!iszero(buf, sizeof(buf)));
prng = arc4random_prng();
ATF_CHECK(prng != &arc4random_global.prng);
ATF_CHECK(!iszero(&prng->arc4_prng, sizeof(prng->arc4_prng)));
ATF_CHECK(prng->arc4_epoch != 0);
arc4random_buf(buf1, sizeof(buf1));
ATF_CHECK(!iszero(buf1, sizeof(buf1)));
ATF_CHECK(memcmp(buf, buf1, sizeof(buf)) != 0);
}
ATF_TC(stackfallback);
ATF_TC_HEAD(stackfallback, tc)
{
atf_tc_set_md_var(tc, "descr",
"Test arc4random with pthread_atfork and thr_keycreate failure");
}
ATF_TC_BODY(stackfallback, tc)
{
unsigned char buf[32], buf1[32];
struct arc4random_prng *local;
arc4random_buf(buf, sizeof(buf));
ATF_CHECK(!iszero(buf, sizeof(buf)));
memset(&arc4random_global.prng, 0, sizeof(arc4random_global.prng));
if ((local = arc4random_prng()) != NULL)
memset(local, 0, sizeof(*local));
arc4random_global.forksafe = false;
arc4random_global.per_thread = false;
arc4random_buf(buf1, sizeof(buf1));
ATF_CHECK(!iszero(buf, sizeof(buf)));
ATF_CHECK(memcmp(buf, buf1, sizeof(buf)) != 0);
ATF_CHECK_EQ_MSG(arc4random_global.prng.arc4_epoch, 0,
"global epoch: %d", arc4random_global.prng.arc4_epoch);
if (local != NULL) {
ATF_CHECK_EQ_MSG(local->arc4_epoch, 0,
"local epoch: %d", local->arc4_epoch);
}
}
ATF_TP_ADD_TCS(tp)
{
ATF_TP_ADD_TC(tp, addrandom);
ATF_TP_ADD_TC(tp, chroot);
ATF_TP_ADD_TC(tp, consolidate);
ATF_TP_ADD_TC(tp, fdlimit);
ATF_TP_ADD_TC(tp, fork);
ATF_TP_ADD_TC(tp, global_aslimit);
ATF_TP_ADD_TC(tp, global_threadkeylimit);
ATF_TP_ADD_TC(tp, local);
ATF_TP_ADD_TC(tp, stackfallback);
return atf_no_error();
}