Jämför commits
11 Incheckningar
769f729dc5
...
4cfdead5d0
Upphovsman | SHA1 | Datum | |
---|---|---|---|
4cfdead5d0 | |||
bbc8193ea9 | |||
c9fdeb4a1d | |||
3732524bfa | |||
4059c1a093 | |||
44b9a17bec | |||
f662398ac3 | |||
7b859d0aed | |||
5cd0a36ced | |||
618f223491 | |||
01c5cbf701 |
193
exile.c
193
exile.c
@ -621,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;
|
||||
}
|
||||
|
||||
@ -938,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)
|
||||
{
|
||||
@ -949,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)
|
||||
@ -1218,6 +1229,9 @@ 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_DIR;
|
||||
@ -1227,6 +1241,9 @@ static unsigned int exile_flags_to_landlock(unsigned int flags, int statmode)
|
||||
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)
|
||||
@ -1293,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)
|
||||
{
|
||||
@ -1311,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)
|
||||
{
|
||||
@ -1328,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)
|
||||
{
|
||||
@ -1481,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));
|
||||
@ -1532,7 +1607,7 @@ int exile_enable_policy(struct exile_policy *policy)
|
||||
close_file_fds();
|
||||
}
|
||||
|
||||
if(enter_namespaces(policy->namespace_options) < 0)
|
||||
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;
|
||||
@ -1615,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)
|
||||
{
|
||||
@ -1634,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)
|
||||
@ -1661,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)
|
||||
{
|
||||
@ -1681,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;
|
||||
}
|
||||
|
||||
@ -1879,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;
|
||||
|
3
exile.h
3
exile.h
@ -375,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;
|
||||
|
148
test.c
148
test.c
@ -618,9 +618,9 @@ int test_launch_get()
|
||||
size_t n = 0;
|
||||
char *content = exile_launch_get(¶ms, &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)
|
||||
@ -661,6 +661,146 @@ int test_clone3_nosys()
|
||||
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;
|
||||
@ -689,6 +829,10 @@ struct dispatcher dispatchers[] = {
|
||||
{ "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
23
test.sh
@ -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
|
||||
|
Referens i nytt ärende
Block a user