Compare commits
	
		
			16 Commits
		
	
	
		
			WIP/enosys
			...
			WIP/cpp
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 73dae3a102 | |||
| f2ca26010a | |||
| 0f39ee7061 | |||
| 41bd6e8f10 | |||
| 7f083909e6 | |||
| 732623fc6f | |||
| dcfbe641f9 | |||
| 72a3b041d9 | |||
| c57ba807d7 | |||
| 6f19c53acf | |||
| 99d26480d7 | |||
| f13cff754c | |||
| 278ae31e2e | |||
| 5ef54a08b4 | |||
| 29b5864dd3 | |||
| 0a4e4850f9 | 
							
								
								
									
										20
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								Makefile
									
									
									
									
									
								
							| @@ -1,17 +1,27 @@ | |||||||
| prefix = /usr/local | prefix = /usr/local | ||||||
| bindir = $(prefix)/bin | bindir = $(prefix)/bin | ||||||
| CFLAGS = -std=c99 -Wall -Wextra -pedantic | CFLAGS = -std=c99 -Wall -Wextra -pedantic | ||||||
|  | CXXFLAGS = -std=c++20 -Wall -Wextra -pedantic | ||||||
|  |  | ||||||
| .DEFAULT_GOAL := test | .DEFAULT_GOAL := tests | ||||||
|  |  | ||||||
|  |  | ||||||
| clean: | clean: | ||||||
| 	rm -f test | 	rm -f test exile.o testcpp | ||||||
|  |  | ||||||
| test: test.c |  | ||||||
| 	$(CC) test.c -g $(CFLAGS) -o test |  | ||||||
|  |  | ||||||
| check: test | exile.o: exile.c exile.h | ||||||
|  | 	$(CC) -c exile.c -g $(CFLAGS) -o exile.o | ||||||
|  |  | ||||||
|  | test: test.c exile.h exile.o | ||||||
|  | 	$(CC) test.c exile.o -g $(CFLAGS) -o test | ||||||
|  |  | ||||||
|  | testcpp: test.cpp exile.h exile.hpp exile.o | ||||||
|  | 	$(CXX) test.cpp exile.o -g $(CXXFLAGS) -o testcpp | ||||||
|  |  | ||||||
|  | tests: test testcpp | ||||||
|  |  | ||||||
|  | check: tests | ||||||
| 	./test.sh | 	./test.sh | ||||||
|  |  | ||||||
| .PHONY: check | .PHONY: check | ||||||
|   | |||||||
							
								
								
									
										199
									
								
								exile.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								exile.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,199 @@ | |||||||
