Compare commits

..

46 Commits

Author SHA1 Message Date
4cfdead5d0 no_fs: Use landlock if possible
This is not 100% the same, but good enough and more importantly,
does not require unsharing user/mount namespace and the chroot call.
2024-05-26 20:12:20 +02:00
bbc8193ea9 Handle newer landlock ABI versions for filesystem isolation 2024-05-26 20:03:20 +02:00
c9fdeb4a1d enter_namespaces(): Add missing newline at error messages 2024-05-26 19:31:14 +02:00
3732524bfa exile_init_policy(): Don't unshare network namespaces by default
This no longer works on some distros (e. g. Ubuntu 24.04) which
move (back) to restrict unprivileged user namespaces, and is
not required when Landlock is available, which is more and more
a given, thankfully.
2024-05-26 19:28:02 +02:00
4059c1a093 landlock_prepare_ruleset(): zero-init landlock structs
'landlock_ruleset_attr' used to only have a single member. Meanwhile,
depending on linux/headers version, others may be present. So zero-init
the struct, as otherwise we might get 'Invalid argument' return codes,
as those we do not explicitly initialize might contain garbage values.
2024-05-24 13:25:10 +02:00
44b9a17bec Allow specifying uid/gid to map in user namespace 2022-12-27 13:25:12 +01:00
f662398ac3 test: test_launch_get(): Fix typo and remove redundant call 2022-12-27 13:14:39 +01:00
7b859d0aed exile_launch_get(): Remove redundant seek 2022-12-26 18:36:17 +01:00
5cd0a36ced test.sh: Fix regression causing status code to be lost
The changes in 01c5cbf701
did not take into account that "tee" would change the exit code.

Use a protable alternative to &>> now.
2022-12-26 18:29:32 +01:00
618f223491 enter_namespaces(): Fix uid/gid mapping
This was not caught before because a test was missing, fprintf() without ferror()
didn't help, and calling code did not depend on uid maps so far.

Add tests.
2022-12-26 18:23:34 +01:00
01c5cbf701 test.sh: Make it more portable 2022-12-20 10:50:42 +01:00
769f729dc5 README.md: Update 2022-10-26 10:27:38 +02:00
40d23af355 concat_path(): Add missing free() calls 2022-10-23 19:54:21 +02:00
b5f83499f3 exile_append_syscall_policy(): Add missing free() 2022-10-23 19:52:56 +02:00
ff60ec227d perform_mounts(): Fix potential leak and fix iteration
We would not free 'concat_path' in all potential paths.
Also, the iteration would not continue potentially.

This was case unlikely to be hit in practise.
2022-10-23 19:48:33 +02:00
e711a1d53a exile_landlock_is_available(): Fix availability check
The check only assumed the existance of ABI version 1, which
is not the case any more.

Closes: https://github.com/quitesimpleorg/exile.h/issues/1
2022-08-16 23:07:49 +02:00
6628bf4fb7 README: Update and minor improvements 2022-08-16 23:07:42 +02:00
3fa73b0b97 Close file fds by default, introduce policy->keep_fds_open
The better default is to close them, not keeping them open.

Does not close sockets and pipes to not interfere with IPC.

Issue: #10
2022-07-17 13:00:02 +02:00
8f38dc4480 check_policy_sanity(): Allow vows and syscall policies
Adjust checks to allow a mixed mode between syscall policies and vows.
Check for some easy to make mistakes in such scenario.
2022-06-09 10:02:12 +02:00
42d44b0cc1 README.md: Minor improvements throughout the file 2022-06-06 14:07:37 +02:00
bd3641981c Introduce EXILE_SYSCALL_DENY_RET_NOSYS for syscalls like clone3()
clone3() is used more and more, but we cannot filter it. We can either
allow it fully or return ENONYS. Some libraries perform fallbacks to the
older clone() in that case, which we can filter again.
2022-06-06 14:07:37 +02:00
bbbdfc44da exile.hpp: do_clone(): free stack memory 2022-05-29 19:25:53 +02:00
2dc61828f1 README: Clarify limitations 2022-04-29 21:25:21 +02:00
cdc265cedf c++: exile_launch(): Correct std::enable_if logic if type is a ptr 2022-04-29 21:23:53 +02:00
91858efa51 vows map: Add memfd_create, rseq 2022-04-22 08:37:34 +02:00
88995d214d README.md: Minor improvements (typos, rephrasing) 2022-04-07 00:04:52 +02:00
6eb47daf84 README: Update Debian section 2022-03-28 19:25:55 +02:00
8bf87717a5 vows: ioctl: Make TIOCSTI illegal even when IOCTL vow is set 2022-03-28 19:14:02 +02:00
bcaefffbe8 Improve various error messages 2022-03-28 19:04:28 +02:00
ed5098f2c6 README: Begin demo section 2022-03-17 17:10:38 +01:00
ea66ef76eb exile_flags_to_landlock(): Cover more with ALL_WRITE, except devices
More consistent with mount(), where MS_NODEV disallows those.

We may need to introduce a flag that simply allows everything
2022-03-17 15:47:22 +01:00
66def7a28f append_syscall_to_bpf(): Check for unlikely case of too many sock_filters 2022-03-17 15:47:22 +01:00
dbf8e87440 exile.hpp: Mark do_clone inline, not static 2022-03-17 15:47:22 +01:00
98421fab90 Makefile: Build exile.o separately, link it in all tests 2022-03-17 15:47:22 +01:00
70c3fef500 exile.h: Retire static child_read/write_pipe vars 2022-03-17 15:47:22 +01:00
69829374c7 exile.h: Move definitions to new file exile.c
Especially with exile_launch(), we will be included
from more than one translation unit. Thus, ODR becomes
a headache now.

