diff --git a/Makefile b/Makefile
index 96cc89a..3383bad 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
 SRCS:=$(sort $(wildcard src/*/*.c))
 OBJS:=$(SRCS:%.c=%.o)
-DIRS:=$(sort $(wildcard src/*))
+DIRS:=$(filter-out src/common,$(sort $(wildcard src/*)))
 NAMES:=$(OBJS:.o=)
 SPEC_PATTERNS:=src/common/% src/api/% src/math/%
 CFLAGS:=-Isrc/common
@@ -29,6 +29,13 @@ BINS:=$(foreach n,$(NAMES),$($(n).BINS)) src/api/main $(MBINS)
 LIBS:=$(foreach n,$(NAMES),$($(n).LIBS))
 ERRS:=$(BINS:%=%.err)
 
+debug:
+	@echo MBINS $(MBINS)
+	@echo BINS $(BINS)
+	@echo LIBS $(LIBS)
+	@echo ERRS $(ERRS)
+	@echo DIRS $(DIRS)
+
 define target_template
 $(1)/all: $(1)/REPORT
 $(1)/clean:
@@ -41,11 +48,16 @@ endef
 
 $(foreach d,$(DIRS),$(eval $(call target_template,$(d))))
 
-src/common/all: src/common/libtest.a
+src/common/all: src/common/REPORT
+src/common/REPORT: src/common/run
+	cat src/common/*.err >$@
+REPORT: src/common/REPORT
+src/common/run: src/common/run.o src/common/libtest.a
+$(ERRS): src/common/run
 
 all:REPORT
 clean:
-	rm -f $(OBJS) $(BINS) $(LIBS) src/common/libtest.a src/*/*.err
+	rm -f $(OBJS) $(BINS) $(LIBS) src/common/libtest.a src/common/run src/*/*.err
 cleanall: clean
 	rm -f REPORT src/*/REPORT
 REPORT:
@@ -89,7 +101,7 @@ $(IOBJS):CFLAGS+=-DX_PS -DX_TPS -DX_SS
 	touch $@
 %.err: %
 # TODO: proper wrapping that records exit status
-	./$< 2>/dev/null >$@ || true
+	src/common/run ./$< 2>/dev/null >$@ || true
 
 .PHONY: all clean cleanall
 
diff --git a/src/common/path.c b/src/common/path.c
index 4fd7633..6ca9951 100644
--- a/src/common/path.c
+++ b/src/common/path.c
@@ -1,4 +1,5 @@
 #include <string.h>
+#include <stdio.h>
 #include "test.h"
 
 /* relative path to p */
diff --git a/src/common/print.c b/src/common/print.c
new file mode 100644
index 0000000..45a4fd5
--- /dev/null
+++ b/src/common/print.c
@@ -0,0 +1,27 @@
+#include <stdio.h>
+#include <stdarg.h>
+#include <unistd.h>
+
+volatile int t_status = 0;
+
+int t_printf(const char *s, ...)
+{
+	va_list ap;
+	char buf[512];
+	int n;
+
+	t_status = 1;
+	va_start(ap, s);
+	n = vsnprintf(buf, sizeof buf, s, ap);
+	va_end(ap);
+	if (n < 0)
+		n = 0;
+	else if (n >= sizeof buf) {
+		n = sizeof buf;
+		buf[n - 1] = '\n';
+		buf[n - 2] = '.';
+		buf[n - 3] = '.';
+		buf[n - 4] = '.';
+	}
+	return write(1, buf, n);
+}
diff --git a/src/common/run.c b/src/common/run.c
new file mode 100644
index 0000000..8a2a0f3
--- /dev/null
+++ b/src/common/run.c
@@ -0,0 +1,87 @@
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <signal.h>
+#include <time.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <sys/time.h>
+#include <sys/resource.h>
+#include <unistd.h>
+#include "test.h"
+
+static void handler(int s)
+{
+}
+
+static void setrl(int r, long lim) {
+	struct rlimit rl;
+
+	if (getrlimit(r, &rl))
+		t_error("getrlimit %d: %s\n", r, strerror(errno));
+	rl.rlim_cur = lim;
+	if (lim < rl.rlim_max)
+		rl.rlim_max = lim;
+	if (setrlimit(r, &rl))
+		t_error("setrlimit %d: %s\n", r, strerror(errno));
+}
+
+static int start(char *argv[])
+{
+	int pid;
+
+	pid = fork();
+	if (pid == 0) {
+		setrl(RLIMIT_STACK, 100*1024);
+		setrl(RLIMIT_CPU, 2);
+		execv(argv[0], argv);
+		t_error("%s exec failed: %s\n", argv[0], strerror(errno));
+		exit(1);
+	}
+	if (pid == -1) {
+		t_error("%s fork failed: %s\n", argv[0], strerror(errno));
+		exit(-1);
+	}
+	return pid;
+}
+
+int main(int argc, char *argv[])
+{
+	int status;
+	sigset_t set;
+	int timeout = 0;
+	int sig = 0;
+	int pid;
+
+	if (argc < 2) {
+		t_error("usage: ./run cmd [args..]\n");
+		return -1;
+	}
+	argv++;
+	sigemptyset(&set);
+	sigaddset(&set, SIGCHLD);
+	sigprocmask(SIG_BLOCK, &set, 0);
+	signal(SIGCHLD, handler);
+	pid = start(argv);
+	if (sigtimedwait(&set, 0, &(struct timespec){5,0}) == -1) {
+		if (errno == EAGAIN)
+			timeout = 1;
+		if (kill(pid, SIGKILL) == -1)
+			t_error("%s kill failed: %s\n", argv[0], strerror(errno));
+	}
+	if (waitpid(pid, &status, 0) != pid) {
+		t_error("%s waitpid failed: %s\n", argv[0], strerror(errno));
+		return -1;
+	}
+	if (WIFEXITED(status)) {
+		if (WEXITSTATUS(status) == 0)
+			return 0;
+		t_printf("FAIL %s [status %d]\n", argv[0], WEXITSTATUS(status));
+	} else if (timeout) {
+		t_printf("FAIL %s [timed out]\n", argv[0]);
+	} else if (WIFSIGNALED(status)) {
+		t_printf("FAIL %s [signal %s]\n", argv[0], strsignal(WTERMSIG(status)));
+	} else
+		t_printf("FAIL %s [unknown]\n", argv[0]);
+	return 1;
+}
diff --git a/src/common/setrlim.c b/src/common/setrlim.c
new file mode 100644
index 0000000..fc76a8e
--- /dev/null
+++ b/src/common/setrlim.c
@@ -0,0 +1,26 @@
+#include <string.h>
+#include <errno.h>
+#include <sys/resource.h>
+#include "test.h"
+
+int t_setrlim(int r, long lim)
+{
+	struct rlimit rl;
+
+	if (getrlimit(r, &rl)) {
+		t_error("getrlimit %d: %s\n", r, strerror(errno));
+		return -1;
+	}
+	if (lim > rl.rlim_max)
+		return -1;
+	if (lim == rl.rlim_max && lim == rl.rlim_cur)
+		return 0;
+	rl.rlim_max = lim;
+	rl.rlim_cur = lim;
+	if (setrlimit(r, &rl)) {
+		t_error("setrlimit %d: %s\n", r, strerror(errno));
+		return -1;
+	}
+	return 0;
+}
+
diff --git a/src/common/test.h b/src/common/test.h
index e24a1db..21e5caf 100644
--- a/src/common/test.h
+++ b/src/common/test.h
@@ -1,36 +1,14 @@
 #include <stdint.h>
-#include <stdio.h>
-#include <stdarg.h>
 #include <unistd.h>
 
 /* TODO: not thread-safe nor fork-safe */
-static volatile int t_status;
+extern volatile int t_status;
 
 #define T_LOC2(l) __FILE__ ":" #l
 #define T_LOC1(l) T_LOC2(l)
-#define t_error(...) t_printf("ERROR " T_LOC1(__LINE__) ": " __VA_ARGS__)
-
-static int t_printf(const char *s, ...)
-{
-	va_list ap;
-	char buf[512];
-	int n;
-
-	t_status = 1;
-	va_start(ap, s);
-	n = vsnprintf(buf, sizeof buf, s, ap);
-	va_end(ap);
-	if (n < 0)
-		n = 0;
-	else if (n >= sizeof buf) {
-		n = sizeof buf;
-		buf[n - 1] = '\n';
-		buf[n - 2] = '.';
-		buf[n - 3] = '.';
-		buf[n - 4] = '.';
-	}
-	return write(1, buf, n);
-}
+#define t_error(...) t_printf(T_LOC1(__LINE__) ": " __VA_ARGS__)
+
+int t_printf(const char *s, ...);
 
 int t_vmfill(void **, size_t *, int);
 
@@ -45,3 +23,5 @@ int t_choose(uint64_t n, size_t k, uint64_t *p);
 
 char *t_pathrel(char *buf, size_t n, char *argv0, char *p);
 
+int t_setrlim(int r, long lim);
+
diff --git a/src/functional/sem.c b/src/functional/sem.c
index d953a8e..86662b6 100644
--- a/src/functional/sem.c
+++ b/src/functional/sem.c
@@ -1,4 +1,5 @@
 #include <stdlib.h>
+#include <stdio.h>
 #include <errno.h>
 #include <string.h>
 #include <semaphore.h>
diff --git a/src/functional/sscanf_long.c b/src/functional/sscanf_long.c
index 160648b..c9208af 100644
--- a/src/functional/sscanf_long.c
+++ b/src/functional/sscanf_long.c
@@ -6,17 +6,6 @@
 #include <sys/resource.h>
 #include "test.h"
 
-static void setrl(int r, long lim)
-{
-	struct rlimit rl;
-
-	if (getrlimit(r, &rl))
-		t_error("getrlimit %d: %s\n", r, strerror(errno));
-	rl.rlim_cur = lim;
-	if (setrlimit(r, &rl))
-		t_error("setrlimit %d: %s\n", r, strerror(errno));
-}
-
 int main(void)
 {
 	enum {n = 8*1024*1024};
@@ -27,7 +16,7 @@ int main(void)
 
 	if (!s)
 		return t_error("out of memory");
-	setrl(RLIMIT_STACK, 128*1024);
+	t_setrlim(RLIMIT_STACK, 100*1024);
 
 	for (i = 0; i < n; i++) s[i] = '1';
 	s[n-3] = ' ';