Skip to content

Commit

Permalink
Update capsicum-test to git commit f4d97414d48b8f8356b971ab9f45dc5c70…
Browse files Browse the repository at this point in the history
…d53c40

This includes various fixes that I submitted recently such as updating the
pdkill() tests for the actual implemented behaviour
(google/capsicum-test#53) and lots of changes to
avoid calling sleep() and replacing it with reliable synchronization
(pull requests 49,51,52,53,54). This should make the testsuite more reliable
when running on Jenkins. Additionally, process status is now retrieved using
libprocstat instead of running `ps` and parsing the output
(google/capsicum-test#50). This fixes one previously
failing test and speeds up execution.

Overall, this update reduces the total runtime from ~60s to about 4-5 seconds.
  • Loading branch information
arichardson committed Mar 2, 2021
2 parents 0401989 + c59f30a commit 955a3f9
Show file tree
Hide file tree
Showing 10 changed files with 412 additions and 129 deletions.
4 changes: 4 additions & 0 deletions contrib/capsicum-test/GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ OS:=$(shell uname)
ARCH?=64
ARCHFLAG=-m$(ARCH)

ifeq ($(OS),FreeBSD)
EXTRA_LIBS=-lprocstat
endif

ifeq ($(OS),Linux)
PROCESSOR:=$(shell uname -p)

Expand Down
23 changes: 16 additions & 7 deletions contrib/capsicum-test/capability-fd.cc
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@ FORK_TEST_ON(Capability, Mmap, TmpFile("cap_mmap_operations")) {
// Given a file descriptor, create a capability with specific rights and
// make sure only those rights work.
#define TRY_FILE_OPS(fd, ...) do { \
SCOPED_TRACE(#__VA_ARGS__); \
cap_rights_t rights; \
cap_rights_init(&rights, __VA_ARGS__); \
TryFileOps((fd), rights); \
Expand Down Expand Up @@ -1019,6 +1020,8 @@ FORK_TEST_ON(Capability, SocketTransfer, TmpFile("cap_fd_transfer")) {
if (child == 0) {
// Child: enter cap mode
EXPECT_OK(cap_enter());
// Child: send startup notification
SEND_INT_MESSAGE(sock_fds[0], MSG_CHILD_STARTED);

// Child: wait to receive FD over socket
int rc = recvmsg(sock_fds[0], &mh, 0);
Expand All @@ -1036,10 +1039,12 @@ FORK_TEST_ON(Capability, SocketTransfer, TmpFile("cap_fd_transfer")) {
EXPECT_RIGHTS_EQ(&r_rs, &rights);
TryReadWrite(cap_fd);

// Child: acknowledge that we have received and tested the file descriptor
SEND_INT_MESSAGE(sock_fds[0], MSG_CHILD_FD_RECEIVED);

// Child: wait for a normal read
int val;
read(sock_fds[0], &val, sizeof(val));
exit(0);
AWAIT_INT_MESSAGE(sock_fds[0], MSG_PARENT_REQUEST_CHILD_EXIT);
exit(testing::Test::HasFailure());
}

int fd = open(TmpFile("cap_fd_transfer"), O_RDWR | O_CREAT, 0644);
Expand All @@ -1054,6 +1059,9 @@ FORK_TEST_ON(Capability, SocketTransfer, TmpFile("cap_fd_transfer")) {
// Confirm we can do the right operations on the capability
TryReadWrite(cap_fd);

// Wait for child to start up:
AWAIT_INT_MESSAGE(sock_fds[1], MSG_CHILD_STARTED);

// Send the file descriptor over the pipe to the sub-process
mh.msg_controllen = CMSG_LEN(sizeof(int));
cmptr = CMSG_FIRSTHDR(&mh);
Expand All @@ -1063,13 +1071,14 @@ FORK_TEST_ON(Capability, SocketTransfer, TmpFile("cap_fd_transfer")) {
*(int *)CMSG_DATA(cmptr) = cap_fd;
buffer1[0] = 0;
iov[0].iov_len = 1;
sleep(3);
int rc = sendmsg(sock_fds[1], &mh, 0);
EXPECT_OK(rc);

sleep(1); // Ensure subprocess runs
int zero = 0;
write(sock_fds[1], &zero, sizeof(zero));
// Check that the child received the message
AWAIT_INT_MESSAGE(sock_fds[1], MSG_CHILD_FD_RECEIVED);

// Tell the child to exit
SEND_INT_MESSAGE(sock_fds[1], MSG_PARENT_REQUEST_CHILD_EXIT);
}

TEST(Capability, SyscallAt) {
Expand Down
126 changes: 101 additions & 25 deletions contrib/capsicum-test/capmode.cc
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,8 @@ TEST(Capmode, Abort) {
FORK_TEST_F(WithFiles, AllowedMiscSyscalls) {
umask(022);
mode_t um_before = umask(022);
int pipefds[2];
EXPECT_OK(pipe(pipefds));
EXPECT_OK(cap_enter()); // Enter capability mode.

mode_t um = umask(022);
Expand All @@ -540,13 +542,19 @@ FORK_TEST_F(WithFiles, AllowedMiscSyscalls) {
pid_t pid = fork();
EXPECT_OK(pid);
if (pid == 0) {
// Child: almost immediately exit.
sleep(1);
// Child: wait for an exit message from parent (so we can test waitpid).
EXPECT_OK(close(pipefds[0]));
SEND_INT_MESSAGE(pipefds[1], MSG_CHILD_STARTED);
AWAIT_INT_MESSAGE(pipefds[1], MSG_PARENT_REQUEST_CHILD_EXIT);
exit(0);
} else if (pid > 0) {
EXPECT_OK(close(pipefds[1]));
AWAIT_INT_MESSAGE(pipefds[0], MSG_CHILD_STARTED);
errno = 0;
EXPECT_CAPMODE(ptrace_(PTRACE_PEEKDATA_, pid, &pid, NULL));
EXPECT_CAPMODE(waitpid(pid, NULL, 0));
EXPECT_CAPMODE(waitpid(pid, NULL, WNOHANG));
SEND_INT_MESSAGE(pipefds[0], MSG_PARENT_REQUEST_CHILD_EXIT);
if (verbose) fprintf(stderr, " child finished\n");
}

// No error return from sync(2) to test, but check errno remains unset.
Expand All @@ -568,68 +576,136 @@ FORK_TEST_F(WithFiles, AllowedMiscSyscalls) {
}

void *thread_fn(void *p) {
int delay = *(int *)p;
sleep(delay);
int fd = (int)(intptr_t)p;
if (verbose) fprintf(stderr, " thread waiting to run\n");
AWAIT_INT_MESSAGE(fd, MSG_PARENT_CHILD_SHOULD_RUN);
EXPECT_OK(getpid_());
EXPECT_CAPMODE(open("/dev/null", O_RDWR));
return NULL;
// Return whether there have been any failures to the main thread.
void *rval = (void *)(intptr_t)testing::Test::HasFailure();
if (verbose) fprintf(stderr, " thread finished: %p\n", rval);
return rval;
}

// Check that restrictions are the same in subprocesses and threads
FORK_TEST(Capmode, NewThread) {
// Fire off a new thread before entering capability mode
pthread_t early_thread;
int one = 1; // second
EXPECT_OK(pthread_create(&early_thread, NULL, thread_fn, &one));
void *thread_rval;
// Create two pipes, one for synchronization with the threads, the other to
// synchronize with the children (since we can't use waitpid after cap_enter).
// Note: Could use pdfork+pdwait instead, but that is tested in procdesc.cc.
int thread_pipe[2];
EXPECT_OK(pipe(thread_pipe));
int proc_pipe[2];
EXPECT_OK(pipe(proc_pipe));
EXPECT_OK(pthread_create(&early_thread, NULL, thread_fn,
(void *)(intptr_t)thread_pipe[1]));

// Fire off a new process before entering capability mode.
if (verbose) fprintf(stderr, " starting second child (non-capability mode)\n");
int early_child = fork();
EXPECT_OK(early_child);
if (early_child == 0) {
// Child: wait and then confirm this process is unaffect by capability mode in the parent.
sleep(1);
if (verbose) fprintf(stderr, " first child started\n");
EXPECT_OK(close(proc_pipe[0]));
// Child: wait and then confirm this process is unaffected by capability mode in the parent.
AWAIT_INT_MESSAGE(proc_pipe[1], MSG_PARENT_CHILD_SHOULD_RUN);
int fd = open("/dev/null", O_RDWR);
EXPECT_OK(fd);
close(fd);
exit(0);
// Notify the parent of success/failure.
int rval = (int)testing::Test::HasFailure();
SEND_INT_MESSAGE(proc_pipe[1], rval);
if (verbose) fprintf(stderr, " first child finished: %d\n", rval);
exit(rval);
}

EXPECT_OK(cap_enter()); // Enter capability mode.
// At this point the current process has both a child process and a
// child thread that were created before entering capability mode.
// - The child process is unaffected by capability mode.
// - The child thread is affected by capability mode.
SEND_INT_MESSAGE(proc_pipe[0], MSG_PARENT_CHILD_SHOULD_RUN);

// Do an allowed syscall.
EXPECT_OK(getpid_());
// Wait for the first child to exit (should get a zero exit code message).
AWAIT_INT_MESSAGE(proc_pipe[0], 0);

// The child processes/threads return HasFailure(), so we depend on no prior errors.
ASSERT_FALSE(testing::Test::HasFailure())
<< "Cannot continue test with pre-existing failures.";
// Now that we're in capability mode, if we create a second child process
// it will be affected by capability mode.
if (verbose) fprintf(stderr, " starting second child (in capability mode)\n");
int child = fork();
EXPECT_OK(child);
if (child == 0) {
if (verbose) fprintf(stderr, " second child started\n");
EXPECT_OK(close(proc_pipe[0]));
// Child: do an allowed and a disallowed syscall.
EXPECT_OK(getpid_());
EXPECT_CAPMODE(open("/dev/null", O_RDWR));
exit(0);
// Notify the parent of success/failure.
int rval = (int)testing::Test::HasFailure();
SEND_INT_MESSAGE(proc_pipe[1], rval);
if (verbose) fprintf(stderr, " second child finished: %d\n", rval);
exit(rval);
}
// Don't (can't) wait for either child.

// Now tell the early_started thread that it can run. We expect it to also
// be affected by capability mode since it's per-process not per-thread.
// Note: it is important that we don't allow the thread to run before fork(),
// since that could result in fork() being called while the thread holds one
// of the gtest-internal mutexes, so the child process deadlocks.
SEND_INT_MESSAGE(thread_pipe[0], MSG_PARENT_CHILD_SHOULD_RUN);
// Wait for the early-started thread.
EXPECT_OK(pthread_join(early_thread, NULL));
EXPECT_OK(pthread_join(early_thread, &thread_rval));
EXPECT_FALSE((bool)(intptr_t)thread_rval) << "thread returned failure";

// Fire off a new thread.
// Wait for the second child to exit (should get a zero exit code message).
AWAIT_INT_MESSAGE(proc_pipe[0], 0);

// Fire off a new (second) child thread, which is also affected by capability mode.
ASSERT_FALSE(testing::Test::HasFailure())
<< "Cannot continue test with pre-existing failures.";
pthread_t child_thread;
int zero = 0; // seconds
EXPECT_OK(pthread_create(&child_thread, NULL, thread_fn, &zero));
EXPECT_OK(pthread_join(child_thread, NULL));
EXPECT_OK(pthread_create(&child_thread, NULL, thread_fn,
(void *)(intptr_t)thread_pipe[1]));
SEND_INT_MESSAGE(thread_pipe[0], MSG_PARENT_CHILD_SHOULD_RUN);
EXPECT_OK(pthread_join(child_thread, &thread_rval));
EXPECT_FALSE((bool)(intptr_t)thread_rval) << "thread returned failure";

// Fork a subprocess which fires off a new thread.
ASSERT_FALSE(testing::Test::HasFailure())
<< "Cannot continue test with pre-existing failures.";
if (verbose) fprintf(stderr, " starting third child (in capability mode)\n");
child = fork();
EXPECT_OK(child);
if (child == 0) {
if (verbose) fprintf(stderr, " third child started\n");
EXPECT_OK(close(proc_pipe[0]));
pthread_t child_thread2;
EXPECT_OK(pthread_create(&child_thread2, NULL, thread_fn, &zero));
EXPECT_OK(pthread_join(child_thread2, NULL));
exit(0);
EXPECT_OK(pthread_create(&child_thread2, NULL, thread_fn,
(void *)(intptr_t)thread_pipe[1]));
SEND_INT_MESSAGE(thread_pipe[0], MSG_PARENT_CHILD_SHOULD_RUN);
EXPECT_OK(pthread_join(child_thread2, &thread_rval));
EXPECT_FALSE((bool)(intptr_t)thread_rval) << "thread returned failure";
// Notify the parent of success/failure.
int rval = (int)testing::Test::HasFailure();
SEND_INT_MESSAGE(proc_pipe[1], rval);
if (verbose) fprintf(stderr, " third child finished: %d\n", rval);
exit(rval);
}
// Sleep for a bit to allow the subprocess to finish.
sleep(2);
// Wait for the third child to exit (should get a zero exit code message).
AWAIT_INT_MESSAGE(proc_pipe[0], 0);
close(proc_pipe[0]);
close(proc_pipe[1]);
close(thread_pipe[0]);
close(thread_pipe[1]);
}

static int had_signal = 0;
static volatile sig_atomic_t had_signal = 0;
static void handle_signal(int) { had_signal = 1; }

FORK_TEST(Capmode, SelfKill) {
Expand Down
88 changes: 63 additions & 25 deletions contrib/capsicum-test/capsicum-test.cc
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
#include "capsicum-test.h"

#ifdef __FreeBSD__
#include <sys/param.h>
#include <sys/proc.h>
#include <sys/queue.h>
#include <sys/socket.h>
#include <sys/sysctl.h>
#include <sys/user.h>
#include <libprocstat.h>
#endif

#include <stdio.h>
#include <string.h>
#include <signal.h>
Expand Down Expand Up @@ -48,31 +58,59 @@ char ProcessState(int pid) {
return '?';
#endif
#ifdef __FreeBSD__
char buffer[1024];
snprintf(buffer, sizeof(buffer), "ps -p %d -o state | grep -v STAT", pid);
sig_t original = signal(SIGCHLD, SIG_IGN);
FILE* cmd = popen(buffer, "r");
usleep(50000); // allow any pending SIGCHLD signals to arrive
signal(SIGCHLD, original);
int result = fgetc(cmd);
fclose(cmd);
// Map FreeBSD codes to Linux codes.
switch (result) {
case EOF:
return '\0';
case 'D': // disk wait
case 'R': // runnable
case 'S': // sleeping
case 'T': // stopped
case 'Z': // zombie
return result;
case 'W': // idle interrupt thread
return 'S';
case 'I': // idle
return 'S';
case 'L': // waiting to acquire lock
default:
return '?';
// First check if the process exists/we have permission to see it. This
// Avoids warning messages being printed to stderr by libprocstat.
size_t len = 0;
int name[4];
name[0] = CTL_KERN;
name[1] = KERN_PROC;
name[2] = KERN_PROC_PID;
name[3] = pid;
if (sysctl(name, nitems(name), NULL, &len, NULL, 0) < 0 && errno == ESRCH) {
if (verbose) fprintf(stderr, "Process %d does not exist\n", pid);
return '\0'; // No such process.
}
unsigned int count = 0;
struct procstat *prstat = procstat_open_sysctl();
EXPECT_NE(NULL, prstat) << "procstat_open_sysctl failed.";
errno = 0;
struct kinfo_proc *p = procstat_getprocs(prstat, KERN_PROC_PID, pid, &count);
if (p == NULL || count == 0) {
if (verbose) fprintf(stderr, "procstat_getprocs failed with %p/%d: %s\n", p, count, strerror(errno));
procstat_close(prstat);
return '\0';
}
char result = '\0';
// See state() in bin/ps/print.c
switch (p->ki_stat) {
case SSTOP:
result = 'T';
break;
case SSLEEP:
if (p->ki_tdflags & TDF_SINTR) /* interruptable (long) */
result = 'S';
else
result = 'D';
break;
case SRUN:
case SIDL:
result = 'R';
break;
case SWAIT:
case SLOCK:
// We treat SWAIT/SLOCK as 'S' here (instead of 'W'/'L').
result = 'S';
break;
case SZOMB:
result = 'Z';
break;
default:
result = '?';
break;
}
procstat_freeprocs(prstat, p);
procstat_close(prstat);
if (verbose) fprintf(stderr, "Process %d in state '%c'\n", pid, result);
return result;
#endif
}
Loading

0 comments on commit 955a3f9

Please sign in to comment.