So move definitions to exile.c.
2022-03-17 15:47:22 +01:00
005851c645 exile.h: Add extern "C" guards 2022-03-17 15:47:22 +01:00
95fa11e928 c++: Add explicit exile_launch() std::basic_string variant 2022-03-17 15:47:22 +01:00
97e2025758 c++: Retire exile_launch_trivial(), use std::enable_if 2022-03-17 15:47:22 +01:00
8cfb73568a Makefile: Add 'tests' target, depend on headers too to rebuild on changes of those 2022-03-17 15:47:22 +01:00
e7a5ba7f7f test.sh: Also run C++ tests 2022-03-17 15:47:22 +01:00
e52eda186b Add test.cpp to test C++ API 2022-03-17 15:47:22 +01:00
90ed5bbae9 Begin C++ API: Add exile.hpp with exile_launch() wrappers 2022-03-17 15:47:22 +01:00
48b6de9036 struct syscall_vow_map: change 'str' to const char* 2022-03-17 15:47:22 +01:00
93acb13929 test: Introduce LOG(), avoid inconsistent printf/fprintf 2022-03-17 15:47:22 +01:00
9247a6636b Introduce exile_vows_from_str() 2022-03-17 15:47:22 +01:00
6 changed files with 535 additions and 146 deletions

196
README.md
View File