|  | #include "exile.h" | ||||||
|  | #include <functional> | ||||||
|  | #include <iostream> | ||||||
|  | #include <string> | ||||||
|  | #include <tuple> | ||||||
|  | #include <memory> | ||||||
|  | #include <sys/wait.h> | ||||||
|  |  | ||||||
|  | #ifndef EXILE_MMAP_SIZE | ||||||
|  | #define EXILE_MMAP_SIZE 128 * 1024 * 1024 //128MB | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |  | ||||||
|  | template<typename T, typename U, typename ... Args> | ||||||
|  | class launch_arg | ||||||
|  | { | ||||||
|  | 	static_assert(std::is_trivially_copyable_v<T>); | ||||||
|  | 	static_assert(!std::is_pointer_v<T>); | ||||||
|  |  | ||||||
|  | public: | ||||||
|  | 	struct exile_policy *policy; | ||||||
|  | 	T *result_shm; | ||||||
|  | 	U fn; | ||||||
|  | 	std::tuple<Args...> args; | ||||||
|  |  | ||||||
|  | 	launch_arg(struct exile_policy *policy, T *result_shm, U fn, Args && ... args) : policy(policy), | ||||||
|  | 	result_shm(result_shm), fn(fn), args(std::forward<Args>(args)...) {} | ||||||
|  |  | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename T, typename U, typename ... Args> | ||||||
|  | class launch_arg_serializer | ||||||
|  | { | ||||||
|  | 	static_assert(std::is_copy_constructible_v<T>); | ||||||
|  |  | ||||||
|  | public: | ||||||
|  | 	struct exile_policy *policy; | ||||||
|  | 	char *serialize_buffer; | ||||||
|  | 	size_t n; | ||||||
|  | 	U fn; | ||||||
|  | 	std::tuple<Args...> args; | ||||||
|  |  | ||||||
|  | 	const std::function<size_t (const T &, char *, size_t n)> &serializer; | ||||||
|  | 	const std::function<T(const char * buf, size_t n)> &deserializer; | ||||||
|  |  | ||||||
|  | 	launch_arg_serializer(struct exile_policy *policy, char *serialize_buffer, size_t n, const std::function<size_t (const T &, char *, size_t)> &serializer, const std::function<T(const char *, size_t)> &deserializer, U fn, Args && ... args) : policy(policy), serialize_buffer(serialize_buffer), n(n), fn(fn), args(std::forward<Args>(args)...), serializer(serializer), deserializer(deserializer)  {} | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename T, typename U, typename ... Args> | ||||||
|  | int exile_clone_handle_trivial(void * arg) | ||||||
|  | { | ||||||
|  | 	static_assert(std::is_trivially_copyable_v<T>); | ||||||
|  | 	static_assert(!std::is_pointer_v<T>); | ||||||
|  |  | ||||||
|  | 	launch_arg<T, U, Args...> *launchargs = (launch_arg<T, U,  Args...> *) arg; | ||||||
|  | 	int ret = exile_enable_policy(launchargs->policy); | ||||||
|  | 	if(ret != 0) | ||||||
|  | 	{ | ||||||
|  | 		EXILE_LOG_ERROR("exile_enable_policy() failed: %s\n", strerror(errno)); | ||||||
|  | 		return 1; | ||||||
|  | 	} | ||||||
|  | 	T result = std::apply(launchargs->fn, launchargs->args); | ||||||
|  | 	std::cout << result; | ||||||
|  | 	memcpy(launchargs->result_shm, &result, sizeof(T)); | ||||||
|  | 	return 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | template<typename T, typename U, typename ... Args> | ||||||
|  | int exile_clone_handle_serializer(void * arg) | ||||||
|  | { | ||||||
|  | 	static_assert(std::is_copy_constructible_v<T>); | ||||||
|  |  | ||||||
|  | 	launch_arg_serializer<T, U, Args...> *launchargs = (launch_arg_serializer<T, U, Args...> *) arg; | ||||||
|  | 	int ret = exile_enable_policy(launchargs->policy); | ||||||
|  | 	if(ret != 0) | ||||||
|  | 	{ | ||||||
|  | 		EXILE_LOG_ERROR("exile_enable_policy() failed: %s\n", strerror(errno)); | ||||||
|  | 		return 1; | ||||||
|  | 	} | ||||||
|  | 	T result = std::apply(launchargs->fn, launchargs->args); | ||||||
|  | 	/* TODO: exception handling */ | ||||||
|  | 	/* TODO: ugly :S */ | ||||||
|  | 	char *target = launchargs->serialize_buffer + sizeof(size_t); | ||||||
|  | 	size_t n = launchargs->n - sizeof(size_t); | ||||||
|  |  | ||||||
|  | 	size_t size = launchargs->serializer(result, target, n); | ||||||
|  | 	memcpy(launchargs->serialize_buffer, &size, sizeof(size_t)); | ||||||
|  |  | ||||||
|  | 	return 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline int do_clone(int (*clonefn)(void *), void *launcharg) | ||||||
|  | { | ||||||
|  | 	struct rlimit rlimit; | ||||||
|  | 	int ret = getrlimit(RLIMIT_STACK, &rlimit); | ||||||
|  | 	if(ret != 0) | ||||||
|  | 	{ | ||||||
|  | 		EXILE_LOG_ERROR("Failed to get stack size: %s\n", strerror(errno)); | ||||||
|  | 		return ret; | ||||||
|  | 	} | ||||||
|  | 	size_t size = rlimit.rlim_cur; | ||||||
|  | 	char *stack = (char *) calloc(1, size); | ||||||
|  | 	if(stack == NULL) | ||||||
|  | 	{ | ||||||
|  | 		EXILE_LOG_ERROR("Failed to allocate stack memory for child\n"); | ||||||
|  | 		return 1; | ||||||
|  | 	} | ||||||
|  | 	stack += size; | ||||||
|  |  | ||||||
|  | 	ret = clone(clonefn, stack, 17 /* SIGCHLD */, launcharg); | ||||||
|  | 	int status = 0; | ||||||
|  | 	waitpid(ret, &status, __WALL); | ||||||
|  | 	if(WIFEXITED(status)) | ||||||
|  | 	{ | ||||||
|  | 		return WEXITSTATUS(status); | ||||||
|  | 	} | ||||||
|  | 	/* TODO: exception or what? */ | ||||||
|  | 	return 23; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | template<typename T, typename U, typename ... Args> | ||||||
|  | typename std::enable_if_t<std::is_trivially_copyable_v<T>, T> exile_launch(struct exile_policy *policy, U fn, Args && ... args) | ||||||
|  | { | ||||||
|  | 	size_t mapsize = sizeof(T); | ||||||
|  | 	T * sharedbuf =  (T *) mmap(NULL, mapsize , PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0); | ||||||
|  | 	if(sharedbuf == NULL) | ||||||
|  | 	{ | ||||||
|  | 		throw std::runtime_error(std::string("mmap failed: ") + strerror(errno)); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	std::shared_ptr<void> deleter(nullptr, [sharedbuf, mapsize](...){ munmap(sharedbuf, mapsize); }); | ||||||
|  | 	launch_arg<T, U, Args...> launcharg(policy, sharedbuf, fn, std::forward<Args>(args)...); | ||||||
|  |  | ||||||
|  | 	int (*clonefn)(void *) = &exile_clone_handle_trivial<T, U, Args...>; | ||||||
|  | 	/* TODO: exception or what? */ | ||||||
|  | 	int ret = do_clone(clonefn, &launcharg); | ||||||
|  | 	if(ret == 0) | ||||||
|  | 	{ | ||||||
|  | 		return *sharedbuf; | ||||||
|  | 	} | ||||||
|  | 	throw std::runtime_error(std::string("clone() failed: " + std::to_string(ret))); | ||||||
|  | 	return T(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | template<typename T, typename U, typename ... Args> | ||||||
|  | typename std::enable_if_t<!std::is_trivially_copyable_v<T> && std::is_copy_constructible_v<T>, T> | ||||||
|  | 	exile_launch(struct exile_policy *policy, const std::function<size_t (const T &, char *, size_t)> &serializer, const std::function<T(const char *, size_t)> &deserializer, U fn, Args && ... args) | ||||||
|  | { | ||||||
|  | 	size_t mapsize = EXILE_MMAP_SIZE; | ||||||
|  | 	char *sharedbuf =  (char *) mmap(NULL, mapsize , PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0); | ||||||
|  | 	if(sharedbuf == NULL) | ||||||
|  | 	{ | ||||||
|  | 		throw std::runtime_error(std::string("mmap failed: ") + strerror(errno)); | ||||||
|  | 	} | ||||||
|  | 	std::shared_ptr<void> deleter(nullptr, [sharedbuf, mapsize](...){ munmap(sharedbuf, mapsize); }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 	launch_arg_serializer<T, U, Args...> launcharg(policy, sharedbuf, mapsize, serializer, deserializer, fn,												   std::forward<Args>(args)...); | ||||||
|  |  | ||||||
|  | 	int (*clonefn)(void *) = &exile_clone_handle_serializer<T, U, Args...>; | ||||||
|  | 	/* TODO: exception or what? */ | ||||||
|  | 	int ret = do_clone(clonefn, &launcharg); | ||||||
|  | 	if(ret == 0) | ||||||
|  | 	{ | ||||||
|  | 		size_t size = 0; | ||||||
|  | 		memcpy(&size, sharedbuf, sizeof(size)); | ||||||
|  |  | ||||||
|  | 		return deserializer(sharedbuf + sizeof(size_t), size); | ||||||
|  | 	} | ||||||
|  | 	throw std::runtime_error(std::string("clone() failed: " + std::to_string(ret))); | ||||||
|  | 	return T(); | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | template<class T> | ||||||
|  | std::basic_string<typename T::value_type> deserialize_stdstring(const char *buf, size_t n) | ||||||
|  | { | ||||||
|  | 	return std::basic_string<typename T::value_type> { buf, n }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | template<class T> | ||||||
|  | size_t serialize_stdstring(const std::basic_string<typename T::value_type> &t, char *buf, size_t n) | ||||||
|  | { | ||||||
|  | 	if(n < t.size()) | ||||||
|  | 	{ | ||||||
|  | 		return 0; | ||||||
|  | 	} | ||||||
|  | 	memcpy(buf, t.data(), t.size()); | ||||||
|  | 	return t.size(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | template<typename T, typename U, typename ... Args> | ||||||
|  | std::basic_string<typename T::value_type> exile_launch(struct exile_policy *policy, U fn, Args && ... args) | ||||||
|  | { | ||||||
|  | 	return exile_launch<T, U, Args...>(policy, &serialize_stdstring<T>, &deserialize_stdstring<T>, fn, std::forward<Args>(args) ...); | ||||||
|  | } | ||||||
							
								
								
									
										105
									
								
								test.c
									
									
									
									
									
								
							
							
						
						
									
										105
									
								
								test.c
									
									
									
									
									
								
							| @@ -6,12 +6,14 @@ | |||||||
| #include <sys/socket.h> | #include <sys/socket.h> | ||||||
| #include <sys/wait.h> | #include <sys/wait.h> | ||||||
|  |  | ||||||
|  | #define LOG(...) do { fprintf(stdout, "%s(): ", __func__); fprintf(stdout, __VA_ARGS__); } while(0) | ||||||
|  |  | ||||||
| int xexile_enable_policy(struct exile_policy *policy) | int xexile_enable_policy(struct exile_policy *policy) | ||||||
| { | { | ||||||
| 	int ret = exile_enable_policy(policy); | 	int ret = exile_enable_policy(policy); | ||||||
| 	if(ret != 0) | 	if(ret != 0) | ||||||
| 	{ | 	{ | ||||||
| 		fprintf(stderr, "exile_enable_policy() failed: %i\n", ret); | 		LOG("failed: %i\n", ret); | ||||||
| 		exit(EXIT_FAILURE); | 		exit(EXIT_FAILURE); | ||||||
| 	} | 	} | ||||||
| 	return 0; | 	return 0; | ||||||
| @@ -38,16 +40,16 @@ static int test_expected_kill(int (*f)()) | |||||||
| 		int c = WTERMSIG(status); | 		int c = WTERMSIG(status); | ||||||
| 		if(c == SIGSYS) | 		if(c == SIGSYS) | ||||||
| 		{ | 		{ | ||||||
| 			printf("Got expected signal\n"); | 			LOG("Got expected signal\n"); | ||||||
| 			return 0; | 			return 0; | ||||||
| 		} | 		} | ||||||
| 		printf("Unexpected status code: %i\n", c); | 		LOG("Unexpected status code: %i\n", c); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	else | 	else | ||||||
| 	{ | 	{ | ||||||
| 		int c = WEXITSTATUS(status); | 		int c = WEXITSTATUS(status); | ||||||
| 		printf("Process was not killed, test fails. Status code of exit: %i\n", c); | 		LOG("Process was not killed, test fails. Status code of exit: %i\n", c); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	return 0; | 	return 0; | ||||||
| @@ -67,7 +69,7 @@ static int test_successful_exit(int (*f)()) | |||||||
| 	if(WIFSIGNALED(status)) | 	if(WIFSIGNALED(status)) | ||||||
| 	{ | 	{ | ||||||
| 		int c = WTERMSIG(status); | 		int c = WTERMSIG(status); | ||||||
| 		printf("Received signal, which was not expected. Signal was: %i\n", c); | 		LOG("Received signal, which was not expected. Signal was: %i\n", c); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	else | 	else | ||||||
| @@ -75,11 +77,11 @@ static int test_successful_exit(int (*f)()) | |||||||
| 		int c = WEXITSTATUS(status); | 		int c = WEXITSTATUS(status); | ||||||
| 		if(c != 0) | 		if(c != 0) | ||||||
| 		{ | 		{ | ||||||
| 			printf("Process failed to exit properly. Status code is: %i\n", c); | 			LOG("Process failed to exit properly. Status code is: %i\n", c); | ||||||
| 		} | 		} | ||||||
| 		return c; | 		return c; | ||||||
| 	} | 	} | ||||||
| 	printf("Process exited sucessfully as expected"); | 	LOG("Process exited sucessfully as expected"); | ||||||
| 	return 0; | 	return 0; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -153,7 +155,7 @@ int test_seccomp_require_last_matchall() | |||||||
| 	int status = exile_enable_policy(policy); | 	int status = exile_enable_policy(policy); | ||||||
| 	if(status == 0) | 	if(status == 0) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Failed. Should not have been enabled!"); | 		LOG("Failed. Should not have been enabled!"); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	return 0; | 	return 0; | ||||||
| @@ -170,7 +172,7 @@ static int do_test_seccomp_errno() | |||||||
| 	uid_t id = syscall(EXILE_SYS(getuid)); | 	uid_t id = syscall(EXILE_SYS(getuid)); | ||||||
|  |  | ||||||
| 	int fd = syscall(EXILE_SYS(close), 0); | 	int fd = syscall(EXILE_SYS(close), 0); | ||||||
| 	printf("close() return code: %i, errno: %s\n", fd, strerror(errno)); | 	LOG("close() return code: %i, errno: %s\n", fd, strerror(errno)); | ||||||
| 	return fd == -1 ? 0 : 1; | 	return fd == -1 ? 0 : 1; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -254,14 +256,14 @@ int test_seccomp_argfilter_mixed() | |||||||
| 	int s = (int) syscall(EXILE_SYS(stat), "/dev/urandom", &statbuf); | 	int s = (int) syscall(EXILE_SYS(stat), "/dev/urandom", &statbuf); | ||||||
| 	if(s != -1) | 	if(s != -1) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Failed: stat was expected to fail, but returned %i\n", s); | 		LOG("Failed: stat was expected to fail, but returned %i\n", s); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	pid_t p = (pid_t) syscall(EXILE_SYS(getpid)); | 	pid_t p = (pid_t) syscall(EXILE_SYS(getpid)); | ||||||
| 	if(p != -1) | 	if(p != -1) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Failed: getpid was expected to fail, but returned %i\n", p); | 		LOG("Failed: getpid was expected to fail, but returned %i\n", p); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -269,13 +271,13 @@ int test_seccomp_argfilter_mixed() | |||||||
| 	int ret = (int) syscall(EXILE_SYS(open),t, O_WRONLY); | 	int ret = (int) syscall(EXILE_SYS(open),t, O_WRONLY); | ||||||
| 	if(ret != -1) | 	if(ret != -1) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Failed: open was expected to fail, but returned %i\n", ret); | 		LOG("Failed: open was expected to fail, but returned %i\n", ret); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	ret = (int) syscall(EXILE_SYS(open), t, O_RDONLY); | 	ret = (int) syscall(EXILE_SYS(open), t, O_RDONLY); | ||||||
| 	if(ret == -1) | 	if(ret == -1) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Failed: open with O_RDONLY was expected to succeed, but returned %i\n", ret); | 		LOG("Failed: open with O_RDONLY was expected to succeed, but returned %i\n", ret); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	return 0; | 	return 0; | ||||||
| @@ -291,13 +293,13 @@ int do_test_seccomp_vow_socket() | |||||||
| 	int s = socket(AF_INET, SOCK_STREAM, 0); | 	int s = socket(AF_INET, SOCK_STREAM, 0); | ||||||
| 	if(s == -1) | 	if(s == -1) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Failed: socket was expected to succeed, but returned %i\n", s); | 		LOG("Failed: socket was expected to succeed, but returned %i\n", s); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	s = socket(AF_UNIX, SOCK_DGRAM, 0); | 	s = socket(AF_UNIX, SOCK_DGRAM, 0); | ||||||
| 	if(s != -1) | 	if(s != -1) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Failed: socket was expected to fail, but returned %i\n", s); | 		LOG("Failed: socket was expected to fail, but returned %i\n", s); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	return 0; | 	return 0; | ||||||
| @@ -312,19 +314,19 @@ int do_test_seccomp_vow_open() | |||||||
| 	int ret = open("/dev/urandom", O_WRONLY  | O_APPEND); | 	int ret = open("/dev/urandom", O_WRONLY  | O_APPEND); | ||||||
| 	if(ret != -1) | 	if(ret != -1) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Failed: open was expected to fail, but returned %i\n", ret); | 		LOG("Failed: open was expected to fail, but returned %i\n", ret); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	ret = open("/dev/urandom", O_RDWR); | 	ret = open("/dev/urandom", O_RDWR); | ||||||
| 	if(ret != -1) | 	if(ret != -1) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Failed: open O_RDWR was expected to fail, but returned %i\n", ret); | 		LOG("Failed: open O_RDWR was expected to fail, but returned %i\n", ret); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	ret = open("/dev/urandom", O_RDONLY); | 	ret = open("/dev/urandom", O_RDONLY); | ||||||
| 	if(ret == -1) | 	if(ret == -1) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Failed: open was expected to succceed, but returned %i\n", ret); | 		LOG("Failed: open was expected to succceed, but returned %i\n", ret); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	return 0; | 	return 0; | ||||||
| @@ -335,13 +337,13 @@ int test_seccomp_vow() | |||||||
| 	int ret = test_successful_exit(&do_test_seccomp_vow_open); | 	int ret = test_successful_exit(&do_test_seccomp_vow_open); | ||||||
| 	if(ret != 0) | 	if(ret != 0) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Failed: do_test_seccomp_vow_open()\n"); | 		LOG("Failed: do_test_seccomp_vow_open()\n"); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	ret = test_successful_exit(&do_test_seccomp_vow_socket); | 	ret = test_successful_exit(&do_test_seccomp_vow_socket); | ||||||
| 	if(ret != 0) | 	if(ret != 0) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Failed: do_test_seccomp_vow_socket()\n"); | 		LOG("Failed: do_test_seccomp_vow_socket()\n"); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	return 0; | 	return 0; | ||||||
| @@ -353,13 +355,13 @@ int test_seccomp_exile_vow_multiple() | |||||||
| 	int ret = exile_vow(EXILE_SYSCALL_VOW_STDIO | EXILE_SYSCALL_VOW_UNIX | EXILE_SYSCALL_VOW_SECCOMP_INSTALL | EXILE_SYSCALL_VOW_DENY_ERROR); | 	int ret = exile_vow(EXILE_SYSCALL_VOW_STDIO | EXILE_SYSCALL_VOW_UNIX | EXILE_SYSCALL_VOW_SECCOMP_INSTALL | EXILE_SYSCALL_VOW_DENY_ERROR); | ||||||
| 	if(ret != 0) | 	if(ret != 0) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Failed: exile_vow() call 1 failed\n"); | 		LOG("Failed: exile_vow() call 1 failed\n"); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	int s = socket(AF_UNIX, SOCK_STREAM, 0); | 	int s = socket(AF_UNIX, SOCK_STREAM, 0); | ||||||
| 	if(s == -1) | 	if(s == -1) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Failed: socket was expected to succeed, but returned %i\n", s); | 		LOG("Failed: socket was expected to succeed, but returned %i\n", s); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -367,13 +369,13 @@ int test_seccomp_exile_vow_multiple() | |||||||
| 	ret = exile_vow(EXILE_SYSCALL_VOW_STDIO | EXILE_SYSCALL_VOW_SECCOMP_INSTALL | EXILE_SYSCALL_VOW_DENY_ERROR); | 	ret = exile_vow(EXILE_SYSCALL_VOW_STDIO | EXILE_SYSCALL_VOW_SECCOMP_INSTALL | EXILE_SYSCALL_VOW_DENY_ERROR); | ||||||
| 	if(ret != 0) | 	if(ret != 0) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Failed: exile_vow() call 2 failed\n"); | 		LOG("Failed: exile_vow() call 2 failed\n"); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	s = socket(AF_UNIX, SOCK_STREAM, 0); | 	s = socket(AF_UNIX, SOCK_STREAM, 0); | ||||||
| 	if(s != -1) | 	if(s != -1) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Failed: socket was expected to fail, but returned %i\n", s); | 		LOG("Failed: socket was expected to fail, but returned %i\n", s); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -381,13 +383,13 @@ int test_seccomp_exile_vow_multiple() | |||||||
| 	ret = exile_vow(EXILE_SYSCALL_VOW_STDIO | EXILE_SYSCALL_VOW_UNIX | EXILE_SYSCALL_VOW_SECCOMP_INSTALL | EXILE_SYSCALL_VOW_DENY_ERROR); | 	ret = exile_vow(EXILE_SYSCALL_VOW_STDIO | EXILE_SYSCALL_VOW_UNIX | EXILE_SYSCALL_VOW_SECCOMP_INSTALL | EXILE_SYSCALL_VOW_DENY_ERROR); | ||||||
| 	if(ret != 0) | 	if(ret != 0) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Failed: exile_vow() call 3 failed\n"); | 		LOG("Failed: exile_vow() call 3 failed\n"); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	s = socket(AF_UNIX, SOCK_STREAM, 0); | 	s = socket(AF_UNIX, SOCK_STREAM, 0); | ||||||
| 	if(s != -1) | 	if(s != -1) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Failed: socket was still expected to fail, but returned %i\n", s); | 		LOG("Failed: socket was still expected to fail, but returned %i\n", s); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -400,7 +402,7 @@ int test_landlock() | |||||||
| { | { | ||||||
| 	if(!exile_landlock_is_available()) | 	if(!exile_landlock_is_available()) | ||||||
| 	{ | 	{ | ||||||
| 		printf("landlock not available, so cannot test\n"); | 		LOG("landlock not available, so cannot test\n"); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	struct exile_policy *policy = exile_init_policy(); | 	struct exile_policy *policy = exile_init_policy(); | ||||||
| @@ -449,14 +451,14 @@ int test_nofs() | |||||||
| 	int s = socket(AF_INET,SOCK_STREAM,0); | 	int s = socket(AF_INET,SOCK_STREAM,0); | ||||||
| 	if(s == -1) | 	if(s == -1) | ||||||
| 	{ | 	{ | ||||||
| 		fprintf(stderr, "Failed to open socket but this was not requested by policy\n"); | 		LOG("Failed to open socket but this was not requested by policy\n"); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	/* Expect seccomp to take care of this */ | 	/* Expect seccomp to take care of this */ | ||||||
| 	if(open("/test", O_CREAT | O_WRONLY) >= 0) | 	if(open("/test", O_CREAT | O_WRONLY) >= 0) | ||||||
| 	{ | 	{ | ||||||
| 		fprintf(stderr, "Failed: We do not expect write access\n"); | 		LOG("Failed: We do not expect write access\n"); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -472,14 +474,14 @@ int test_no_new_fds() | |||||||
|  |  | ||||||
| 	if(open("/tmp/test", O_CREAT | O_WRONLY) >= 0) | 	if(open("/tmp/test", O_CREAT | O_WRONLY) >= 0) | ||||||
| 	{ | 	{ | ||||||
| 		fprintf(stderr, "Failed: Could open new file descriptor\n"); | 		LOG("Failed: Could open new file descriptor\n"); | ||||||
| 		return -1; | 		return -1; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	int s = socket(AF_INET,SOCK_STREAM,0); | 	int s = socket(AF_INET,SOCK_STREAM,0); | ||||||
| 	if(s >= 0) | 	if(s >= 0) | ||||||
| 	{ | 	{ | ||||||
| 		fprintf(stderr, "Failed: socket got opened but policy denied\n"); | 		LOG("Failed: socket got opened but policy denied\n"); | ||||||
| 		return -1; | 		return -1; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -487,6 +489,7 @@ int test_no_new_fds() | |||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | extern int mkpath(const char *p, mode_t mode, int baseisfile); | ||||||
| int test_mkpath() | int test_mkpath() | ||||||
| { | { | ||||||
| 	system("rm -rf /tmp/.exile.h/"); | 	system("rm -rf /tmp/.exile.h/"); | ||||||
| @@ -495,13 +498,13 @@ int test_mkpath() | |||||||
| 	int ret = mkpath(filepath,  0700, 1); | 	int ret = mkpath(filepath,  0700, 1); | ||||||
| 	if(ret != 0) | 	if(ret != 0) | ||||||
| 	{ | 	{ | ||||||
| 		fprintf(stderr, "Failed: mkpath(file) returned: %i\n", ret); | 		LOG("Failed: mkpath(file) returned: %i\n", ret); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	ret = mkpath(dirpath, 0700, 0); | 	ret = mkpath(dirpath, 0700, 0); | ||||||
| 	if(ret != 0) | 	if(ret != 0) | ||||||
| 	{ | 	{ | ||||||
| 		fprintf(stderr, "Failed: mkpath(dirpath) returned: %i\n", ret); | 		LOG("Failed: mkpath(dirpath) returned: %i\n", ret); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -509,23 +512,23 @@ int test_mkpath() | |||||||
| 	ret = stat(filepath, &statbuf); | 	ret = stat(filepath, &statbuf); | ||||||
| 	if(ret != 0) | 	if(ret != 0) | ||||||
| 	{ | 	{ | ||||||
| 		fprintf(stderr, "Failed: stat on filepath returned: %i\n", ret); | 		LOG("Failed: stat on filepath returned: %i\n", ret); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	if(!S_ISREG(statbuf.st_mode)) | 	if(!S_ISREG(statbuf.st_mode)) | ||||||
| 	{ | 	{ | ||||||
| 		fprintf(stderr, "Failed: mkpath did not create a file: %i\n", ret); | 		LOG("Failed: mkpath did not create a file: %i\n", ret); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	ret = stat(dirpath, &statbuf); | 	ret = stat(dirpath, &statbuf); | ||||||
| 	if(ret != 0) | 	if(ret != 0) | ||||||
| 	{ | 	{ | ||||||
| 		fprintf(stderr, "Failed: stat on dirpath returned: %i\n", ret); | 		LOG("Failed: stat on dirpath returned: %i\n", ret); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	if(!S_ISDIR(statbuf.st_mode)) | 	if(!S_ISDIR(statbuf.st_mode)) | ||||||
| 	{ | 	{ | ||||||
| 		fprintf(stderr, "Failed: mkpath did not create a directory: %i\n", ret); | 		LOG("Failed: mkpath did not create a directory: %i\n", ret); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	system("rm -rf /tmp/.exile.h/"); | 	system("rm -rf /tmp/.exile.h/"); | ||||||
| @@ -545,12 +548,14 @@ int test_fail_flags() | |||||||
| 	return 0; | 	return 0; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | static int *read_pipe = NULL; | ||||||
| int do_launch_test(void *arg) | int do_launch_test(void *arg) | ||||||
| { | { | ||||||
| 	int num = *(int *)(arg); | 	int num = *(int *)(arg); | ||||||
| 	num += 1; | 	num += 1; | ||||||
| 	char buffer[512] = { 0 }; | 	char buffer[512] = { 0 }; | ||||||
| 	read(child_write_pipe[0], buffer, sizeof(buffer)-1); | 	read(*read_pipe, buffer, sizeof(buffer)-1); | ||||||
| 	printf("Sandboxed +1: %i\n", num); | 	printf("Sandboxed +1: %i\n", num); | ||||||
| 	printf("Echoing: %s\n", buffer); | 	printf("Echoing: %s\n", buffer); | ||||||
| 	fflush(stdout); | 	fflush(stdout); | ||||||
| @@ -566,10 +571,11 @@ int test_launch() | |||||||
| 	params.func = &do_launch_test; | 	params.func = &do_launch_test; | ||||||
| 	params.funcarg = # | 	params.funcarg = # | ||||||
| 	params.policy = policy; | 	params.policy = policy; | ||||||
|  | 	read_pipe = ¶ms.child_write_pipe[0]; | ||||||
| 	int launchfd = exile_launch(¶ms, &res); | 	int launchfd = exile_launch(¶ms, &res); | ||||||
| 	if(launchfd < 0) | 	if(launchfd < 0) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Failed to launch\n"); | 		LOG("Failed to launch\n"); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -577,11 +583,11 @@ int test_launch() | |||||||
| 	write(res.write_fd, "1234", 4); | 	write(res.write_fd, "1234", 4); | ||||||
| 	int s = read(res.read_fd, buffer, sizeof(buffer)-1); | 	int s = read(res.read_fd, buffer, sizeof(buffer)-1); | ||||||
| 	write(1, buffer, s); | 	write(1, buffer, s); | ||||||
| 	printf("Before wait, got: %i\n", s); | 	LOG("Before wait, got: %i\n", s); | ||||||
| 	fflush(stdout); | 	fflush(stdout); | ||||||
| 	if(strstr(buffer, "Echoing: 1234") == NULL) | 	if(strstr(buffer, "Echoing: 1234") == NULL) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Failed: Did not get back what we wrote\n"); | 		LOG("Failed: Did not get back what we wrote\n"); | ||||||
| 	} | 	} | ||||||
| 	int status = 0; | 	int status = 0; | ||||||
| 	waitpid(res.tid, &status, __WALL); | 	waitpid(res.tid, &status, __WALL); | ||||||
| @@ -614,12 +620,24 @@ int test_launch_get() | |||||||
| 	unsigned int len = strlen(LAUNCH_GET_TEST_STR); | 	unsigned int len = strlen(LAUNCH_GET_TEST_STR); | ||||||
| 	if(n != strlen(LAUNCH_GET_TEST_STR)) | 	if(n != strlen(LAUNCH_GET_TEST_STR)) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Lenght does does not match: %lu vs %u\n", n, len); | 		LOG("Lenght does does not match: %lu vs %u\n", n, len); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	if(strcmp(content, LAUNCH_GET_TEST_STR) != 0) | 	if(strcmp(content, LAUNCH_GET_TEST_STR) != 0) | ||||||
| 	{ | 	{ | ||||||
| 		printf("Received content differs\n"); | 		LOG("Received content differs\n"); | ||||||
|  | 		return 1; | ||||||
|  | 	} | ||||||
|  | 	return 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | int test_vows_from_str() | ||||||
|  | { | ||||||
|  | 	uint64_t expected = EXILE_SYSCALL_VOW_CHOWN | EXILE_SYSCALL_VOW_WPATH | EXILE_SYSCALL_VOW_INET | EXILE_SYSCALL_VOW_DENY_ERROR; | ||||||
|  | 	uint64_t actual = exile_vows_from_str("chown wpath inet error"); | ||||||
|  | 	if(expected != actual) | ||||||
|  | 	{ | ||||||
|  | 		LOG("Masks don't match: %lu vs %lu\n", expected, actual); | ||||||
| 		return 1; | 		return 1; | ||||||
| 	} | 	} | ||||||
| 	return 0; | 	return 0; | ||||||
| @@ -651,6 +669,7 @@ struct dispatcher dispatchers[] = { | |||||||
| 	{ "failflags", &test_fail_flags}, | 	{ "failflags", &test_fail_flags}, | ||||||
| 	{ "launch", &test_launch}, | 	{ "launch", &test_launch}, | ||||||
| 	{ "launch-get", &test_launch_get}, | 	{ "launch-get", &test_launch_get}, | ||||||
|  | 	{ "vow_from_str", &test_vows_from_str}, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| int main(int argc, char *argv[]) | int main(int argc, char *argv[]) | ||||||
|   | |||||||
							
								
								
									
										92
									
								
								test.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								test.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | |||||||
|  | #include "exile.hpp" | ||||||
|  | #include "assert.h" | ||||||
|  | #include <map> | ||||||
|  |  | ||||||
|  | std::string sandboxed_reverse(std::string str) | ||||||
|  | { | ||||||
|  | 	std::reverse(str.begin(), str.end()); | ||||||
|  | 	return str; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | size_t stdstrlen(const std::string &str) | ||||||
|  | { | ||||||
|  | 	return str.size(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | int incrementer(int arg) | ||||||
|  | { | ||||||
|  | 	return ++arg; | ||||||
|  | } | ||||||
|  | int test_exile_launch_trivial() | ||||||
|  | { | ||||||
|  | 	int u = 22; | ||||||
|  | 	int result = exile_launch<int>(exile_init_policy(), &incrementer, u); | ||||||
|  | 	assert(result == 23); | ||||||
|  | 	return 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | int test_exile_launch_stdstring() | ||||||
|  | { | ||||||
|  | 	std::string str = "abc123"; | ||||||
|  | 	std::string reversed = exile_launch<std::string>(exile_init_policy(), &sandboxed_reverse, str); | ||||||
|  | 	assert(reversed == "321cba"); | ||||||
|  | 	return 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct not_trivially_copyable | ||||||
|  | { | ||||||
|  | public: | ||||||
|  | 	std::string somecontent; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | int test_exile_launch_serializer() | ||||||
|  | { | ||||||
|  | 	static_assert(! std::is_trivially_copyable_v<not_trivially_copyable>); | ||||||
|  |  | ||||||
|  | 	auto serializer = [](const not_trivially_copyable &obj, char *buf, size_t n){ | ||||||
|  | 		serialize_stdstring<std::string>(obj.somecontent, buf, n); | ||||||
|  | 		return obj.somecontent.size(); | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	auto deserializer = [](const char *buffer, size_t n) { | ||||||
|  | 		not_trivially_copyable obj; | ||||||
|  | 		obj.somecontent = deserialize_stdstring<std::string>(buffer, n); | ||||||
|  | 		return obj; | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	not_trivially_copyable result = exile_launch<not_trivially_copyable>(exile_init_policy(), serializer, deserializer, []() {not_trivially_copyable obj; obj.somecontent = "Just something"; return obj;}); | ||||||
|  |  | ||||||
|  | 	assert(result.somecontent == "Just something"); | ||||||
|  | 	return 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | int main(int argc, char *argv[]) | ||||||
|  | { | ||||||
|  | 	if(argc < 2) | ||||||
|  | 	{ | ||||||
|  | 		std::cerr << "Missing test" << std::endl; | ||||||
|  | 		return 1; | ||||||
|  | 	} | ||||||
|  | 	std::map<std::string, int (*)()> map = { | ||||||
|  | 		{ "launch-trivial-cpp", &test_exile_launch_trivial} , | ||||||
|  | 		{ "launch-stdstring-cpp", &test_exile_launch_stdstring }, | ||||||
|  | 		{ "launch-serializer-cpp", &test_exile_launch_serializer }, | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	std::string test = argv[1]; | ||||||
|  | 	if(test == "--dumptests") | ||||||
|  | 	{ | ||||||
|  | 		for(auto &entry : map) | ||||||
|  | 		{ | ||||||
|  | 			std::cout << entry.first << std::endl; | ||||||
|  | 		} | ||||||
|  | 		return 0; | ||||||
|  | 	} | ||||||
|  | 	int (*fn)() = map[test]; | ||||||
|  | 	if(fn != nullptr) | ||||||
|  | 	{ | ||||||
|  | 		return fn(); | ||||||
|  | 	} | ||||||
|  | 	std::cerr << "Unknown test" << std::endl; | ||||||
|  | 	return 1; | ||||||
|  | } | ||||||
							
								
								
									
										18
									
								
								test.sh
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								test.sh
									
									
									
									
									
								
							| @@ -44,14 +44,15 @@ function runtest_skipped() | |||||||
|  |  | ||||||
| function runtest() | function runtest() | ||||||
| { | { | ||||||
| 	testname="$1" | 	testbin="$1" | ||||||
| 	test_log_file="$2" | 	testname="$2" | ||||||
|  | 	test_log_file="$3" | ||||||
|  |  | ||||||
| 	echo "Running: $testname. Date: $(date)" > "${test_log_file}" | 	echo "Running: $testname. Date: $(date)" > "${test_log_file}" | ||||||
|  |  | ||||||
| 	echo -n "Running $1... " | 	echo -n "Running $testname... " | ||||||
| 	#exit $? to suppress shell message like "./test.sh: line 18: pid Bad system call" | 	#exit $? to suppress shell message like "./test.sh: line 18: pid Bad system call" | ||||||
| 	(./test $1 || exit $?) &>> "${test_log_file}" | 	(./$testbin "$testname" || exit $?) &>> "${test_log_file}" | ||||||
| 	ret=$? | 	ret=$? | ||||||
| 	SUCCESS="no" | 	SUCCESS="no" | ||||||
| 	if [ $ret -eq 0 ] ; then | 	if [ $ret -eq 0 ] ; then | ||||||
| @@ -64,7 +65,7 @@ function runtest() | |||||||
| 		runtest_fail | 		runtest_fail | ||||||
| 	fi | 	fi | ||||||
|  |  | ||||||
| 	echo "Finished: ${testname}. Date: $(date). Success: $SUCCESS" >> "${test_log_file}" | 	echo "Finished: ${testname} (${testbin}). Date: $(date). Success: $SUCCESS" >> "${test_log_file}" | ||||||
| } | } | ||||||
|  |  | ||||||
| GIT_ID=$( git log --pretty="format:%h" -n1 ) | GIT_ID=$( git log --pretty="format:%h" -n1 ) | ||||||
| @@ -79,7 +80,12 @@ LOG_OUTPUT_DIR_PATH="${LOG_OUTPUT_DIR}/exile_test_${GIT_ID}_${TIMESTAMP}" | |||||||
|  |  | ||||||
| for test in $( ./test --dumptests ) ; do | for test in $( ./test --dumptests ) ; do | ||||||
| 	testname=$( echo $test ) | 	testname=$( echo $test ) | ||||||
| 	runtest "$testname" "${LOG_OUTPUT_DIR_PATH}/log.${testname}" | 	runtest test "$testname" "${LOG_OUTPUT_DIR_PATH}/log.${testname}" | ||||||
|  | done | ||||||
|  |  | ||||||
|  | for test in $( ./testcpp --dumptests ) ; do | ||||||
|  | 	testname=$( echo $test ) | ||||||
|  | 	runtest testcpp "$testname" "${LOG_OUTPUT_DIR_PATH}/log.${testname}" | ||||||
| done | done | ||||||
| echo | echo | ||||||
| echo "Tests finished. Logs in $(realpath ${LOG_OUTPUT_DIR_PATH})" | echo "Tests finished. Logs in $(realpath ${LOG_OUTPUT_DIR_PATH})" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user