@ -1,60 +1,168 @@
# exile.h
`exile.h` is a header-only library, enabling processes to easily isolate themselves on Linux for exploit mitigation. exile.h wants to make existing technologies, such as Seccomp and Linux Namespaces, easier to use. Those generally
require knowledge of details and are not trivial for developers to employ, which prevents a more widespread adoption.
`exile.h` provides an API for processes on Linux to easily isolate themselves in order
to mitigate the effect of exploited vulnerabilities, i. e. when attacker has achieved
arbitrary code execution. exile.h makes it simpler for developers to use existing technologies such as Seccomp and Linux Namespaces. Those generally require knowledge of details and are not trivial for developers to employ, which prevents a more widespread adoption.
The following section gives small quick examples. Then the motivation is explained in more detail.
Proper API documentation will be maintained in other files.
The following section offers small examples. Then the motivation is explained in more detail. Proper API documentation will be maintained in other files.
## Quick demo
TODO This section will demonstrate the simplicity of the API, but only serves as an overview.
This section quickly demonstrates the simplicity of the API. It serves as an overview to get a first impression.
system() is used to keep the example C code short. It also demonstrates that subprocesses are also subject to restrictions imposed by exile.h.
While the examples show different features separately, it is generally possible to combine those.
### Filesystem isolation
```c
#include "exile.h"
#include <assert.h>
int main(void)
{
system("echo test > /home/user/testfile");
struct exile_policy *policy = exile_init_policy();
exile_append_path_policies(policy, EXILE_FS_ALLOW_ALL_READ, "/home/user");
exile_append_path_policies(policy, EXILE_FS_ALLOW_ALL_READ | EXILE_FS_ALLOW_ALL_WRITE, "/tmp");
int ret = exile_enable_policy(policy);
if(ret != 0)
{
exit(EXIT_FAILURE);
}
int fd = open("/home/user/test", O_CREAT | O_WRONLY | O_TRUNC, 0600);
assert(fd == -1);
fd = open("/home/user/testfile", O_RDONLY);
//use fd
assert(fd != -1);
fd = open("/tmp/testfile", O_CREAT | O_WRONLY | O_TRUNC, 0600);
//use fd
assert(fd != -1);
return 0;
}
```
The assert() calls won't be fired, consistent with the policy that allows only reading
from /home/user. We can write to /tmp/ though as it was specified in the policy.
### vows(): pledge()-like API / System call policies
exile.h allows specifying which syscalls are permitted or denied. In the following example,
'ls' is never executed, as the specified "vows" do not allow the execve() system call. The process will be killed.
### System call policies / vows
```c
#include "exile.h"
int main(void)
{
struct exile_policy *policy = exile_init_policy();
policy->vow_promises = exile_vows_from_str("stdio rpath wpath cpath");
exile_enable_policy(policy);
printf("Trying to execute...");
execlp("/bin/ls", "ls", "/", NULL);
}
```
### Isolation of single functions
exile_launch() demo
### Isolation from network
exile offers a quick way to isolate a process from the default network namespace.
```c
#include "exile.h"
int main(void)
{
struct exile_policy *policy = exile_init_policy();
policy->namespace_options |= EXILE_UNSHARE_NETWORK;
int ret = exile_enable_policy(policy);
if(ret != 0)
{
exit(EXIT_FAILURE);
}
system("curl -I https://evil.tld");
}
```
Produces ```curl: (6) Could not resolve host: evil.tld```. For example, this is useful for subprocesses which do not need
network access, but perform tasks such as parsing user-supplied file formats.
### Isolation of single functions (EXPERIMENTAL)
Currently, work is being done that hopefully will allow isolation of individual function calls in a mostly pain-free manner.
Consider the following C++ code:
```cpp
#include <iostream>
#include <fstream>
#include "exile.hpp"
std::string cat(std::string path)
{
std::fstream f1;
f1.open(path.c_str(), std::ios::in);
std::string content;
std::string line;
while(getline(f1, line)) {
content += line + "\n";
}
return content;
}
int main(void)
{
struct exile_policy *policy = exile_init_policy();
policy->vow_promises = exile_vows_from_str("stdio rpath");
std::string content = exile_launch<std::string>(policy, cat, "/etc/hosts");
std::cout << content;
policy = exile_init_policy();
policy->vow_promises = exile_vows_from_str("stdio");
try
{
content = exile_launch<std::string>(policy, cat, "/etc/hosts");
std::cout << content;
}
catch(std::exception &e)
{
std::cout << "launch failure: " << e.what() << std::endl;
}
}
```
We execute "cat()". The first call succeeds. In the second, we get an exception, because
the subprocess "cat()" was launched in violated the policy (missing "rpath" vow).
Naturally, there is a performance overhead. Certain challenges remain, such as the fact
that being executed in a subproces, we operate on copies, so handling references
is not something that has been given much thought. There is also the fact
that clone()ing from threads opens a can of worms, particularly with locks. Hence, exile_launch() is best avoided in multi-threaded contexts.
## Status
No release yet, experimental, API is unstable, builds will break on updates of this library.
Currently, it's mainly evolving from the needs of my other projects.
Currently, it's mainly evolving from the needs of my other projects which use exile.h.
### Real-world usage
- looqs: https://github.com/quitesimpleorg/looqs
- qswiki: https://gitea.quitesimple.org/crtxcr/qswiki
## Motivation and Background
exile.h unlocks existing Linux mechanisms to facilite isolation of processes from resources. Limiting the scope of what programs can do helps defending the rest of the system when a process gets under attacker's control (when classic mitigations such as ASLR etc. failed). To this end, OpenBSD has the pledge() and unveil() functions available. Those functions are helpful mitigation mechanisms, but such accessible ways are unfortunately not readily available on Linux. This is where exile.h steps in.
exile.h unlocks existing Linux mechanisms to facilitate isolation of processes from resources. Limiting the scope of what programs can do helps defending the rest of the system when a process gets under attacker's control (when classic mitigations such as ASLR etc. failed). To this end, OpenBSD has the pledge() and unveil() functions available. Those functions are helpful mitigation mechanisms, but such accessible ways are unfortunately not readily available on Linux. This is where exile.h steps in.
Seccomp allows to restrict system calls available to a process and thus decrease the systems attack surface, but it generally is not easy to use. Requiring BPF filter instructions, you generally just can't make use of it right away. exile.h provides an API inspired by pledge(), building on top of seccomp. It also provides an interface to manually restrict the system calls that can be issued.
Seccomp allows restricting the system calls available to a process and thus decrease the systems attack surface, but it generally is not easy to use. Requiring BPF filter instructions, you generally just can't make use of it right away without learning
about BPF. exile.h provides an API inspired by pledge(), building on top of seccomp. It also provides an interface to manually restrict the system calls that can be issued.
Traditional methods employed to restrict file system access, like different uids/gids, chroot, bind-mounts, namespaces etc. may require administrator intervention, are perhaps only suitable
for daemons and not desktop applications, or are generally rather involved. As a positive example, Landlock since 5.13 is a vast improvement to limit file system access of processes. It also greatly simplifies exile.h' implementation of fs isolation.
Traditional methods employed to restrict file system access, like different uids/gids, chroot, bind-mounts, namespaces etc. may require administrator intervention, are perhaps only suitable for daemons and not desktop applications, or are generally rather involved. As a positive example, Landlock since 5.13 is a vast improvement to limit file system access of processes. It also greatly simplifies exile.h' implementation of fs isolation.
Abstracting those details may help developers bring sandboxing into their applications.
## Example: Archive extraction
A programming uncompressing archives does not need network access, but should a bug allow code execution, obviously the payload may also access the network. Once the target path is known, it doesn't need access to the whole file system, only write-permissions to the target directory and read on the archive file(s).
TODO example with exile.h applied on "tar" or "unzip". Link to repo.
## Example: Web apps
Those generally don't need access to the whole filesystem hierarchy, nor do they necessarily require the ability to execute other processes.
Way more examples can be given, but we can put it in simple words: A general purpose OS allow a process to do more things than it actually needs to do.
## Features
- Restricting file system access (using Landlock or Namespaces/chroot as fallback)
- Systemcall filtering (using seccomp-bpf). An interface inspired by OpenBSD's pledge() is available, removing the need to specifc rules for syscalls.
- Systemcall filtering (using seccomp-bpf). An interface inspired by OpenBSD's pledge() is available
- Dropping privileges in general, such as capabilities
- Isolating the application from the network, etc. through Namespaces
- Helpers to isolate single functions
## What it's not
A way for end users/administrators to restrict processes. In the future, a wrapper binary may be available to achieve this, but it generally aims for developers to bring sandboxing/isolation into their software, like web browsers do. This allows a more fine-grained approach, as the developers
is more familiar with the software. Applying restrictions with solutions like AppArmor requires
them to be present on the system and it's easy to break things this way.
A way for end users/administrators to restrict processes. In the future, a wrapper binary may be available to achieve this, but it generally aims for developers to bring sandboxing/isolation into their software. This allows a more fine-grained approach, as the developers are more familiar with their software. Applying restrictions with solutions like AppArmor requires
them to be present and installed on the system and it's easy to break things this way.
Therefore, software should ideally be written with sandboxing in mind from the beginning.
@ -66,40 +174,44 @@ It's recommended to start with [README.usage.md] to get a feeling for exile.h.
API-Documentation: [README.api.md]
## Limitations
Built upon kernel technologies, exile.h naturally inherits their limitations:
- New syscalls can be introduced by new kernel versions. exile.h must keep in sync, and users must keep the library up to date.
- seccomp has no deep argument inspection (yet), particularly new syscalls
cannot be reasonably filtered, such as clone3(), or io_uring.
- You can't know what syscalls libraries will issue. An update to existing
libraries may cause them to use different syscalls not allowed by a policy. However, using vows and keeping up to date with exile.h should cover that.
- Landlock, currently, does not apply to syscalls such as stat().
TODO:
- seccomp must be kept up to date syscalls kernel
- ioctl does not know the fd, so checking values is kind of strange
- redundancies: some things are handled by capabilties, other by seccomp or both
- seccomp no deep argument inspection
- landlock: stat() does not apply
- no magic, be reasonable, devs should not get sloppy, restrict IPC.
## Requirements
Kernel >=3.17
While mostly transparent to users of this API, kernel >= 5.13 is required to take advantage of Landlock and furthermore it depends on distro-provided kernels being reasonable and enabling it by default. In practise, this means that Landlock probably won't be used for now, and exile.h will use a combination of namespaces, bind mounts and chroot as fallbacks.
While mostly transparent to users of this API, kernel >= 5.13 is required to take advantage of Landlock. Furthermore, it depends on distro-provided kernels being reasonable and enabling it by default. In practise, Landlock maybe won't be used in some cases so exile.h will use a combination of namespaces, bind mounts and chroot as fallbacks.
## FAQ
### Does the process need to be priviliged to utilize the library?
### Does the process need to be privileged to utilize the library?
No.
### It doesn't work on Debian!
You can thank a Debian-specific kernel patch for that. In the future,
the library may check against that. Execute
### It doesn't work on my Debian version!
You can thank a Debian-specific kernel patch for that. Execute
`echo 1 > /proc/sys/kernel/unprivileged_userns_clone` to disable that patch for now.
### Examples
- looqs: https://gitea.quitesimple.org/crtxcr/looqs
- qswiki: https://gitea.quitesimple.org/crtxcr/qswiki
Note that newer releases should not cause this problem any longer, as [explained](https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html#linux-user-namespaces) in the Debian release notes.
Outdated:
- cgit sandboxed: https://gitea.quitesimple.org/crtxcr/cgitsb
- qpdfviewsb sandboxed (quick and dirty): https://gitea.quitesimple.org/crtxcr/qpdfviewsb
### Why "vows"?
pledge() cannot be properly implemented using seccomp. The "vow" concept here may look similiar, and it is, but it's not pledge().
### Other projects
- [sandbox2](https://developers.google.com/code-sandboxing/sandbox2/)
### Contributing

261
exile.c
View File

@ -274,11 +274,13 @@ static struct syscall_vow_map exile_vow_map[] =
{EXILE_SYS(sched_getattr), EXILE_SYSCALL_VOW_SCHED},
{EXILE_SYS(renameat2), EXILE_SYSCALL_VOW_CPATH},
{EXILE_SYS(getrandom), EXILE_SYSCALL_VOW_STDIO},
{EXILE_SYS(memfd_create), EXILE_SYSCALL_VOW_STDIO},
{EXILE_SYS(execveat), EXILE_SYSCALL_VOW_EXEC},
{EXILE_SYS(mlock2), EXILE_SYSCALL_VOW_STDIO},
{EXILE_SYS(copy_file_range), EXILE_SYSCALL_VOW_STDIO},
{EXILE_SYS(statx), EXILE_SYSCALL_VOW_RPATH},
{EXILE_SYS(clone3), EXILE_SYSCALL_VOW_CLONE},
{EXILE_SYS(rseq), EXILE_SYSCALL_VOW_THREAD},
{EXILE_SYS(clone3), EXILE_SYSCALL_VOW_CLONE|EXILE_SYSCALL_VOW_THREAD},
{EXILE_SYS(close_range), EXILE_SYSCALL_VOW_STDIO},
{EXILE_SYS(openat2), EXILE_SYSCALL_VOW_RPATH|EXILE_SYSCALL_VOW_WPATH},
{EXILE_SYS(faccessat2), EXILE_SYSCALL_VOW_RPATH},
@ -359,10 +361,11 @@ inline int exile_landlock_is_available()
{
#if HAVE_LANDLOCK == 1
int ruleset = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
return ruleset == 1;
return ruleset > 0;
#endif
return 0;
}
int exile_append_syscall_policy(struct exile_policy *exile_policy, long syscall, unsigned int syscall_policy, struct sock_filter *argfilters, size_t n)
{
struct exile_syscall_policy *newpolicy = (struct exile_syscall_policy *) calloc(1, sizeof(struct exile_syscall_policy));
@ -379,6 +382,7 @@ int exile_append_syscall_policy(struct exile_policy *exile_policy, long syscall,
{
EXILE_LOG_ERROR("Too many argfilters supplied\n");
exile_policy->exile_flags |= EXILE_FLAG_ADD_SYSCALL_POLICY_FAIL;
free(newpolicy);
return -1;
}
for(size_t i = 0; i < n; i++)
@ -430,6 +434,7 @@ int get_vow_argfilter(long syscall, uint64_t vow_promises, struct sock_filter *f
struct exile_syscall_filter ioctl_filter[] = {
EXILE_SYSCALL_FILTER_LOAD_ARG(1),
{ EXILE_SYSCALL_VOW_IOCTL, EXILE_BPF_NO_MATCH_SET(TIOCSTI), 1 },
{ EXILE_SYSCALL_VOW_IOCTL, EXILE_BPF_RETURN_MATCHING, 1 },
{ EXILE_SYSCALL_VOW_STDIO, EXILE_BPF_MATCH(FIONREAD), 1},
{ EXILE_SYSCALL_VOW_STDIO, EXILE_BPF_MATCH(FIONBIO), 1},
@ -518,7 +523,7 @@ int get_vow_argfilter(long syscall, uint64_t vow_promises, struct sock_filter *f
current_count = COUNT_EXILE_SYSCALL_FILTER(open_filter);
break;
case EXILE_SYS(openat2):
*policy = EXILE_SYSCALL_DENY_RET_ERROR;
*policy = EXILE_SYSCALL_DENY_RET_NOSYS;
return 0;
break;
case EXILE_SYS(socket):
@ -536,7 +541,7 @@ int get_vow_argfilter(long syscall, uint64_t vow_promises, struct sock_filter *f
case EXILE_SYS(clone3):
if((vow_promises & EXILE_SYSCALL_VOW_CLONE) == 0)
{
*policy = EXILE_SYSCALL_DENY_RET_ERROR;
*policy = EXILE_SYSCALL_DENY_RET_NOSYS;
return 0;
}
break;
@ -616,10 +621,12 @@ struct exile_policy *exile_init_policy()
{
return NULL;
}
result->drop_caps = 1;
result->drop_caps = 0;
result->not_dumpable = 1;
result->no_new_privs = 1;
result->namespace_options = EXILE_UNSHARE_MOUNT | EXILE_UNSHARE_USER;
result->namespace_options = EXILE_UNSHARE_AUTOMATIC;
result->namespace_uid = 0;
result->namespace_gid = 0;
return result;
}
@ -643,7 +650,7 @@ int (exile_append_path_policies)(struct exile_policy *exile_policy, unsigned int
int fd = open(path, O_PATH);
if(fd == -1)
{
EXILE_LOG_ERROR("Failed to open the specified path: %s\n", strerror(errno));
EXILE_LOG_ERROR("Failed to open %s: %s\n", path, strerror(errno));
exile_policy->exile_flags |= EXILE_FLAG_ADD_PATH_POLICY_FAIL;
return -1;
}
@ -811,11 +818,13 @@ char *concat_path(const char *first, const char *second)
if(written < 0)
{
EXILE_LOG_ERROR("Error during path concatination\n");
free(result);
return NULL;
}
if(written >= PATH_MAX)
{
EXILE_LOG_ERROR("path concatination truncated\n");
free(result);
return NULL;
}
return result;
@ -851,7 +860,7 @@ static int create_chroot_dirs(const char *chroot_target_path, struct exile_path_
ret = mkpath(path_inside_chroot, 0700, baseisfile);
if(ret < 0)
{
EXILE_LOG_ERROR("Error creating directory structure while mounting paths to chroot. %s\n", strerror(errno));
EXILE_LOG_ERROR("Error creating directory structure %s while mounting paths to chroot: %s\n", path_inside_chroot, strerror(errno));
free(path_inside_chroot);
return ret;
}
@ -865,6 +874,8 @@ static int create_chroot_dirs(const char *chroot_target_path, struct exile_path_
static int perform_mounts(const char *chroot_target_path, struct exile_path_policy *path_policy)
{
while(path_policy != NULL)
{
if(path_policy->policy & EXILE_FS_ALLOW_ALL_READ || path_policy->policy & EXILE_FS_ALLOW_ALL_WRITE)
{
int mount_flags = get_policy_mount_flags(path_policy);
@ -876,8 +887,6 @@ static int perform_mounts(const char *chroot_target_path, struct exile_path_poli
//all we do is bind mounts
mount_flags |= MS_BIND;
if(path_policy->policy & EXILE_FS_ALLOW_ALL_READ || path_policy->policy & EXILE_FS_ALLOW_ALL_WRITE)
{
int ret = mount(path_policy->path, path_inside_chroot, NULL, mount_flags, NULL);
if(ret < 0 )
{
@ -894,9 +903,10 @@ static int perform_mounts(const char *chroot_target_path, struct exile_path_poli
free(path_inside_chroot);
return ret;
}
path_policy = path_policy->next;
free(path_inside_chroot);
}
path_policy = path_policy->next;
}
return 0;
}
@ -930,10 +940,15 @@ void exile_free_policy(struct exile_policy *ctxt)
}
/* Enters the specified namespaces */
static int enter_namespaces(int namespace_options)
static int enter_namespaces(int namespace_options, uid_t namespace_uid, gid_t namespace_gid)
{
if(namespace_options & EXILE_UNSHARE_USER)
{
uid_t current_uid = getuid();
gid_t current_gid = getgid();
char buf[1024] = {0};
int ret = unshare(CLONE_NEWUSER);
if(ret == -1)
{
@ -941,47 +956,51 @@ static int enter_namespaces(int namespace_options)
return ret;
}
uid_t current_uid = getuid();
gid_t current_gid = getgid();
int fd = open("/proc/self/setgroups", O_WRONLY);
if(fd == -1)
{
EXILE_LOG_ERROR("Failed to open /proc/self/setgroups for writing\n");
return -1;
}
int writesize = snprintf(buf, sizeof(buf), "deny");
int writeret = write(fd, buf, writesize);
if(writeret < 0 || writeret < writesize)
{
EXILE_LOG_ERROR("Failed to write to /proc/self/setgroups: %i (%s)\n", writeret, strerror(errno));
return -1;
}
close(fd);
FILE *fp = fopen("/proc/self/setgroups", "w");
if(fp == NULL)
fd = open("/proc/self/uid_map", O_WRONLY);
if(fd == -1)
{
EXILE_LOG_ERROR("fopen failed while trying to deny setgroups\n");
EXILE_LOG_ERROR("Failed to open /proc/self/uid_map for writing\n");
return -1;
}
if(fprintf(fp, "deny") < 0)
writesize = snprintf(buf, sizeof(buf), "%u %u 1\n", namespace_uid, current_uid);
writeret = write(fd, buf, writesize);
if(writeret < 0 || writeret < writesize)
{
EXILE_LOG_ERROR("fprintf failed while trying to write setgroups\n");
EXILE_LOG_ERROR("Failed to write to /proc/self/uid_map: %i (%s)\n", writeret, strerror(errno));
return -1;
}
fclose(fp);
close(fd);
fp = fopen("/proc/self/uid_map", "w");
if(fp == NULL)
{
EXILE_LOG_ERROR("fopen failed while trying to write uid_map\n");
return -1;
}
if(fprintf(fp, "0 %i", current_uid) < 0)
{
EXILE_LOG_ERROR("fprintf failed while trying to write uid_map\n");
return -1;
}
fclose(fp);
fp = fopen("/proc/self/gid_map", "w");
if(fp == NULL)
fd = open("/proc/self/gid_map", O_WRONLY);
if(fd == -1)
{
EXILE_LOG_ERROR("fopen failed while trying to write gid_map\n");
EXILE_LOG_ERROR("Failed to open /proc/self/gid_map for writing\n");
return -1;
}
if(fprintf(fp, "0 %i", current_gid) < 0)
writesize = snprintf(buf, sizeof(buf), "%u %u 1\n", namespace_gid, current_gid);
writeret = write(fd, buf, writesize);
if(writeret < 0 || writeret < writesize)
{
EXILE_LOG_ERROR("fprintf failed while trying to write gid_map\n");
EXILE_LOG_ERROR("Failed to write to /proc/self/gid_map: %i (%s)\n", writeret, strerror(errno));
return -1;
}
fclose(fp);
close(fd);
}
if(namespace_options & EXILE_UNSHARE_MOUNT)
@ -1072,6 +1091,10 @@ static struct sock_filter *append_syscall_to_bpf(struct exile_syscall_policy *sy
{
action = SECCOMP_RET_ERRNO|EACCES;
}
if(action == EXILE_SYSCALL_DENY_RET_NOSYS)
{
action = SECCOMP_RET_ERRNO|ENOSYS;
}
long syscall = syscallpolicy->syscall;
struct sock_filter syscall_load = BPF_STMT(BPF_LD+BPF_W+BPF_ABS, offsetof(struct seccomp_data, nr));
@ -1138,7 +1161,7 @@ static struct sock_filter *append_syscall_to_bpf(struct exile_syscall_policy *sy
static int is_valid_syscall_policy(unsigned int policy)
{
return policy == EXILE_SYSCALL_ALLOW || policy == EXILE_SYSCALL_DENY_RET_ERROR || policy == EXILE_SYSCALL_DENY_KILL_PROCESS;
return policy == EXILE_SYSCALL_ALLOW || policy == EXILE_SYSCALL_DENY_RET_ERROR || policy == EXILE_SYSCALL_DENY_KILL_PROCESS || policy == EXILE_SYSCALL_DENY_RET_NOSYS;
}
/*
@ -1206,12 +1229,21 @@ static unsigned int exile_flags_to_landlock(unsigned int flags, int statmode)
if(flags & EXILE_FS_ALLOW_ALL_WRITE)
{
result |= LANDLOCK_ACCESS_FS_WRITE_FILE;
#ifdef LANDLOCK_ACCESS_FS_TRUNCATE
result |= LANDLOCK_ACCESS_FS_TRUNCATE;
#endif
if(S_ISDIR(statmode))
{
result |= LANDLOCK_ACCESS_FS_REMOVE_FILE;
result |= LANDLOCK_ACCESS_FS_MAKE_REG;
result |= LANDLOCK_ACCESS_FS_REMOVE_DIR;
result |= LANDLOCK_ACCESS_FS_REMOVE_FILE;
result |= LANDLOCK_ACCESS_FS_MAKE_DIR;
result |= LANDLOCK_ACCESS_FS_MAKE_FIFO;
result |= LANDLOCK_ACCESS_FS_MAKE_REG;
result |= LANDLOCK_ACCESS_FS_MAKE_SOCK;
result |= LANDLOCK_ACCESS_FS_MAKE_SYM;
#ifdef LANDLOCK_ACCESS_FS_REFER
result |= LANDLOCK_ACCESS_FS_REFER;
#endif
}
}
if(flags & EXILE_FS_ALLOW_EXEC)
@ -1278,15 +1310,42 @@ static unsigned int exile_flags_to_landlock(unsigned int flags, int statmode)
return result;
}
/* Sets maximum values for the handled access fs... */
static int landlock_set_max_handled_access(struct landlock_ruleset_attr *ruleset)
{
int abi = landlock_create_ruleset(NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION);
if(abi < 0)
{
EXILE_LOG_ERROR("Can't determine landlock ABI version\n");
return -1;
}
ruleset->handled_access_net = 0;
if(abi == 1)
{
ruleset->handled_access_fs = ((LANDLOCK_ACCESS_FS_MAKE_SYM << 1) - 1);
}
if(abi == 2)
{
ruleset->handled_access_fs = ((LANDLOCK_ACCESS_FS_REFER << 1) - 1);
}
if(abi >= 3)
{
ruleset->handled_access_fs = ((LANDLOCK_ACCESS_FS_TRUNCATE << 1) - 1);
/* TODO: think about net */
}
return 0;
}
static int landlock_prepare_ruleset(struct exile_path_policy *policies)
{
int ruleset_fd = -1;
struct landlock_ruleset_attr ruleset_attr;
/* We here want the maximum possible ruleset, so set the var to the max possible bitmask.
Stolen/Adapted from: [linux src]/security/landlock/limits.h
*/
ruleset_attr.handled_access_fs = ((LANDLOCK_ACCESS_FS_MAKE_SYM << 1) - 1);
struct landlock_ruleset_attr ruleset_attr = {0};
if(landlock_set_max_handled_access(&ruleset_attr) != 0)
{
return -1;
}
ruleset_fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
{
@ -1296,7 +1355,7 @@ static int landlock_prepare_ruleset(struct exile_path_policy *policies)
struct exile_path_policy *policy = policies;
while(policy != NULL)
{
struct landlock_path_beneath_attr path_beneath;
struct landlock_path_beneath_attr path_beneath = {0};
path_beneath.parent_fd = open(policy->path, O_PATH | O_CLOEXEC);
if(path_beneath.parent_fd < 0)
{
@ -1313,6 +1372,13 @@ static int landlock_prepare_ruleset(struct exile_path_policy *policies)
return ret;
}
path_beneath.allowed_access = exile_flags_to_landlock(policy->policy, sb.st_mode);
/* Required, so the .allowed_access fits .handled_access_fs of the ruleset.
* Needed for backwards compatibility, e. g. new binary compiled with new headers,
executed on a kernel with an older ABI version which does not have some constant defined...
*/
path_beneath.allowed_access &= ruleset_attr.handled_access_fs;
ret = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0);
if(ret)
{
@ -1347,7 +1413,7 @@ static int check_policy_sanity(struct exile_policy *policy)
{
if(path_policy_needs_landlock(path_policy))
{
EXILE_LOG_ERROR("A path policy needs landlock, but landlock is not available. Fallback not possible\n");
EXILE_LOG_ERROR("A path policy (%s) needs landlock, but landlock is not available. Fallback not possible\n", path_policy->path);
return -1;
}
path_policy = path_policy->next;
@ -1400,6 +1466,11 @@ static int check_policy_sanity(struct exile_policy *policy)
{
if(syscall_policy->syscall == EXILE_SYSCALL_MATCH_ALL)
{
if(policy->vow_promises != 0)
{
EXILE_LOG_ERROR("It's not possible to specify a default, all matching syscall policy while also using vows\n");
return -1;
}
last_match_all = i;
match_all_policy = syscall_policy->policy;
}
@ -1410,7 +1481,7 @@ static int check_policy_sanity(struct exile_policy *policy)
syscall_policy = syscall_policy->next;
++i;
}
if(last_match_all == -1 || i - last_match_all != 1)
if(policy->vow_promises == 0 && (last_match_all == -1 || i - last_match_all != 1))
{
EXILE_LOG_ERROR("The last entry in the syscall policy list must match all syscalls (default rule)\n");
return -1;
@ -1431,7 +1502,20 @@ static void close_file_fds()
long max_files = sysconf(_SC_OPEN_MAX);
for(long i = 3; i <= max_files; i++)
{
close((int)i);
struct stat statbuf;
int fd = (int) max_files;
int result = fstat(i, &statbuf);
if(result == -1 && errno != EBADF && errno != EACCES)
{
EXILE_LOG_ERROR("Could not fstat %i: %s\n", fd, strerror(errno));
abort();
}
int type = statbuf.st_mode & S_IFMT;
if(type != S_IFIFO && type != S_IFSOCK)
{
/* No error check, retrying not recommended */
close(fd);
}
}
}
@ -1448,6 +1532,30 @@ static int enable_no_fs(struct exile_policy *policy)
{
close_file_fds();
if(exile_landlock_is_available())
{
struct landlock_ruleset_attr ruleset_attr = {0};
if(landlock_set_max_handled_access(&ruleset_attr) != 0)
{
return -1;
}
int ruleset_fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
{
EXILE_LOG_ERROR("Failed to create landlock ruleset\n");
return -1;
}
int ret = landlock_restrict_self(ruleset_fd, 0);
if(ret != 0)
{
EXILE_LOG_ERROR("Failed to enable no_fs with landlock: %s\n", strerror(errno));
close(ruleset_fd);
return -1;
}
close(ret);
return 0;
}
if(chdir("/proc/self/fdinfo") != 0)
{
EXILE_LOG_ERROR("Failed to change to safe directory: %s\n", strerror(errno));
@ -1494,7 +1602,12 @@ int exile_enable_policy(struct exile_policy *policy)
return -EINVAL;
}
if(enter_namespaces(policy->namespace_options) < 0)
if(policy->keep_fds_open != 1)
{
close_file_fds();
}
if(enter_namespaces(policy->namespace_options, policy->namespace_uid, policy->namespace_gid) < 0)
{
EXILE_LOG_ERROR("Error while trying to enter namespaces\n");
return -1;
@ -1577,14 +1690,6 @@ int exile_enable_policy(struct exile_policy *policy)
}
#endif
if(policy->no_fs)
{
if(enable_no_fs(policy) != 0)
{
EXILE_LOG_ERROR("Failed to take away filesystem access of process\n");
return -1;
}
}
if(policy->no_new_fds)
{
@ -1596,15 +1701,6 @@ int exile_enable_policy(struct exile_policy *policy)
}
}
if(policy->drop_caps)
{
if(drop_caps() < 0)
{
EXILE_LOG_ERROR("failed to drop capabilities\n");
return -1;
}
}
if(policy->not_dumpable)
{
if(prctl(PR_SET_DUMPABLE, 0) == -1)
@ -1623,6 +1719,15 @@ int exile_enable_policy(struct exile_policy *policy)
}
}
if(policy->no_fs)
{
if(enable_no_fs(policy) != 0)
{
EXILE_LOG_ERROR("Failed to take away filesystem access of process\n");
return -1;
}
}
#if HAVE_LANDLOCK == 1
if (can_use_landlock && policy->path_policies != NULL && landlock_restrict_self(landlock_ruleset_fd, 0) != 0)
{
@ -1643,12 +1748,19 @@ int exile_enable_policy(struct exile_policy *policy)
}
}
if(policy->drop_caps)
{
if(drop_caps() < 0)
{
EXILE_LOG_ERROR("failed to drop capabilities\n");
return -1;
}
}
if(policy->syscall_policies != NULL)
{
return exile_enable_syscall_policy(policy);
}
return 0;
}
@ -1841,13 +1953,6 @@ char *exile_launch_get(struct exile_launch_params *launch_params, size_t *n)
}
}
fclose(stream);
int seek = fseek(stream, 0, SEEK_SET);
if(seek == -1)
{
EXILE_LOG_ERROR("fseek failed\n");
close(launch_result.read_fd);
return NULL;
}
close(launch_result.read_fd);
*n = size;
return result;

View File

@ -75,6 +75,7 @@
#define EXILE_UNSHARE_NETWORK 1<<1
#define EXILE_UNSHARE_USER 1<<2
#define EXILE_UNSHARE_MOUNT 1<<3
#define EXILE_UNSHARE_AUTOMATIC 1<<4
#ifndef EXILE_LOG_ERROR
#define EXILE_LOG_ERROR(...) do { fprintf(stderr, "exile.h: %s(): Error: ", __func__); fprintf(stderr, __VA_ARGS__); } while(0)
@ -273,6 +274,7 @@ struct exile_path_policy
#define EXILE_SYSCALL_ALLOW 1
#define EXILE_SYSCALL_DENY_KILL_PROCESS 2
#define EXILE_SYSCALL_DENY_RET_ERROR 3
#define EXILE_SYSCALL_DENY_RET_NOSYS 4
#define EXILE_BPF_NOP \
BPF_STMT(BPF_JMP+BPF_JA,0)
@ -362,6 +364,7 @@ struct exile_policy
int no_new_privs;
int no_fs;
int no_new_fds;
int keep_fds_open;
int namespace_options;
int disable_syscall_filter;
/* Bind mounts all paths in path_policies into the chroot and applies
@ -372,6 +375,9 @@ struct exile_policy
uint64_t vow_promises;
uid_t namespace_uid;
gid_t namespace_gid;
/* Do not manually add policies here, use exile_append_path_policies() */
struct exile_path_policy *path_policies;
struct exile_path_policy **path_policies_tail;

View File

@ -100,6 +100,7 @@ inline int do_clone(int (*clonefn)(void *), void *launcharg)
}
size_t size = rlimit.rlim_cur;
char *stack = (char *) calloc(1, size);
char *stackbegin = stack;
if(stack == NULL)
{
EXILE_LOG_ERROR("Failed to allocate stack memory for child\n");
@ -110,6 +111,7 @@ inline int do_clone(int (*clonefn)(void *), void *launcharg)
ret = clone(clonefn, stack, 17 /* SIGCHLD */, launcharg);
int status = 0;
waitpid(ret, &status, __WALL);
free(stackbegin);
if(WIFEXITED(status))
{
return WEXITSTATUS(status);
@ -119,7 +121,7 @@ inline int do_clone(int (*clonefn)(void *), void *launcharg)
}
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)
typename std::enable_if_t<std::is_trivially_copyable_v<T> && !std::is_pointer_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);
@ -145,7 +147,7 @@ typename std::enable_if_t<std::is_trivially_copyable_v<T>, T> exile_launch(struc
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>
typename std::enable_if_t<std::is_pointer_v<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;

167
test.c
View File

@ -618,9 +618,9 @@ int test_launch_get()
size_t n = 0;
char *content = exile_launch_get(&params, &n);
unsigned int len = strlen(LAUNCH_GET_TEST_STR);
if(n != strlen(LAUNCH_GET_TEST_STR))
if(n != len)
{
LOG("Lenght does does not match: %lu vs %u\n", n, len);
LOG("Lenght does not match: %lu vs %u\n", n, len);
return 1;
}
if(strcmp(content, LAUNCH_GET_TEST_STR) != 0)
@ -643,6 +643,164 @@ int test_vows_from_str()
return 0;
}
int test_clone3_nosys()
{
struct exile_policy *policy = exile_init_policy();
policy->vow_promises = exile_vows_from_str("stdio rpath wpath cpath thread error");
exile_enable_policy(policy);
/* While args are invalid, it should never reach clone3 syscall handler, so it's irrelevant for
our test*/
long ret = syscall(__NR_clone3, NULL, 0);
if(ret == -1 && errno != ENOSYS)
{
LOG("clone3() was not allowed but did not return ENOSYS. It returned: %li, errno: %i\n", ret, errno);
return 1;
}
return 0;
}
int do_test_nsuidmap(const char *path, const char *firstfield, const char *secondfield, const char *thirdfield)
{
char *line = NULL;
size_t n = 0;
FILE *fp = fopen(path, "r");
int ret = getdelim(&line, &n, ' ', fp);
while(ret != -1 && strlen(line) == 1 && *line == ' ')
ret = getdelim(&line, &n, ' ', fp);
if(ret == -1)
{
LOG("getdelim() failed to read a line from %s\n", path);
return 1;
}
line[ret-1] = '\0';
if(strcmp(line, firstfield) != 0)
{
LOG("Invalid value for first entry in map: Expected: %s, was: %s\n", firstfield, line);
return 1;
}
ret = getdelim(&line, &n, ' ', fp);
while(ret != -1 && strlen(line) == 1 && *line == ' ')
ret = getdelim(&line, &n, ' ', fp);
if(ret == -1)
{
LOG("getdelim() failed to read a line from map\n");
return 1;
}
line[ret-1] = '\0';
if(strcmp(line, secondfield) != 0)
{
LOG("Invalid value for second entry in map: Expected: %s, was: %s\n", secondfield, line);
return 1;
}
ret = getdelim(&line, &n, ' ', fp);
while(ret != -1 && strlen(line) == 1 && *line == ' ')
ret = getdelim(&line, &n, ' ', fp);
if(ret == -1)
{
LOG("getdelim() failed to read a line from uid_map\n");
return 1;
}
line[ret-1] = '\0';
if(strcmp(line, thirdfield) != 0)
{
LOG("Invalid value for second entry in map: Expected: %s, was: %s\n", thirdfield, line);
return 1;
}
fclose(fp);
return 0;
}
int test_unshare_user()
{
char uidstr[64];
snprintf(uidstr, sizeof(uidstr), "%u", getuid());
char gidstr[64];
snprintf(gidstr, sizeof(gidstr), "%u", getgid());
struct exile_policy *policy = exile_init_policy();
policy->namespace_options = EXILE_UNSHARE_USER;
xexile_enable_policy(policy);
if(do_test_nsuidmap("/proc/self/uid_map", "0", uidstr, "1") != 0)
{
LOG("/proc/self/uid_map failed\n");
return 1;
}
if(do_test_nsuidmap("/proc/self/gid_map", "0", gidstr, "1") != 0)
{
LOG("/proc/self/gid_map failed\n");
return 1;
}
FILE *fp = fopen("/proc/self/setgroups", "r");
char buffer[4096] = { 0 };
fread(buffer, sizeof(buffer), 1, fp);
fclose(fp);
if(strcmp(buffer, "deny\n") != 0)
{
LOG("/proc/self/setgroups does not contain 'deny'\n");
return 1;
}
return 0;
}
int test_unshare_user_own_uid()
{
uid_t uid = getuid();
gid_t gid = getgid();
char uidstr[64];
snprintf(uidstr, sizeof(uidstr), "%u", uid);
char gidstr[64];
snprintf(gidstr, sizeof(gidstr), "%u", gid);
struct exile_policy *policy = exile_init_policy();
policy->namespace_options = EXILE_UNSHARE_USER;
policy->namespace_gid = gid;
policy->namespace_uid = uid;
xexile_enable_policy(policy);
if(do_test_nsuidmap("/proc/self/uid_map", uidstr, uidstr, "1") != 0)
{
LOG("/proc/self/uid_map failed\n");
return 1;
}
if(do_test_nsuidmap("/proc/self/gid_map", gidstr, gidstr, "1") != 0)
{
LOG("/proc/self/gid_map failed\n");
return 1;
}
FILE *fp = fopen("/proc/self/setgroups", "r");
char buffer[4096] = { 0 };
fread(buffer, sizeof(buffer), 1, fp);
fclose(fp);
if(strcmp(buffer, "deny\n") != 0)
{
LOG("/proc/self/setgroups does not contain 'deny'\n");
return 1;
}
return 0;
}
struct dispatcher
{
char *name;
@ -670,6 +828,11 @@ struct dispatcher dispatchers[] = {
{ "launch", &test_launch},
{ "launch-get", &test_launch_get},
{ "vow_from_str", &test_vows_from_str},
{ "clone3_nosys", &test_clone3_nosys},
{ "unshare-user", &test_unshare_user},
{ "unshare-user-own-uid", &test_unshare_user_own_uid},
};
int main(int argc, char *argv[])

23
test.sh
View File

@ -8,41 +8,41 @@ COUNT_SUCCEEDED=0
COUNT_FAILED=0
COUNT_SKIPPED=0
function print_fail()
print_fail()
{
echo -e "${RED}$@${NC}" 1>&2
printf "${RED}$@${NC}\n" 1>&2
}
function print_success()
print_success()
{
echo -e "${GREEN}$@${NC}"
printf "${GREEN}$@${NC}\n"
}
function print_skipped()
print_skipped()
{
echo -e "${YELLOW}$@${NC}"
printf "${YELLOW}$@${NC}\n"
}
function runtest_fail()
runtest_fail()
{
print_fail "failed"
COUNT_FAILED=$(($COUNT_FAILED+1))
}
function runtest_success()
runtest_success()
{
print_success "ok"
COUNT_SUCCEEDED=$((COUNT_SUCCEEDED+1))
}
function runtest_skipped()
runtest_skipped()
{
print_skipped "skipped"
COUNT_SKIPPED=$((COUNT_SKIPPED+1))
}
function runtest()
runtest()
{
testbin="$1"
testname="$2"
@ -52,7 +52,8 @@ function runtest()
echo -n "Running $testname... "
#exit $? to suppress shell message like "./test.sh: line 18: pid Bad system call"
(./$testbin "$testname" || exit $?) &>> "${test_log_file}"
(./$testbin "$testname" || exit $?) >> "${test_log_file}" 2>&1
ret=$?
SUCCESS="no"
if [ $ret -eq 0 ] ; then